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(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,
121 skip_relationships: skip_relationships?(params)
126 POST /api/v1/statuses
128 Creates a scheduled status when `scheduled_at` param is present and it's far enough
132 assigns: %{user: user},
133 body_params: %{status: _, scheduled_at: scheduled_at} = params
137 when not is_nil(scheduled_at) do
138 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
141 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
142 scheduled_at: scheduled_at
145 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
146 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
148 |> put_view(ScheduledActivityView)
149 |> render("show.json", scheduled_activity: scheduled_activity)
152 params = Map.drop(params, [:scheduled_at])
153 create(%Plug.Conn{conn | body_params: params}, %{})
161 POST /api/v1/statuses
163 Creates a regular status
165 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
166 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
168 with {:ok, activity} <- CommonAPI.post(user, params) do
169 try_render(conn, "show.json",
173 with_direct_conversation_id: true
178 |> put_status(:unprocessable_entity)
179 |> json(%{error: message})
183 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
184 params = Map.put(params, :status, "")
185 create(%Plug.Conn{conn | body_params: params}, %{})
188 @doc "GET /api/v1/statuses/:id"
189 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
190 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
191 true <- Visibility.visible_for_user?(activity, user) do
192 try_render(conn, "show.json",
195 with_direct_conversation_id: true
198 _ -> {:error, :not_found}
202 @doc "DELETE /api/v1/statuses/:id"
203 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
204 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
207 {:error, :not_found} = e -> e
208 _e -> render_error(conn, :forbidden, "Can't delete this post")
212 @doc "POST /api/v1/statuses/:id/reblog"
213 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
214 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
215 %Activity{} = announce <- Activity.normalize(announce.data) do
216 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
220 @doc "POST /api/v1/statuses/:id/unreblog"
221 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
222 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
223 %Activity{} = activity <- Activity.get_by_id(activity_id) do
224 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
228 @doc "POST /api/v1/statuses/:id/favourite"
229 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
230 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
231 %Activity{} = activity <- Activity.get_by_id(activity_id) do
232 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
236 @doc "POST /api/v1/statuses/:id/unfavourite"
237 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
238 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
239 %Activity{} = activity <- Activity.get_by_id(activity_id) do
240 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
244 @doc "POST /api/v1/statuses/:id/pin"
245 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
246 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
247 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
251 @doc "POST /api/v1/statuses/:id/unpin"
252 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
253 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
254 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
258 @doc "POST /api/v1/statuses/:id/bookmark"
259 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
260 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
261 %User{} = user <- User.get_cached_by_nickname(user.nickname),
262 true <- Visibility.visible_for_user?(activity, user),
263 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
264 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
268 @doc "POST /api/v1/statuses/:id/unbookmark"
269 def unbookmark(%{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.destroy(user.id, activity.id) do
274 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
278 @doc "POST /api/v1/statuses/:id/mute"
279 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
280 with %Activity{} = activity <- Activity.get_by_id(id),
281 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
282 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
286 @doc "POST /api/v1/statuses/:id/unmute"
287 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
288 with %Activity{} = activity <- Activity.get_by_id(id),
289 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
290 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
294 @doc "GET /api/v1/statuses/:id/card"
295 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
296 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
297 with %Activity{} = activity <- Activity.get_by_id(status_id),
298 true <- Visibility.visible_for_user?(activity, user) do
299 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
300 render(conn, "card.json", data)
302 _ -> render_error(conn, :not_found, "Record not found")
306 @doc "GET /api/v1/statuses/:id/favourited_by"
307 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
308 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
309 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
310 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
313 |> Ecto.Query.where([u], u.ap_id in ^likes)
315 |> Enum.filter(&(not User.blocks?(user, &1)))
318 |> put_view(AccountView)
319 |> render("index.json", for: user, users: users, as: :user)
321 {:visible, false} -> {:error, :not_found}
326 @doc "GET /api/v1/statuses/:id/reblogged_by"
327 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
328 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
329 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
330 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
331 Object.normalize(activity) do
334 |> Activity.Queries.by_type()
335 |> Ecto.Query.where([a], a.actor in ^announces)
336 # this is to use the index
337 |> Activity.Queries.by_object_id(ap_id)
339 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
340 |> Enum.map(& &1.actor)
345 |> Ecto.Query.where([u], u.ap_id in ^announces)
347 |> Enum.filter(&(not User.blocks?(user, &1)))
350 |> put_view(AccountView)
351 |> render("index.json", for: user, users: users, as: :user)
353 {:visible, false} -> {:error, :not_found}
358 @doc "GET /api/v1/statuses/:id/context"
359 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
360 with %Activity{} = activity <- Activity.get_by_id(id) do
362 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
363 "blocking_user" => user,
365 "exclude_id" => activity.id
368 render(conn, "context.json", activity: activity, activities: activities, user: user)
372 @doc "GET /api/v1/favourites"
373 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
376 |> Map.new(fn {key, value} -> {to_string(key), value} end)
377 |> Map.take(Pleroma.Pagination.page_keys())
379 activities = ActivityPub.fetch_favourites(user, params)
382 |> add_link_headers(activities)
383 |> render("index.json",
384 activities: activities,
387 skip_relationships: skip_relationships?(params)
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,
410 skip_relationships: skip_relationships?(params)