Consistency
[akkoma] / lib / pleroma / web / mastodon_api / controllers / status_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.StatusController do
6 use Pleroma.Web, :controller
7
8 import Pleroma.Web.ControllerHelper,
9 only: [try_render: 3, add_link_headers: 2]
10
11 require Ecto.Query
12
13 alias Pleroma.Activity
14 alias Pleroma.Bookmark
15 alias Pleroma.Object
16 alias Pleroma.Repo
17 alias Pleroma.ScheduledActivity
18 alias Pleroma.User
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.Plugs.OAuthScopesPlug
25 alias Pleroma.Web.Plugs.RateLimiter
26
27 plug(Pleroma.Web.ApiSpec.CastAndValidate)
28
29 plug(
30 :skip_plug,
31 Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]
32 )
33
34 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
35
36 plug(
37 OAuthScopesPlug,
38 %{@unauthenticated_access | scopes: ["read:statuses"]}
39 when action in [
40 :index,
41 :show,
42 :card,
43 :context
44 ]
45 )
46
47 plug(
48 OAuthScopesPlug,
49 %{scopes: ["write:statuses"]}
50 when action in [
51 :create,
52 :delete,
53 :reblog,
54 :unreblog
55 ]
56 )
57
58 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
59
60 plug(
61 OAuthScopesPlug,
62 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
63 )
64
65 plug(
66 OAuthScopesPlug,
67 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
68 )
69
70 plug(
71 OAuthScopesPlug,
72 %{@unauthenticated_access | scopes: ["read:accounts"]}
73 when action in [:favourited_by, :reblogged_by]
74 )
75
76 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
77
78 # Note: scope not present in Mastodon: read:bookmarks
79 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
80
81 # Note: scope not present in Mastodon: write:bookmarks
82 plug(
83 OAuthScopesPlug,
84 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
85 )
86
87 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
88
89 plug(
90 RateLimiter,
91 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
92 when action in ~w(reblog unreblog)a
93 )
94
95 plug(
96 RateLimiter,
97 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
98 when action in ~w(favourite unfavourite)a
99 )
100
101 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
102
103 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
104
105 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
106
107 @doc """
108 GET `/api/v1/statuses?ids[]=1&ids[]=2`
109
110 `ids` query param is required
111 """
112 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
113 limit = 100
114
115 activities =
116 ids
117 |> Enum.take(limit)
118 |> Activity.all_by_ids_with_object()
119 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
120
121 render(conn, "index.json",
122 activities: activities,
123 for: user,
124 as: :activity,
125 with_muted: Map.get(params, :with_muted, false)
126 )
127 end
128
129 @doc """
130 POST /api/v1/statuses
131 """
132 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
133 def create(
134 %{
135 assigns: %{user: user},
136 body_params: %{status: _, scheduled_at: scheduled_at} = params
137 } = conn,
138 _
139 )
140 when not is_nil(scheduled_at) do
141 params =
142 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
143 |> put_application(conn)
144
145 attrs = %{
146 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
147 scheduled_at: scheduled_at
148 }
149
150 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
151 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
152 conn
153 |> put_view(ScheduledActivityView)
154 |> render("show.json", scheduled_activity: scheduled_activity)
155 else
156 {:far_enough, _} ->
157 params = Map.drop(params, [:scheduled_at])
158 create(%Plug.Conn{conn | body_params: params}, %{})
159
160 error ->
161 error
162 end
163 end
164
165 # Creates a regular status
166 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
167 params =
168 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
169 |> put_application(conn)
170
171 with {:ok, activity} <- CommonAPI.post(user, params) do
172 try_render(conn, "show.json",
173 activity: activity,
174 for: user,
175 as: :activity,
176 with_direct_conversation_id: true
177 )
178 else
179 {:error, {:reject, message}} ->
180 conn
181 |> put_status(:unprocessable_entity)
182 |> json(%{error: message})
183
184 {:error, message} ->
185 conn
186 |> put_status(:unprocessable_entity)
187 |> json(%{error: message})
188 end
189 end
190
191 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
192 params = Map.put(params, :status, "")
193 create(%Plug.Conn{conn | body_params: params}, %{})
194 end
195
196 @doc "GET /api/v1/statuses/:id"
197 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
198 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
199 true <- Visibility.visible_for_user?(activity, user) do
200 try_render(conn, "show.json",
201 activity: activity,
202 for: user,
203 with_direct_conversation_id: true,
204 with_muted: Map.get(params, :with_muted, false)
205 )
206 else
207 _ -> {:error, :not_found}
208 end
209 end
210
211 @doc "DELETE /api/v1/statuses/:id"
212 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
213 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
214 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
215 try_render(conn, "show.json",
216 activity: activity,
217 for: user,
218 with_direct_conversation_id: true,
219 with_source: true
220 )
221 else
222 _e -> {:error, :not_found}
223 end
224 end
225
226 @doc "POST /api/v1/statuses/:id/reblog"
227 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
228 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
229 %Activity{} = announce <- Activity.normalize(announce.data) do
230 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
231 end
232 end
233
234 @doc "POST /api/v1/statuses/:id/unreblog"
235 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
236 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
237 %Activity{} = activity <- Activity.get_by_id(activity_id) do
238 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
239 end
240 end
241
242 @doc "POST /api/v1/statuses/:id/favourite"
243 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
244 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
245 %Activity{} = activity <- Activity.get_by_id(activity_id) do
246 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
247 end
248 end
249
250 @doc "POST /api/v1/statuses/:id/unfavourite"
251 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
252 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
253 %Activity{} = activity <- Activity.get_by_id(activity_id) do
254 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
255 end
256 end
257
258 @doc "POST /api/v1/statuses/:id/pin"
259 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
260 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
261 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
262 end
263 end
264
265 @doc "POST /api/v1/statuses/:id/unpin"
266 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
267 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
268 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
269 end
270 end
271
272 @doc "POST /api/v1/statuses/:id/bookmark"
273 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
274 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
275 %User{} = user <- User.get_cached_by_nickname(user.nickname),
276 true <- Visibility.visible_for_user?(activity, user),
277 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
278 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
279 end
280 end
281
282 @doc "POST /api/v1/statuses/:id/unbookmark"
283 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
284 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
285 %User{} = user <- User.get_cached_by_nickname(user.nickname),
286 true <- Visibility.visible_for_user?(activity, user),
287 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
288 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
289 end
290 end
291
292 @doc "POST /api/v1/statuses/:id/mute"
293 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
294 with %Activity{} = activity <- Activity.get_by_id(id),
295 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
296 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
297 end
298 end
299
300 @doc "POST /api/v1/statuses/:id/unmute"
301 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
302 with %Activity{} = activity <- Activity.get_by_id(id),
303 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
304 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
305 end
306 end
307
308 @doc "GET /api/v1/statuses/:id/card"
309 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
310 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
311 with %Activity{} = activity <- Activity.get_by_id(status_id),
312 true <- Visibility.visible_for_user?(activity, user) do
313 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
314 render(conn, "card.json", data)
315 else
316 _ -> render_error(conn, :not_found, "Record not found")
317 end
318 end
319
320 @doc "GET /api/v1/statuses/:id/favourited_by"
321 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
322 with true <- Pleroma.Config.get([:instance, :show_reactions]),
323 %Activity{} = activity <- Activity.get_by_id_with_object(id),
324 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
325 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
326 users =
327 User
328 |> Ecto.Query.where([u], u.ap_id in ^likes)
329 |> Repo.all()
330 |> Enum.filter(&(not User.blocks?(user, &1)))
331
332 conn
333 |> put_view(AccountView)
334 |> render("index.json", for: user, users: users, as: :user)
335 else
336 {:visible, false} -> {:error, :not_found}
337 _ -> json(conn, [])
338 end
339 end
340
341 @doc "GET /api/v1/statuses/:id/reblogged_by"
342 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
343 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
344 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
345 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
346 Object.normalize(activity, fetch: false) do
347 announces =
348 "Announce"
349 |> Activity.Queries.by_type()
350 |> Ecto.Query.where([a], a.actor in ^announces)
351 # this is to use the index
352 |> Activity.Queries.by_object_id(ap_id)
353 |> Repo.all()
354 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
355 |> Enum.map(& &1.actor)
356 |> Enum.uniq()
357
358 users =
359 User
360 |> Ecto.Query.where([u], u.ap_id in ^announces)
361 |> Repo.all()
362 |> Enum.filter(&(not User.blocks?(user, &1)))
363
364 conn
365 |> put_view(AccountView)
366 |> render("index.json", for: user, users: users, as: :user)
367 else
368 {:visible, false} -> {:error, :not_found}
369 _ -> json(conn, [])
370 end
371 end
372
373 @doc "GET /api/v1/statuses/:id/context"
374 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
375 with %Activity{} = activity <- Activity.get_by_id(id) do
376 activities =
377 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
378 blocking_user: user,
379 user: user,
380 exclude_id: activity.id
381 })
382
383 render(conn, "context.json", activity: activity, activities: activities, user: user)
384 end
385 end
386
387 @doc "GET /api/v1/favourites"
388 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
389 activities = ActivityPub.fetch_favourites(user, params)
390
391 conn
392 |> add_link_headers(activities)
393 |> render("index.json",
394 activities: activities,
395 for: user,
396 as: :activity
397 )
398 end
399
400 @doc "GET /api/v1/bookmarks"
401 def bookmarks(%{assigns: %{user: user}} = conn, params) do
402 user = User.get_cached_by_id(user.id)
403
404 bookmarks =
405 user.id
406 |> Bookmark.for_user_query()
407 |> Pleroma.Pagination.fetch_paginated(params)
408
409 activities =
410 bookmarks
411 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
412
413 conn
414 |> add_link_headers(bookmarks)
415 |> render("index.json",
416 activities: activities,
417 for: user,
418 as: :activity
419 )
420 end
421
422 defp put_application(params, %{assigns: %{token: %{app_id: app_id}}} = _conn) do
423 Map.put(params, :application, Pleroma.Web.OAuth.App.get_app_by_id(app_id))
424 end
425
426 defp put_application(params, _), do: Map.put(params, :application, %{name: "Web", website: nil})
427 end