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, skip_relationships?: 1]
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(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
29 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
33 %{@unauthenticated_access | scopes: ["read:statuses"]}
44 %{scopes: ["write:statuses"]}
53 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
57 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
62 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
67 %{@unauthenticated_access | scopes: ["read:accounts"]}
68 when action in [:favourited_by, :reblogged_by]
71 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
73 # Note: scope not present in Mastodon: read:bookmarks
74 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
76 # Note: scope not present in Mastodon: write:bookmarks
79 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
82 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
86 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
87 when action in ~w(reblog unreblog)a
92 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
93 when action in ~w(favourite unfavourite)a
96 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
98 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
101 GET `/api/v1/statuses?ids[]=1&ids[]=2`
103 `ids` query param is required
105 def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do
111 |> Activity.all_by_ids_with_object()
112 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
114 render(conn, "index.json",
115 activities: activities,
118 skip_relationships: skip_relationships?(params)
123 POST /api/v1/statuses
125 Creates a scheduled status when `scheduled_at` param is present and it's far enough
128 %{assigns: %{user: user}} = conn,
129 %{"status" => _, "scheduled_at" => scheduled_at} = params
131 params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
133 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
134 attrs <- %{"params" => params, "scheduled_at" => scheduled_at},
135 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
137 |> put_view(ScheduledActivityView)
138 |> render("show.json", scheduled_activity: scheduled_activity)
141 create(conn, Map.drop(params, ["scheduled_at"]))
149 POST /api/v1/statuses
151 Creates a regular status
153 def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
154 params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
156 with {:ok, activity} <- CommonAPI.post(user, params) do
157 try_render(conn, "show.json",
161 with_direct_conversation_id: true
166 |> put_status(:unprocessable_entity)
167 |> json(%{error: message})
171 def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
172 create(conn, Map.put(params, "status", ""))
175 @doc "GET /api/v1/statuses/:id"
176 def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
177 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
178 true <- Visibility.visible_for_user?(activity, user) do
179 try_render(conn, "show.json",
182 with_direct_conversation_id: true
185 _ -> {:error, :not_found}
189 @doc "DELETE /api/v1/statuses/:id"
190 def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
191 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
194 {:error, :not_found} = e -> e
195 _e -> render_error(conn, :forbidden, "Can't delete this post")
199 @doc "POST /api/v1/statuses/:id/reblog"
200 def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
201 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
202 %Activity{} = announce <- Activity.normalize(announce.data) do
203 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
207 @doc "POST /api/v1/statuses/:id/unreblog"
208 def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
209 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
210 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
211 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
215 @doc "POST /api/v1/statuses/:id/favourite"
216 def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
217 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
218 %Activity{} = activity <- Activity.get_by_id(activity_id) do
219 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
223 @doc "POST /api/v1/statuses/:id/unfavourite"
224 def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
225 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
226 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
227 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
231 @doc "POST /api/v1/statuses/:id/pin"
232 def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
233 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
234 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
238 @doc "POST /api/v1/statuses/:id/unpin"
239 def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
240 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
241 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
245 @doc "POST /api/v1/statuses/:id/bookmark"
246 def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
247 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
248 %User{} = user <- User.get_cached_by_nickname(user.nickname),
249 true <- Visibility.visible_for_user?(activity, user),
250 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
251 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
255 @doc "POST /api/v1/statuses/:id/unbookmark"
256 def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
257 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
258 %User{} = user <- User.get_cached_by_nickname(user.nickname),
259 true <- Visibility.visible_for_user?(activity, user),
260 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
261 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
265 @doc "POST /api/v1/statuses/:id/mute"
266 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
267 with %Activity{} = activity <- Activity.get_by_id(id),
268 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
269 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
273 @doc "POST /api/v1/statuses/:id/unmute"
274 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
275 with %Activity{} = activity <- Activity.get_by_id(id),
276 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
277 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
281 @doc "GET /api/v1/statuses/:id/card"
282 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
283 def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
284 with %Activity{} = activity <- Activity.get_by_id(status_id),
285 true <- Visibility.visible_for_user?(activity, user) do
286 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
287 render(conn, "card.json", data)
289 _ -> render_error(conn, :not_found, "Record not found")
293 @doc "GET /api/v1/statuses/:id/favourited_by"
294 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
295 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
296 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
297 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
300 |> Ecto.Query.where([u], u.ap_id in ^likes)
302 |> Enum.filter(&(not User.blocks?(user, &1)))
305 |> put_view(AccountView)
306 |> render("index.json", for: user, users: users, as: :user)
308 {:visible, false} -> {:error, :not_found}
313 @doc "GET /api/v1/statuses/:id/reblogged_by"
314 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
315 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
316 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
317 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
318 Object.normalize(activity) do
321 |> Activity.Queries.by_type()
322 |> Ecto.Query.where([a], a.actor in ^announces)
323 # this is to use the index
324 |> Activity.Queries.by_object_id(ap_id)
326 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
327 |> Enum.map(& &1.actor)
332 |> Ecto.Query.where([u], u.ap_id in ^announces)
334 |> Enum.filter(&(not User.blocks?(user, &1)))
337 |> put_view(AccountView)
338 |> render("index.json", for: user, users: users, as: :user)
340 {:visible, false} -> {:error, :not_found}
345 @doc "GET /api/v1/statuses/:id/context"
346 def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
347 with %Activity{} = activity <- Activity.get_by_id(id) do
349 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
350 "blocking_user" => user,
352 "exclude_id" => activity.id
355 render(conn, "context.json", activity: activity, activities: activities, user: user)
359 @doc "GET /api/v1/favourites"
360 def favourites(%{assigns: %{user: user}} = conn, params) do
362 ActivityPub.fetch_favourites(
364 Map.take(params, Pleroma.Pagination.page_keys())
368 |> add_link_headers(activities)
369 |> render("index.json",
370 activities: activities,
373 skip_relationships: skip_relationships?(params)
377 @doc "GET /api/v1/bookmarks"
378 def bookmarks(%{assigns: %{user: user}} = conn, params) do
379 user = User.get_cached_by_id(user.id)
383 |> Bookmark.for_user_query()
384 |> Pleroma.Pagination.fetch_paginated(params)
388 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
391 |> add_link_headers(bookmarks)
392 |> render("index.json",
393 activities: activities,
396 skip_relationships: skip_relationships?(params)