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
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,
125 POST /api/v1/statuses
127 Creates a scheduled status when `scheduled_at` param is present and it's far enough
131 assigns: %{user: user},
132 body_params: %{status: _, scheduled_at: scheduled_at} = params
136 when not is_nil(scheduled_at) do
137 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
140 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
141 scheduled_at: scheduled_at
144 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
145 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
147 |> put_view(ScheduledActivityView)
148 |> render("show.json", scheduled_activity: scheduled_activity)
151 params = Map.drop(params, [:scheduled_at])
152 create(%Plug.Conn{conn | body_params: params}, %{})
160 POST /api/v1/statuses
162 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
177 |> put_status(:unprocessable_entity)
178 |> json(%{error: message})
182 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
183 params = Map.put(params, :status, "")
184 create(%Plug.Conn{conn | body_params: params}, %{})
187 @doc "GET /api/v1/statuses/:id"
188 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
189 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
190 true <- Visibility.visible_for_user?(activity, user) do
191 try_render(conn, "show.json",
194 with_direct_conversation_id: true
197 _ -> {:error, :not_found}
201 @doc "DELETE /api/v1/statuses/:id"
202 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
203 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
206 {:error, :not_found} = e -> e
207 _e -> render_error(conn, :forbidden, "Can't delete this post")
211 @doc "POST /api/v1/statuses/:id/reblog"
212 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
213 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
214 %Activity{} = announce <- Activity.normalize(announce.data) do
215 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
219 @doc "POST /api/v1/statuses/:id/unreblog"
220 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
221 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
222 %Activity{} = activity <- Activity.get_by_id(activity_id) do
223 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
227 @doc "POST /api/v1/statuses/:id/favourite"
228 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
229 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
230 %Activity{} = activity <- Activity.get_by_id(activity_id) do
231 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
235 @doc "POST /api/v1/statuses/:id/unfavourite"
236 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
237 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
238 %Activity{} = activity <- Activity.get_by_id(activity_id) do
239 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
243 @doc "POST /api/v1/statuses/:id/pin"
244 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
245 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
246 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
250 @doc "POST /api/v1/statuses/:id/unpin"
251 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
252 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
253 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
257 @doc "POST /api/v1/statuses/:id/bookmark"
258 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
259 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
260 %User{} = user <- User.get_cached_by_nickname(user.nickname),
261 true <- Visibility.visible_for_user?(activity, user),
262 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
263 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
267 @doc "POST /api/v1/statuses/:id/unbookmark"
268 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
269 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
270 %User{} = user <- User.get_cached_by_nickname(user.nickname),
271 true <- Visibility.visible_for_user?(activity, user),
272 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
273 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
277 @doc "POST /api/v1/statuses/:id/mute"
278 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
279 with %Activity{} = activity <- Activity.get_by_id(id),
280 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
281 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
285 @doc "POST /api/v1/statuses/:id/unmute"
286 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
287 with %Activity{} = activity <- Activity.get_by_id(id),
288 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
289 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
293 @doc "GET /api/v1/statuses/:id/card"
294 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
295 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
296 with %Activity{} = activity <- Activity.get_by_id(status_id),
297 true <- Visibility.visible_for_user?(activity, user) do
298 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
299 render(conn, "card.json", data)
301 _ -> render_error(conn, :not_found, "Record not found")
305 @doc "GET /api/v1/statuses/:id/favourited_by"
306 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
307 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
308 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
309 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
312 |> Ecto.Query.where([u], u.ap_id in ^likes)
314 |> Enum.filter(&(not User.blocks?(user, &1)))
317 |> put_view(AccountView)
318 |> render("index.json", for: user, users: users, as: :user)
320 {:visible, false} -> {:error, :not_found}
325 @doc "GET /api/v1/statuses/:id/reblogged_by"
326 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
327 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
328 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
329 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
330 Object.normalize(activity) do
333 |> Activity.Queries.by_type()
334 |> Ecto.Query.where([a], a.actor in ^announces)
335 # this is to use the index
336 |> Activity.Queries.by_object_id(ap_id)
338 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
339 |> Enum.map(& &1.actor)
344 |> Ecto.Query.where([u], u.ap_id in ^announces)
346 |> Enum.filter(&(not User.blocks?(user, &1)))
349 |> put_view(AccountView)
350 |> render("index.json", for: user, users: users, as: :user)
352 {:visible, false} -> {:error, :not_found}
357 @doc "GET /api/v1/statuses/:id/context"
358 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
359 with %Activity{} = activity <- Activity.get_by_id(id) do
361 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
362 "blocking_user" => user,
364 "exclude_id" => activity.id
367 render(conn, "context.json", activity: activity, activities: activities, user: user)
371 @doc "GET /api/v1/favourites"
372 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
375 |> Map.new(fn {key, value} -> {to_string(key), value} end)
376 |> Map.take(Pleroma.Pagination.page_keys())
378 activities = ActivityPub.fetch_favourites(user, params)
381 |> add_link_headers(activities)
382 |> render("index.json",
383 activities: activities,
389 @doc "GET /api/v1/bookmarks"
390 def bookmarks(%{assigns: %{user: user}} = conn, params) do
391 user = User.get_cached_by_id(user.id)
395 |> Bookmark.for_user_query()
396 |> Pleroma.Pagination.fetch_paginated(params)
400 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
403 |> add_link_headers(bookmarks)
404 |> render("index.json",
405 activities: activities,