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,
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
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}, %{})
163 # 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
175 {:error, {:reject, message}} ->
177 |> put_status(:unprocessable_entity)
178 |> json(%{error: message})
182 |> put_status(:unprocessable_entity)
183 |> json(%{error: message})
187 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
188 params = Map.put(params, :status, "")
189 create(%Plug.Conn{conn | body_params: params}, %{})
192 @doc "GET /api/v1/statuses/:id"
193 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
194 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
195 true <- Visibility.visible_for_user?(activity, user) do
196 try_render(conn, "show.json",
199 with_direct_conversation_id: true,
200 with_muted: Map.get(params, :with_muted, false)
203 _ -> {:error, :not_found}
207 @doc "DELETE /api/v1/statuses/:id"
208 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
209 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
210 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
211 try_render(conn, "show.json",
214 with_direct_conversation_id: true,
218 _e -> {:error, :not_found}
222 @doc "POST /api/v1/statuses/:id/reblog"
223 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
224 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
225 %Activity{} = announce <- Activity.normalize(announce.data) do
226 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
230 @doc "POST /api/v1/statuses/:id/unreblog"
231 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
232 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
233 %Activity{} = activity <- Activity.get_by_id(activity_id) do
234 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
238 @doc "POST /api/v1/statuses/:id/favourite"
239 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
240 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
241 %Activity{} = activity <- Activity.get_by_id(activity_id) do
242 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
246 @doc "POST /api/v1/statuses/:id/unfavourite"
247 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
248 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
249 %Activity{} = activity <- Activity.get_by_id(activity_id) do
250 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
254 @doc "POST /api/v1/statuses/:id/pin"
255 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
256 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
257 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
261 @doc "POST /api/v1/statuses/:id/unpin"
262 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
263 with {:ok, activity} <- CommonAPI.unpin(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/bookmark"
269 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
270 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
271 %User{} = user <- User.get_cached_by_nickname(user.nickname),
272 true <- Visibility.visible_for_user?(activity, user),
273 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
274 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
278 @doc "POST /api/v1/statuses/:id/unbookmark"
279 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
280 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
281 %User{} = user <- User.get_cached_by_nickname(user.nickname),
282 true <- Visibility.visible_for_user?(activity, user),
283 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
284 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
288 @doc "POST /api/v1/statuses/:id/mute"
289 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
290 with %Activity{} = activity <- Activity.get_by_id(id),
291 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
292 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
296 @doc "POST /api/v1/statuses/:id/unmute"
297 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
298 with %Activity{} = activity <- Activity.get_by_id(id),
299 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
300 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
304 @doc "GET /api/v1/statuses/:id/card"
305 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
306 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
307 with %Activity{} = activity <- Activity.get_by_id(status_id),
308 true <- Visibility.visible_for_user?(activity, user) do
309 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
310 render(conn, "card.json", data)
312 _ -> render_error(conn, :not_found, "Record not found")
316 @doc "GET /api/v1/statuses/:id/favourited_by"
317 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
318 with true <- Pleroma.Config.get([:instance, :show_reactions]),
319 %Activity{} = activity <- Activity.get_by_id_with_object(id),
320 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
321 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
324 |> Ecto.Query.where([u], u.ap_id in ^likes)
326 |> Enum.filter(&(not User.blocks?(user, &1)))
329 |> put_view(AccountView)
330 |> render("index.json", for: user, users: users, as: :user)
332 {:visible, false} -> {:error, :not_found}
337 @doc "GET /api/v1/statuses/:id/reblogged_by"
338 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
339 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
340 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
341 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
342 Object.normalize(activity, fetch: false) do
345 |> Activity.Queries.by_type()
346 |> Ecto.Query.where([a], a.actor in ^announces)
347 # this is to use the index
348 |> Activity.Queries.by_object_id(ap_id)
350 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
351 |> Enum.map(& &1.actor)
356 |> Ecto.Query.where([u], u.ap_id in ^announces)
358 |> Enum.filter(&(not User.blocks?(user, &1)))
361 |> put_view(AccountView)
362 |> render("index.json", for: user, users: users, as: :user)
364 {:visible, false} -> {:error, :not_found}
369 @doc "GET /api/v1/statuses/:id/context"
370 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
371 with %Activity{} = activity <- Activity.get_by_id(id) do
373 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
376 exclude_id: activity.id
379 render(conn, "context.json", activity: activity, activities: activities, user: user)
383 @doc "GET /api/v1/favourites"
384 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
385 activities = ActivityPub.fetch_favourites(user, params)
388 |> add_link_headers(activities)
389 |> render("index.json",
390 activities: activities,
396 @doc "GET /api/v1/bookmarks"
397 def bookmarks(%{assigns: %{user: user}} = conn, params) do
398 user = User.get_cached_by_id(user.id)
402 |> Bookmark.for_user_query()
403 |> Pleroma.Pagination.fetch_paginated(params)
407 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
410 |> add_link_headers(bookmarks)
411 |> render("index.json",
412 activities: activities,