1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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.OAuth.Token
25 alias Pleroma.Web.Plugs.OAuthScopesPlug
26 alias Pleroma.Web.Plugs.RateLimiter
28 plug(Pleroma.Web.ApiSpec.CastAndValidate)
30 plug(:skip_public_check when action in [:index, :show])
32 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
36 %{@unauthenticated_access | scopes: ["read:statuses"]}
46 %{scopes: ["write:statuses"]}
55 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
59 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
64 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
69 %{@unauthenticated_access | scopes: ["read:accounts"]}
70 when action in [:favourited_by, :reblogged_by]
73 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
75 # Note: scope not present in Mastodon: read:bookmarks
76 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
78 # Note: scope not present in Mastodon: write:bookmarks
81 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
84 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
88 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
89 when action in ~w(reblog unreblog)a
94 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
95 when action in ~w(favourite unfavourite)a
98 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
100 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
102 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
105 GET `/api/v1/statuses?ids[]=1&ids[]=2`
107 `ids` query param is required
109 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
115 |> Activity.all_by_ids_with_object()
116 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
118 render(conn, "index.json",
119 activities: activities,
122 with_muted: Map.get(params, :with_muted, false)
127 POST /api/v1/statuses
129 # 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
139 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
140 |> put_application(conn)
143 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
144 scheduled_at: scheduled_at
147 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
148 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
150 |> put_view(ScheduledActivityView)
151 |> render("show.json", scheduled_activity: scheduled_activity)
154 params = Map.drop(params, [:scheduled_at])
155 create(%Plug.Conn{conn | body_params: params}, %{})
162 # Creates a regular status
163 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
165 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
166 |> put_application(conn)
168 with {:ok, activity} <- CommonAPI.post(user, params) do
169 try_render(conn, "show.json",
173 with_direct_conversation_id: true
176 {:error, {:reject, message}} ->
178 |> put_status(:unprocessable_entity)
179 |> json(%{error: message})
183 |> put_status(:unprocessable_entity)
184 |> json(%{error: message})
188 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
189 params = Map.put(params, :status, "")
190 create(%Plug.Conn{conn | body_params: params}, %{})
193 @doc "GET /api/v1/statuses/:id"
194 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
195 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
196 true <- Visibility.visible_for_user?(activity, user) do
197 try_render(conn, "show.json",
200 with_direct_conversation_id: true,
201 with_muted: Map.get(params, :with_muted, false)
204 _ -> {:error, :not_found}
208 @doc "DELETE /api/v1/statuses/:id"
209 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
210 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
211 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
212 try_render(conn, "show.json",
215 with_direct_conversation_id: true,
219 _e -> {:error, :not_found}
223 @doc "POST /api/v1/statuses/:id/reblog"
224 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
225 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
226 %Activity{} = announce <- Activity.normalize(announce.data) do
227 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
231 @doc "POST /api/v1/statuses/:id/unreblog"
232 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
233 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
234 %Activity{} = activity <- Activity.get_by_id(activity_id) do
235 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
239 @doc "POST /api/v1/statuses/:id/favourite"
240 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
241 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
242 %Activity{} = activity <- Activity.get_by_id(activity_id) do
243 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
247 @doc "POST /api/v1/statuses/:id/unfavourite"
248 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
249 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
250 %Activity{} = activity <- Activity.get_by_id(activity_id) do
251 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
255 @doc "POST /api/v1/statuses/:id/pin"
256 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
257 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
258 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
260 {:error, :pinned_statuses_limit_reached} ->
261 {:error, "You have already pinned the maximum number of statuses"}
263 {:error, :ownership_error} ->
264 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
266 {:error, :visibility_error} ->
267 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
274 @doc "POST /api/v1/statuses/:id/unpin"
275 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
276 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
277 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
281 @doc "POST /api/v1/statuses/:id/bookmark"
282 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
283 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
284 %User{} = user <- User.get_cached_by_nickname(user.nickname),
285 true <- Visibility.visible_for_user?(activity, user),
286 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
287 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
291 @doc "POST /api/v1/statuses/:id/unbookmark"
292 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
293 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
294 %User{} = user <- User.get_cached_by_nickname(user.nickname),
295 true <- Visibility.visible_for_user?(activity, user),
296 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
297 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
301 @doc "POST /api/v1/statuses/:id/mute"
302 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
303 with %Activity{} = activity <- Activity.get_by_id(id),
304 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
305 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
309 @doc "POST /api/v1/statuses/:id/unmute"
310 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
311 with %Activity{} = activity <- Activity.get_by_id(id),
312 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
313 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
317 @doc "GET /api/v1/statuses/:id/favourited_by"
318 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
319 with true <- Pleroma.Config.get([:instance, :show_reactions]),
320 %Activity{} = activity <- Activity.get_by_id_with_object(id),
321 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
322 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
325 |> Ecto.Query.where([u], u.ap_id in ^likes)
327 |> Enum.filter(&(not User.blocks?(user, &1)))
330 |> put_view(AccountView)
331 |> render("index.json", for: user, users: users, as: :user)
333 {:visible, false} -> {:error, :not_found}
338 @doc "GET /api/v1/statuses/:id/reblogged_by"
339 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
340 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
341 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
342 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
343 Object.normalize(activity, fetch: false) do
346 |> Activity.Queries.by_type()
347 |> Ecto.Query.where([a], a.actor in ^announces)
348 # this is to use the index
349 |> Activity.Queries.by_object_id(ap_id)
351 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
352 |> Enum.map(& &1.actor)
357 |> Ecto.Query.where([u], u.ap_id in ^announces)
359 |> Enum.filter(&(not User.blocks?(user, &1)))
362 |> put_view(AccountView)
363 |> render("index.json", for: user, users: users, as: :user)
365 {:visible, false} -> {:error, :not_found}
370 @doc "GET /api/v1/statuses/:id/context"
371 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
372 with %Activity{} = activity <- Activity.get_by_id(id) do
374 activity.data["context"]
375 |> ActivityPub.fetch_activities_for_context(%{
378 exclude_id: activity.id
380 |> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
382 render(conn, "context.json", activity: activity, activities: activities, user: user)
386 @doc "GET /api/v1/favourites"
387 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
388 activities = ActivityPub.fetch_favourites(user, params)
391 |> add_link_headers(activities)
392 |> render("index.json",
393 activities: activities,
399 @doc "GET /api/v1/bookmarks"
400 def bookmarks(%{assigns: %{user: user}} = conn, params) do
401 user = User.get_cached_by_id(user.id)
405 |> Bookmark.for_user_query()
406 |> Pleroma.Pagination.fetch_paginated(params)
410 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
413 |> add_link_headers(bookmarks)
414 |> render("index.json",
415 activities: activities,
421 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
422 if user.disclose_client do
423 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
424 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
426 Map.put(params, :generator, nil)
430 defp put_application(params, _), do: Map.put(params, :generator, nil)