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 when not is_nil(scheduled_at) do
132 params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
134 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
135 attrs <- %{"params" => params, "scheduled_at" => scheduled_at},
136 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
138 |> put_view(ScheduledActivityView)
139 |> render("show.json", scheduled_activity: scheduled_activity)
142 create(conn, Map.drop(params, ["scheduled_at"]))
150 POST /api/v1/statuses
152 Creates a regular status
154 def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
155 params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
157 with {:ok, activity} <- CommonAPI.post(user, params) do
158 try_render(conn, "show.json",
162 with_direct_conversation_id: true
167 |> put_status(:unprocessable_entity)
168 |> json(%{error: message})
172 def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
173 create(conn, Map.put(params, "status", ""))
176 @doc "GET /api/v1/statuses/:id"
177 def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
178 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
179 true <- Visibility.visible_for_user?(activity, user) do
180 try_render(conn, "show.json",
183 with_direct_conversation_id: true
186 _ -> {:error, :not_found}
190 @doc "DELETE /api/v1/statuses/:id"
191 def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
192 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
195 {:error, :not_found} = e -> e
196 _e -> render_error(conn, :forbidden, "Can't delete this post")
200 @doc "POST /api/v1/statuses/:id/reblog"
201 def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
202 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
203 %Activity{} = announce <- Activity.normalize(announce.data) do
204 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
208 @doc "POST /api/v1/statuses/:id/unreblog"
209 def unreblog(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
210 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
211 %Activity{} = activity <- Activity.get_by_id(activity_id) do
212 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
216 @doc "POST /api/v1/statuses/:id/favourite"
217 def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
218 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
219 %Activity{} = activity <- Activity.get_by_id(activity_id) do
220 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
224 @doc "POST /api/v1/statuses/:id/unfavourite"
225 def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
226 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
227 %Activity{} = activity <- Activity.get_by_id(activity_id) do
228 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
232 @doc "POST /api/v1/statuses/:id/pin"
233 def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
234 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
235 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
239 @doc "POST /api/v1/statuses/:id/unpin"
240 def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
241 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
242 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
246 @doc "POST /api/v1/statuses/:id/bookmark"
247 def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
248 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
249 %User{} = user <- User.get_cached_by_nickname(user.nickname),
250 true <- Visibility.visible_for_user?(activity, user),
251 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
252 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
256 @doc "POST /api/v1/statuses/:id/unbookmark"
257 def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
258 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
259 %User{} = user <- User.get_cached_by_nickname(user.nickname),
260 true <- Visibility.visible_for_user?(activity, user),
261 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
262 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
266 @doc "POST /api/v1/statuses/:id/mute"
267 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
268 with %Activity{} = activity <- Activity.get_by_id(id),
269 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
270 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
274 @doc "POST /api/v1/statuses/:id/unmute"
275 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
276 with %Activity{} = activity <- Activity.get_by_id(id),
277 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
278 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
282 @doc "GET /api/v1/statuses/:id/card"
283 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
284 def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
285 with %Activity{} = activity <- Activity.get_by_id(status_id),
286 true <- Visibility.visible_for_user?(activity, user) do
287 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
288 render(conn, "card.json", data)
290 _ -> render_error(conn, :not_found, "Record not found")
294 @doc "GET /api/v1/statuses/:id/favourited_by"
295 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
296 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
297 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
298 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
301 |> Ecto.Query.where([u], u.ap_id in ^likes)
303 |> Enum.filter(&(not User.blocks?(user, &1)))
306 |> put_view(AccountView)
307 |> render("index.json", for: user, users: users, as: :user)
309 {:visible, false} -> {:error, :not_found}
314 @doc "GET /api/v1/statuses/:id/reblogged_by"
315 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
316 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
317 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
318 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
319 Object.normalize(activity) do
322 |> Activity.Queries.by_type()
323 |> Ecto.Query.where([a], a.actor in ^announces)
324 # this is to use the index
325 |> Activity.Queries.by_object_id(ap_id)
327 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
328 |> Enum.map(& &1.actor)
333 |> Ecto.Query.where([u], u.ap_id in ^announces)
335 |> Enum.filter(&(not User.blocks?(user, &1)))
338 |> put_view(AccountView)
339 |> render("index.json", for: user, users: users, as: :user)
341 {:visible, false} -> {:error, :not_found}
346 @doc "GET /api/v1/statuses/:id/context"
347 def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
348 with %Activity{} = activity <- Activity.get_by_id(id) do
350 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
351 "blocking_user" => user,
353 "exclude_id" => activity.id
356 render(conn, "context.json", activity: activity, activities: activities, user: user)
360 @doc "GET /api/v1/favourites"
361 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
363 ActivityPub.fetch_favourites(
365 Map.take(params, Pleroma.Pagination.page_keys())
369 |> add_link_headers(activities)
370 |> render("index.json",
371 activities: activities,
374 skip_relationships: skip_relationships?(params)
378 @doc "GET /api/v1/bookmarks"
379 def bookmarks(%{assigns: %{user: user}} = conn, params) do
380 user = User.get_cached_by_id(user.id)
384 |> Bookmark.for_user_query()
385 |> Pleroma.Pagination.fetch_paginated(params)
389 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
392 |> add_link_headers(bookmarks)
393 |> render("index.json",
394 activities: activities,
397 skip_relationships: skip_relationships?(params)