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
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,
129 POST /api/v1/statuses
131 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
141 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
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}, %{})
164 POST /api/v1/statuses
166 Creates a regular status
168 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
169 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
171 with {:ok, activity} <- CommonAPI.post(user, params) do
172 try_render(conn, "show.json",
176 with_direct_conversation_id: true
179 {:error, {:reject, message}} ->
181 |> put_status(:unprocessable_entity)
182 |> json(%{error: message})
186 |> put_status(:unprocessable_entity)
187 |> json(%{error: message})
191 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
192 params = Map.put(params, :status, "")
193 create(%Plug.Conn{conn | body_params: params}, %{})
196 @doc "GET /api/v1/statuses/:id"
197 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
198 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
199 true <- Visibility.visible_for_user?(activity, user) do
200 try_render(conn, "show.json",
203 with_direct_conversation_id: true
206 _ -> {:error, :not_found}
210 @doc "DELETE /api/v1/statuses/:id"
211 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
212 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
213 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
214 try_render(conn, "show.json",
217 with_direct_conversation_id: true,
221 _e -> {:error, :not_found}
225 @doc "POST /api/v1/statuses/:id/reblog"
226 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
227 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
228 %Activity{} = announce <- Activity.normalize(announce.data) do
229 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
233 @doc "POST /api/v1/statuses/:id/unreblog"
234 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
235 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
236 %Activity{} = activity <- Activity.get_by_id(activity_id) do
237 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
241 @doc "POST /api/v1/statuses/:id/favourite"
242 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
243 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
244 %Activity{} = activity <- Activity.get_by_id(activity_id) do
245 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
249 @doc "POST /api/v1/statuses/:id/unfavourite"
250 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
251 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
252 %Activity{} = activity <- Activity.get_by_id(activity_id) do
253 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
257 @doc "POST /api/v1/statuses/:id/pin"
258 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
259 with {:ok, activity} <- CommonAPI.pin(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/unpin"
265 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
266 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
267 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
271 @doc "POST /api/v1/statuses/:id/bookmark"
272 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
273 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
274 %User{} = user <- User.get_cached_by_nickname(user.nickname),
275 true <- Visibility.visible_for_user?(activity, user),
276 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
277 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
281 @doc "POST /api/v1/statuses/:id/unbookmark"
282 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
283 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
284 %User{} = user <- User.get_cached_by_nickname(user.nickname),
285 true <- Visibility.visible_for_user?(activity, user),
286 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
287 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
291 @doc "POST /api/v1/statuses/:id/mute"
292 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
293 with %Activity{} = activity <- Activity.get_by_id(id),
294 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
295 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
299 @doc "POST /api/v1/statuses/:id/unmute"
300 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
301 with %Activity{} = activity <- Activity.get_by_id(id),
302 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
303 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
307 @doc "GET /api/v1/statuses/:id/card"
308 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
309 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
310 with %Activity{} = activity <- Activity.get_by_id(status_id),
311 true <- Visibility.visible_for_user?(activity, user) do
312 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
313 render(conn, "card.json", data)
315 _ -> render_error(conn, :not_found, "Record not found")
319 @doc "GET /api/v1/statuses/:id/favourited_by"
320 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
321 with true <- Pleroma.Config.get([:instance, :show_reactions]),
322 %Activity{} = activity <- Activity.get_by_id_with_object(id),
323 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
324 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
327 |> Ecto.Query.where([u], u.ap_id in ^likes)
329 |> Enum.filter(&(not User.blocks?(user, &1)))
332 |> put_view(AccountView)
333 |> render("index.json", for: user, users: users, as: :user)
335 {:visible, false} -> {:error, :not_found}
340 @doc "GET /api/v1/statuses/:id/reblogged_by"
341 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
342 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
343 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
344 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
345 Object.normalize(activity) do
348 |> Activity.Queries.by_type()
349 |> Ecto.Query.where([a], a.actor in ^announces)
350 # this is to use the index
351 |> Activity.Queries.by_object_id(ap_id)
353 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
354 |> Enum.map(& &1.actor)
359 |> Ecto.Query.where([u], u.ap_id in ^announces)
361 |> Enum.filter(&(not User.blocks?(user, &1)))
364 |> put_view(AccountView)
365 |> render("index.json", for: user, users: users, as: :user)
367 {:visible, false} -> {:error, :not_found}
372 @doc "GET /api/v1/statuses/:id/context"
373 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
374 with %Activity{} = activity <- Activity.get_by_id(id) do
376 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
379 exclude_id: activity.id
382 render(conn, "context.json", activity: activity, activities: activities, user: user)
386 @doc "GET /api/v1/favourites"
387 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
388 activities = ActivityPub.fetch_favourites(user, params)
391 |> add_link_headers(activities)
392 |> render("index.json",
393 activities: activities,
399 @doc "GET /api/v1/bookmarks"
400 def bookmarks(%{assigns: %{user: user}} = conn, params) do
401 user = User.get_cached_by_id(user.id)
405 |> Bookmark.for_user_query()
406 |> Pleroma.Pagination.fetch_paginated(params)
410 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
413 |> add_link_headers(bookmarks)
414 |> render("index.json",
415 activities: activities,