docs: Remove quarantine section
[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.Config
18 alias Pleroma.ScheduledActivity
19 alias Pleroma.User
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.AccountView
24 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
25 alias Pleroma.Web.OAuth.Token
26 alias Pleroma.Web.Plugs.OAuthScopesPlug
27 alias Pleroma.Web.Plugs.RateLimiter
28
29 plug(Pleroma.Web.ApiSpec.CastAndValidate)
30
31 plug(:skip_public_check when action in [:index, :show])
32
33 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
34 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
35
36 plug(
37 OAuthScopesPlug,
38 %{@unauthenticated_access | scopes: ["read:statuses"]}
39 when action in [
40 :index,
41 :show,
42 :context,
43 :translate,
44 :show_history,
45 :show_source
46 ]
47 )
48
49 plug(
50 OAuthScopesPlug,
51 %{scopes: ["write:statuses"]}
52 when action in [
53 :create,
54 :delete,
55 :reblog,
56 :unreblog,
57 :update
58 ]
59 )
60
61 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
62
63 plug(
64 OAuthScopesPlug,
65 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
66 )
67
68 plug(
69 OAuthScopesPlug,
70 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
71 )
72
73 plug(
74 OAuthScopesPlug,
75 %{@unauthenticated_access | scopes: ["read:accounts"]}
76 when action in [:favourited_by, :reblogged_by]
77 )
78
79 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
80
81 # Note: scope not present in Mastodon: read:bookmarks
82 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
83
84 # Note: scope not present in Mastodon: write:bookmarks
85 plug(
86 OAuthScopesPlug,
87 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
88 )
89
90 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
91
92 plug(
93 RateLimiter,
94 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
95 when action in ~w(reblog unreblog)a
96 )
97
98 plug(
99 RateLimiter,
100 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
101 when action in ~w(favourite unfavourite)a
102 )
103
104 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
105
106 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
107
108 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
109
110 @doc """
111 GET `/api/v1/statuses?ids[]=1&ids[]=2`
112
113 `ids` query param is required
114 """
115 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
116 limit = 100
117
118 activities =
119 ids
120 |> Enum.take(limit)
121 |> Activity.all_by_ids_with_object()
122 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
123
124 render(conn, "index.json",
125 activities: activities,
126 for: user,
127 as: :activity,
128 with_muted: Map.get(params, :with_muted, false)
129 )
130 end
131
132 @doc """
133 POST /api/v1/statuses
134 """
135 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
136 def create(
137 %{
138 assigns: %{user: user},
139 body_params: %{status: _, scheduled_at: scheduled_at} = params
140 } = conn,
141 _
142 )
143 when not is_nil(scheduled_at) do
144 params =
145 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
146 |> put_application(conn)
147
148 attrs = %{
149 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
150 scheduled_at: scheduled_at
151 }
152
153 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
154 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
155 conn
156 |> put_view(ScheduledActivityView)
157 |> render("show.json", scheduled_activity: scheduled_activity)
158 else
159 {:far_enough, _} ->
160 params = Map.drop(params, [:scheduled_at])
161 create(%Plug.Conn{conn | body_params: params}, %{})
162
163 error ->
164 error
165 end
166 end
167
168 # Creates a regular status
169 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
170 params =
171 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
172 |> put_application(conn)
173
174 with {:ok, activity} <- CommonAPI.post(user, params) do
175 try_render(conn, "show.json",
176 activity: activity,
177 for: user,
178 as: :activity,
179 with_direct_conversation_id: true
180 )
181 else
182 {:error, {:reject, message}} ->
183 conn
184 |> put_status(:unprocessable_entity)
185 |> json(%{error: message})
186
187 {:error, message} ->
188 conn
189 |> put_status(:unprocessable_entity)
190 |> json(%{error: message})
191 end
192 end
193
194 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
195 params = Map.put(params, :status, "")
196 create(%Plug.Conn{conn | body_params: params}, %{})
197 end
198
199 @doc "GET /api/v1/statuses/:id/history"
200 def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
201 with user = assigns[:user],
202 %Activity{} = activity <- Activity.get_by_id_with_object(id),
203 true <- Visibility.visible_for_user?(activity, user) do
204 try_render(conn, "history.json",
205 activity: activity,
206 for: user,
207 with_direct_conversation_id: true,
208 with_muted: Map.get(params, :with_muted, false)
209 )
210 else
211 _ -> {:error, :not_found}
212 end
213 end
214
215 @doc "GET /api/v1/statuses/:id/source"
216 def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
217 with user = assigns[:user],
218 %Activity{} = activity <- Activity.get_by_id_with_object(id),
219 true <- Visibility.visible_for_user?(activity, user) do
220 try_render(conn, "source.json",
221 activity: activity,
222 for: user
223 )
224 else
225 _ -> {:error, :not_found}
226 end
227 end
228
229 @doc "PUT /api/v1/statuses/:id"
230 def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
231 with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
232 {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
233 {_, true} <- {:is_create, activity.data["type"] == "Create"},
234 actor <- Activity.user_actor(activity),
235 {_, true} <- {:own_status, actor.id == user.id},
236 changes <- body_params |> put_application(conn),
237 {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
238 {_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
239 try_render(conn, "show.json",
240 activity: activity,
241 for: user,
242 with_direct_conversation_id: true,
243 with_muted: Map.get(params, :with_muted, false)
244 )
245 else
246 {:own_status, _} -> {:error, :forbidden}
247 {:pipeline, _} -> {:error, :internal_server_error}
248 _ -> {:error, :not_found}
249 end
250 end
251
252 @doc "GET /api/v1/statuses/:id"
253 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
254 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
255 true <- Visibility.visible_for_user?(activity, user) do
256 try_render(conn, "show.json",
257 activity: activity,
258 for: user,
259 with_direct_conversation_id: true,
260 with_muted: Map.get(params, :with_muted, false)
261 )
262 else
263 _ -> {:error, :not_found}
264 end
265 end
266
267 @doc "DELETE /api/v1/statuses/:id"
268 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
269 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
270 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
271 try_render(conn, "show.json",
272 activity: activity,
273 for: user,
274 with_direct_conversation_id: true,
275 with_source: true
276 )
277 else
278 _e -> {:error, :not_found}
279 end
280 end
281
282 @doc "POST /api/v1/statuses/:id/reblog"
283 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
284 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
285 %Activity{} = announce <- Activity.normalize(announce.data) do
286 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
287 end
288 end
289
290 @doc "POST /api/v1/statuses/:id/unreblog"
291 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
292 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
293 %Activity{} = activity <- Activity.get_by_id(activity_id) do
294 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
295 end
296 end
297
298 @doc "POST /api/v1/statuses/:id/favourite"
299 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
300 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
301 %Activity{} = activity <- Activity.get_by_id(activity_id) do
302 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
303 end
304 end
305
306 @doc "POST /api/v1/statuses/:id/unfavourite"
307 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
308 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
309 %Activity{} = activity <- Activity.get_by_id(activity_id) do
310 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
311 end
312 end
313
314 @doc "POST /api/v1/statuses/:id/pin"
315 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
316 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
317 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
318 else
319 {:error, :pinned_statuses_limit_reached} ->
320 {:error, "You have already pinned the maximum number of statuses"}
321
322 {:error, :ownership_error} ->
323 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
324
325 {:error, :visibility_error} ->
326 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
327
328 error ->
329 error
330 end
331 end
332
333 @doc "POST /api/v1/statuses/:id/unpin"
334 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
335 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
336 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
337 end
338 end
339
340 @doc "POST /api/v1/statuses/:id/bookmark"
341 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
342 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
343 %User{} = user <- User.get_cached_by_nickname(user.nickname),
344 true <- Visibility.visible_for_user?(activity, user),
345 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
346 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
347 end
348 end
349
350 @doc "POST /api/v1/statuses/:id/unbookmark"
351 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
352 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
353 %User{} = user <- User.get_cached_by_nickname(user.nickname),
354 true <- Visibility.visible_for_user?(activity, user),
355 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
356 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
357 end
358 end
359
360 @doc "POST /api/v1/statuses/:id/mute"
361 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
362 with %Activity{} = activity <- Activity.get_by_id(id),
363 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
364 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
365 end
366 end
367
368 @doc "POST /api/v1/statuses/:id/unmute"
369 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
370 with %Activity{} = activity <- Activity.get_by_id(id),
371 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
372 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
373 end
374 end
375
376 @doc "GET /api/v1/statuses/:id/favourited_by"
377 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
378 with true <- Pleroma.Config.get([:instance, :show_reactions]),
379 %Activity{} = activity <- Activity.get_by_id_with_object(id),
380 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
381 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
382 users =
383 User
384 |> Ecto.Query.where([u], u.ap_id in ^likes)
385 |> Repo.all()
386 |> Enum.filter(&(not User.blocks?(user, &1)))
387
388 conn
389 |> put_view(AccountView)
390 |> render("index.json", for: user, users: users, as: :user)
391 else
392 {:visible, false} -> {:error, :not_found}
393 _ -> json(conn, [])
394 end
395 end
396
397 @doc "GET /api/v1/statuses/:id/reblogged_by"
398 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
399 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
400 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
401 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
402 Object.normalize(activity, fetch: false) do
403 announces =
404 "Announce"
405 |> Activity.Queries.by_type()
406 |> Ecto.Query.where([a], a.actor in ^announces)
407 # this is to use the index
408 |> Activity.Queries.by_object_id(ap_id)
409 |> Repo.all()
410 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
411 |> Enum.map(& &1.actor)
412 |> Enum.uniq()
413
414 users =
415 User
416 |> Ecto.Query.where([u], u.ap_id in ^announces)
417 |> Repo.all()
418 |> Enum.filter(&(not User.blocks?(user, &1)))
419
420 conn
421 |> put_view(AccountView)
422 |> render("index.json", for: user, users: users, as: :user)
423 else
424 {:visible, false} -> {:error, :not_found}
425 _ -> json(conn, [])
426 end
427 end
428
429 @doc "GET /api/v1/statuses/:id/context"
430 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
431 with %Activity{} = activity <- Activity.get_by_id(id) do
432 activities =
433 activity.data["context"]
434 |> ActivityPub.fetch_activities_for_context(%{
435 blocking_user: user,
436 user: user,
437 exclude_id: activity.id
438 })
439 |> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
440
441 render(conn, "context.json", activity: activity, activities: activities, user: user)
442 end
443 end
444
445 @doc "GET /api/v1/favourites"
446 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
447 activities = ActivityPub.fetch_favourites(user, params)
448
449 conn
450 |> add_link_headers(activities)
451 |> render("index.json",
452 activities: activities,
453 for: user,
454 as: :activity
455 )
456 end
457
458 @doc "GET /api/v1/bookmarks"
459 def bookmarks(%{assigns: %{user: user}} = conn, params) do
460 user = User.get_cached_by_id(user.id)
461
462 bookmarks =
463 user.id
464 |> Bookmark.for_user_query()
465 |> Pleroma.Pagination.fetch_paginated(params)
466
467 activities =
468 bookmarks
469 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
470
471 conn
472 |> add_link_headers(bookmarks)
473 |> render("index.json",
474 activities: activities,
475 for: user,
476 as: :activity
477 )
478 end
479
480 @doc "GET /api/v1/statuses/:id/translations/:language"
481 def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language} = params) do
482 with {:enabled, true} <- {:enabled, Config.get([:translator, :enabled])},
483 %Activity{} = activity <- Activity.get_by_id_with_object(id),
484 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
485 translation_module <- Config.get([:translator, :module]),
486 {:ok, detected, translation} <-
487 fetch_or_translate(
488 activity.id,
489 activity.object.data["content"],
490 Map.get(params, :from, nil),
491 language,
492 translation_module
493 ) do
494 json(conn, %{detected_language: detected, text: translation})
495 else
496 {:enabled, false} ->
497 conn
498 |> put_status(:bad_request)
499 |> json(%{"error" => "Translation is not enabled"})
500
501 {:visible, false} ->
502 {:error, :not_found}
503
504 e ->
505 e
506 end
507 end
508
509 defp fetch_or_translate(status_id, text, source_language, target_language, translation_module) do
510 @cachex.fetch!(
511 :translations_cache,
512 "translations:#{status_id}:#{source_language}:#{target_language}",
513 fn _ ->
514 value = translation_module.translate(text, source_language, target_language)
515
516 with {:ok, _, _} <- value do
517 value
518 else
519 _ -> {:ignore, value}
520 end
521 end
522 )
523 end
524
525 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
526 if user.disclose_client do
527 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
528 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
529 else
530 Map.put(params, :generator, nil)
531 end
532 end
533
534 defp put_application(params, _), do: Map.put(params, :generator, nil)
535 end