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