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