Prefer naming this put_application because we're putting it into the params map
[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(
167 %{assigns: %{user: user}, body_params: %{status: _} = params} = conn,
168 _
169 ) 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"
200 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
201 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
202 true <- Visibility.visible_for_user?(activity, user) do
203 try_render(conn, "show.json",
204 activity: activity,
205 for: user,
206 with_direct_conversation_id: true,
207 with_muted: Map.get(params, :with_muted, false)
208 )
209 else
210 _ -> {:error, :not_found}
211 end
212 end
213
214 @doc "DELETE /api/v1/statuses/:id"
215 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
216 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
217 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
218 try_render(conn, "show.json",
219 activity: activity,
220 for: user,
221 with_direct_conversation_id: true,
222 with_source: true
223 )
224 else
225 _e -> {:error, :not_found}
226 end
227 end
228
229 @doc "POST /api/v1/statuses/:id/reblog"
230 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
231 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
232 %Activity{} = announce <- Activity.normalize(announce.data) do
233 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
234 end
235 end
236
237 @doc "POST /api/v1/statuses/:id/unreblog"
238 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
239 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
240 %Activity{} = activity <- Activity.get_by_id(activity_id) do
241 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
242 end
243 end
244
245 @doc "POST /api/v1/statuses/:id/favourite"
246 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
247 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
248 %Activity{} = activity <- Activity.get_by_id(activity_id) do
249 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
250 end
251 end
252
253 @doc "POST /api/v1/statuses/:id/unfavourite"
254 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
255 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
256 %Activity{} = activity <- Activity.get_by_id(activity_id) do
257 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
258 end
259 end
260
261 @doc "POST /api/v1/statuses/:id/pin"
262 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
263 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
264 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
265 end
266 end
267
268 @doc "POST /api/v1/statuses/:id/unpin"
269 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
270 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
271 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
272 end
273 end
274
275 @doc "POST /api/v1/statuses/:id/bookmark"
276 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
277 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
278 %User{} = user <- User.get_cached_by_nickname(user.nickname),
279 true <- Visibility.visible_for_user?(activity, user),
280 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
281 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
282 end
283 end
284
285 @doc "POST /api/v1/statuses/:id/unbookmark"
286 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
287 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
288 %User{} = user <- User.get_cached_by_nickname(user.nickname),
289 true <- Visibility.visible_for_user?(activity, user),
290 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
291 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
292 end
293 end
294
295 @doc "POST /api/v1/statuses/:id/mute"
296 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
297 with %Activity{} = activity <- Activity.get_by_id(id),
298 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
299 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
300 end
301 end
302
303 @doc "POST /api/v1/statuses/:id/unmute"
304 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
305 with %Activity{} = activity <- Activity.get_by_id(id),
306 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
307 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
308 end
309 end
310
311 @doc "GET /api/v1/statuses/:id/card"
312 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
313 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
314 with %Activity{} = activity <- Activity.get_by_id(status_id),
315 true <- Visibility.visible_for_user?(activity, user) do
316 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
317 render(conn, "card.json", data)
318 else
319 _ -> render_error(conn, :not_found, "Record not found")
320 end
321 end
322
323 @doc "GET /api/v1/statuses/:id/favourited_by"
324 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
325 with true <- Pleroma.Config.get([:instance, :show_reactions]),
326 %Activity{} = activity <- Activity.get_by_id_with_object(id),
327 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
328 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
329 users =
330 User
331 |> Ecto.Query.where([u], u.ap_id in ^likes)
332 |> Repo.all()
333 |> Enum.filter(&(not User.blocks?(user, &1)))
334
335 conn
336 |> put_view(AccountView)
337 |> render("index.json", for: user, users: users, as: :user)
338 else
339 {:visible, false} -> {:error, :not_found}
340 _ -> json(conn, [])
341 end
342 end
343
344 @doc "GET /api/v1/statuses/:id/reblogged_by"
345 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
346 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
347 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
348 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
349 Object.normalize(activity, fetch: false) do
350 announces =
351 "Announce"
352 |> Activity.Queries.by_type()
353 |> Ecto.Query.where([a], a.actor in ^announces)
354 # this is to use the index
355 |> Activity.Queries.by_object_id(ap_id)
356 |> Repo.all()
357 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
358 |> Enum.map(& &1.actor)
359 |> Enum.uniq()
360
361 users =
362 User
363 |> Ecto.Query.where([u], u.ap_id in ^announces)
364 |> Repo.all()
365 |> Enum.filter(&(not User.blocks?(user, &1)))
366
367 conn
368 |> put_view(AccountView)
369 |> render("index.json", for: user, users: users, as: :user)
370 else
371 {:visible, false} -> {:error, :not_found}
372 _ -> json(conn, [])
373 end
374 end
375
376 @doc "GET /api/v1/statuses/:id/context"
377 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
378 with %Activity{} = activity <- Activity.get_by_id(id) do
379 activities =
380 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
381 blocking_user: user,
382 user: user,
383 exclude_id: activity.id
384 })
385
386 render(conn, "context.json", activity: activity, activities: activities, user: user)
387 end
388 end
389
390 @doc "GET /api/v1/favourites"
391 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
392 activities = ActivityPub.fetch_favourites(user, params)
393
394 conn
395 |> add_link_headers(activities)
396 |> render("index.json",
397 activities: activities,
398 for: user,
399 as: :activity
400 )
401 end
402
403 @doc "GET /api/v1/bookmarks"
404 def bookmarks(%{assigns: %{user: user}} = conn, params) do
405 user = User.get_cached_by_id(user.id)
406
407 bookmarks =
408 user.id
409 |> Bookmark.for_user_query()
410 |> Pleroma.Pagination.fetch_paginated(params)
411
412 activities =
413 bookmarks
414 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
415
416 conn
417 |> add_link_headers(bookmarks)
418 |> render("index.json",
419 activities: activities,
420 for: user,
421 as: :activity
422 )
423 end
424
425 defp put_application(params, %{assigns: %{token: %{app_id: app_id}}} = _conn) do
426 params |> Map.put(:application, Pleroma.Web.OAuth.App.get_app_by_id(app_id))
427 end
428
429 defp put_application(params, _), do: Map.put(params, :application, %{name: "Web", website: nil})
430 end