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