First
[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.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.OAuth.Token
25   alias Pleroma.Web.Plugs.OAuthScopesPlug
26   alias Pleroma.Web.Plugs.RateLimiter
27
28   plug(Pleroma.Web.ApiSpec.CastAndValidate)
29
30   plug(:skip_public_check when action in [:index, :show])
31
32   @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
33
34   plug(
35     OAuthScopesPlug,
36     %{@unauthenticated_access | scopes: ["read:statuses"]}
37     when action in [
38            :index,
39            :show,
40            :card,
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(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
114     limit = 100
115
116     activities =
117       ids
118       |> Enum.take(limit)
119       |> Activity.all_by_ids_with_object()
120       |> Enum.filter(&Visibility.visible_for_user?(&1, user))
121
122     render(conn, "index.json",
123       activities: activities,
124       for: user,
125       as: :activity,
126       with_muted: Map.get(params, :with_muted, false)
127     )
128   end
129
130   @doc """
131   POST /api/v1/statuses
132   """
133   # Creates a scheduled status when `scheduled_at` param is present and it's far enough
134   def create(
135         %{
136           assigns: %{user: user},
137           body_params: %{status: _, scheduled_at: scheduled_at} = params
138         } = conn,
139         _
140       )
141       when not is_nil(scheduled_at) do
142     params =
143       Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
144       |> put_application(conn)
145
146     attrs = %{
147       params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
148       scheduled_at: scheduled_at
149     }
150
151     with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
152          {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
153       conn
154       |> put_view(ScheduledActivityView)
155       |> render("show.json", scheduled_activity: scheduled_activity)
156     else
157       {:far_enough, _} ->
158         params = Map.drop(params, [:scheduled_at])
159         create(%Plug.Conn{conn | body_params: params}, %{})
160
161       error ->
162         error
163     end
164   end
165
166   # Creates a regular status
167   def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
168     params =
169       Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
170       |> put_application(conn)
171
172     with {:ok, activity} <- CommonAPI.post(user, params) do
173       try_render(conn, "show.json",
174         activity: activity,
175         for: user,
176         as: :activity,
177         with_direct_conversation_id: true
178       )
179     else
180       {:error, {:reject, message}} ->
181         conn
182         |> put_status(:unprocessable_entity)
183         |> json(%{error: message})
184
185       {:error, message} ->
186         conn
187         |> put_status(:unprocessable_entity)
188         |> json(%{error: message})
189     end
190   end
191
192   def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
193     params = Map.put(params, :status, "")
194     create(%Plug.Conn{conn | body_params: params}, %{})
195   end
196
197   @doc "GET /api/v1/statuses/:id/history"
198   def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
199     with user = assigns[:user],
200          %Activity{} = activity <- Activity.get_by_id_with_object(id),
201          true <- Visibility.visible_for_user?(activity, user) do
202       try_render(conn, "history.json",
203         activity: activity,
204         for: user,
205         with_direct_conversation_id: true,
206         with_muted: Map.get(params, :with_muted, false)
207       )
208     else
209       _ -> {:error, :not_found}
210     end
211   end
212
213   @doc "GET /api/v1/statuses/:id/source"
214   def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
215     with user = assigns[:user],
216          %Activity{} = activity <- Activity.get_by_id_with_object(id),
217          true <- Visibility.visible_for_user?(activity, user) do
218       try_render(conn, "source.json",
219         activity: activity,
220         for: user
221       )
222     else
223       _ -> {:error, :not_found}
224     end
225   end
226
227   @doc "PUT /api/v1/statuses/:id"
228   def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
229     with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
230          {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
231          {_, true} <- {:is_create, activity.data["type"] == "Create"},
232          actor <- Activity.user_actor(activity),
233          {_, true} <- {:own_status, actor.id == user.id},
234          changes <- body_params |> put_application(conn),
235          {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
236          {_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
237       try_render(conn, "show.json",
238         activity: activity,
239         for: user,
240         with_direct_conversation_id: true,
241         with_muted: Map.get(params, :with_muted, false)
242       )
243     else
244       {:own_status, _} -> {:error, :forbidden}
245       {:pipeline, _} -> {:error, :internal_server_error}
246       _ -> {:error, :not_found}
247     end
248   end
249
250   @doc "GET /api/v1/statuses/:id"
251   def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
252     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
253          true <- Visibility.visible_for_user?(activity, user) do
254       try_render(conn, "show.json",
255         activity: activity,
256         for: user,
257         with_direct_conversation_id: true,
258         with_muted: Map.get(params, :with_muted, false)
259       )
260     else
261       _ -> {:error, :not_found}
262     end
263   end
264
265   @doc "DELETE /api/v1/statuses/:id"
266   def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
267     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
268          {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
269       try_render(conn, "show.json",
270         activity: activity,
271         for: user,
272         with_direct_conversation_id: true,
273         with_source: true
274       )
275     else
276       _e -> {:error, :not_found}
277     end
278   end
279
280   @doc "POST /api/v1/statuses/:id/reblog"
281   def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
282     with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
283          %Activity{} = announce <- Activity.normalize(announce.data) do
284       try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
285     end
286   end
287
288   @doc "POST /api/v1/statuses/:id/unreblog"
289   def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
290     with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
291          %Activity{} = activity <- Activity.get_by_id(activity_id) do
292       try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
293     end
294   end
295
296   @doc "POST /api/v1/statuses/:id/favourite"
297   def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
298     with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
299          %Activity{} = activity <- Activity.get_by_id(activity_id) do
300       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
301     end
302   end
303
304   @doc "POST /api/v1/statuses/:id/unfavourite"
305   def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
306     with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
307          %Activity{} = activity <- Activity.get_by_id(activity_id) do
308       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
309     end
310   end
311
312   @doc "POST /api/v1/statuses/:id/pin"
313   def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
314     with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
315       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
316     else
317       {:error, :pinned_statuses_limit_reached} ->
318         {:error, "You have already pinned the maximum number of statuses"}
319
320       {:error, :ownership_error} ->
321         {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
322
323       {:error, :visibility_error} ->
324         {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
325
326       error ->
327         error
328     end
329   end
330
331   @doc "POST /api/v1/statuses/:id/unpin"
332   def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
333     with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
334       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
335     end
336   end
337
338   @doc "POST /api/v1/statuses/:id/bookmark"
339   def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
340     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
341          %User{} = user <- User.get_cached_by_nickname(user.nickname),
342          true <- Visibility.visible_for_user?(activity, user),
343          {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
344       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
345     end
346   end
347
348   @doc "POST /api/v1/statuses/:id/unbookmark"
349   def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
350     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
351          %User{} = user <- User.get_cached_by_nickname(user.nickname),
352          true <- Visibility.visible_for_user?(activity, user),
353          {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
354       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
355     end
356   end
357
358   @doc "POST /api/v1/statuses/:id/mute"
359   def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
360     with %Activity{} = activity <- Activity.get_by_id(id),
361          {:ok, activity} <- CommonAPI.add_mute(user, activity, params) 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/unmute"
367   def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
368     with %Activity{} = activity <- Activity.get_by_id(id),
369          {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
370       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
371     end
372   end
373
374   @doc "GET /api/v1/statuses/:id/card"
375   @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
376   def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
377     with %Activity{} = activity <- Activity.get_by_id(status_id),
378          true <- Visibility.visible_for_user?(activity, user) do
379       data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
380       render(conn, "card.json", data)
381     else
382       _ -> render_error(conn, :not_found, "Record not found")
383     end
384   end
385
386   @doc "GET /api/v1/statuses/:id/favourited_by"
387   def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
388     with true <- Pleroma.Config.get([:instance, :show_reactions]),
389          %Activity{} = activity <- Activity.get_by_id_with_object(id),
390          {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
391          %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
392       users =
393         User
394         |> Ecto.Query.where([u], u.ap_id in ^likes)
395         |> Repo.all()
396         |> Enum.filter(&(not User.blocks?(user, &1)))
397
398       conn
399       |> put_view(AccountView)
400       |> render("index.json", for: user, users: users, as: :user)
401     else
402       {:visible, false} -> {:error, :not_found}
403       _ -> json(conn, [])
404     end
405   end
406
407   @doc "GET /api/v1/statuses/:id/reblogged_by"
408   def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
409     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
410          {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
411          %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
412            Object.normalize(activity, fetch: false) do
413       announces =
414         "Announce"
415         |> Activity.Queries.by_type()
416         |> Ecto.Query.where([a], a.actor in ^announces)
417         # this is to use the index
418         |> Activity.Queries.by_object_id(ap_id)
419         |> Repo.all()
420         |> Enum.filter(&Visibility.visible_for_user?(&1, user))
421         |> Enum.map(& &1.actor)
422         |> Enum.uniq()
423
424       users =
425         User
426         |> Ecto.Query.where([u], u.ap_id in ^announces)
427         |> Repo.all()
428         |> Enum.filter(&(not User.blocks?(user, &1)))
429
430       conn
431       |> put_view(AccountView)
432       |> render("index.json", for: user, users: users, as: :user)
433     else
434       {:visible, false} -> {:error, :not_found}
435       _ -> json(conn, [])
436     end
437   end
438
439   @doc "GET /api/v1/statuses/:id/context"
440   def context(%{assigns: %{user: user}} = conn, %{id: id}) do
441     with %Activity{} = activity <- Activity.get_by_id(id) do
442       activities =
443         ActivityPub.fetch_activities_for_context(activity.data["context"], %{
444           blocking_user: user,
445           user: user,
446           exclude_id: activity.id
447         })
448
449       render(conn, "context.json", activity: activity, activities: activities, user: user)
450     end
451   end
452
453   @doc "GET /api/v1/favourites"
454   def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
455     activities = ActivityPub.fetch_favourites(user, params)
456
457     conn
458     |> add_link_headers(activities)
459     |> render("index.json",
460       activities: activities,
461       for: user,
462       as: :activity
463     )
464   end
465
466   @doc "GET /api/v1/bookmarks"
467   def bookmarks(%{assigns: %{user: user}} = conn, params) do
468     user = User.get_cached_by_id(user.id)
469
470     bookmarks =
471       user.id
472       |> Bookmark.for_user_query()
473       |> Pleroma.Pagination.fetch_paginated(params)
474
475     activities =
476       bookmarks
477       |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
478
479     conn
480     |> add_link_headers(bookmarks)
481     |> render("index.json",
482       activities: activities,
483       for: user,
484       as: :activity
485     )
486   end
487
488   defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
489     if user.disclose_client do
490       %{client_name: client_name, website: website} = Repo.preload(token, :app).app
491       Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
492     else
493       Map.put(params, :generator, nil)
494     end
495   end
496
497   defp put_application(params, _), do: Map.put(params, :generator, nil)
498 end