Merge remote-tracking branch 'pleroma/develop' into cycles-email
[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 else
264 {:error, :pinned_statuses_limit_reached} ->
265 {:error, "You have already pinned the maximum number of statuses"}
266
267 {:error, :ownership_error} ->
268 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
269
270 {:error, :visibility_error} ->
271 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
272
273 error ->
274 error
275 end
276 end
277
278 @doc "POST /api/v1/statuses/:id/unpin"
279 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
280 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) 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/bookmark"
286 def bookmark(%{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.create(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/unbookmark"
296 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
297 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
298 %User{} = user <- User.get_cached_by_nickname(user.nickname),
299 true <- Visibility.visible_for_user?(activity, user),
300 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
301 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
302 end
303 end
304
305 @doc "POST /api/v1/statuses/:id/mute"
306 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
307 with %Activity{} = activity <- Activity.get_by_id(id),
308 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
309 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
310 end
311 end
312
313 @doc "POST /api/v1/statuses/:id/unmute"
314 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
315 with %Activity{} = activity <- Activity.get_by_id(id),
316 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
317 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
318 end
319 end
320
321 @doc "GET /api/v1/statuses/:id/card"
322 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
323 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
324 with %Activity{} = activity <- Activity.get_by_id(status_id),
325 true <- Visibility.visible_for_user?(activity, user) do
326 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
327 render(conn, "card.json", data)
328 else
329 _ -> render_error(conn, :not_found, "Record not found")
330 end
331 end
332
333 @doc "GET /api/v1/statuses/:id/favourited_by"
334 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
335 with true <- Pleroma.Config.get([:instance, :show_reactions]),
336 %Activity{} = activity <- Activity.get_by_id_with_object(id),
337 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
338 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
339 users =
340 User
341 |> Ecto.Query.where([u], u.ap_id in ^likes)
342 |> Repo.all()
343 |> Enum.filter(&(not User.blocks?(user, &1)))
344
345 conn
346 |> put_view(AccountView)
347 |> render("index.json", for: user, users: users, as: :user)
348 else
349 {:visible, false} -> {:error, :not_found}
350 _ -> json(conn, [])
351 end
352 end
353
354 @doc "GET /api/v1/statuses/:id/reblogged_by"
355 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
356 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
357 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
358 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
359 Object.normalize(activity, fetch: false) do
360 announces =
361 "Announce"
362 |> Activity.Queries.by_type()
363 |> Ecto.Query.where([a], a.actor in ^announces)
364 # this is to use the index
365 |> Activity.Queries.by_object_id(ap_id)
366 |> Repo.all()
367 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
368 |> Enum.map(& &1.actor)
369 |> Enum.uniq()
370
371 users =
372 User
373 |> Ecto.Query.where([u], u.ap_id in ^announces)
374 |> Repo.all()
375 |> Enum.filter(&(not User.blocks?(user, &1)))
376
377 conn
378 |> put_view(AccountView)
379 |> render("index.json", for: user, users: users, as: :user)
380 else
381 {:visible, false} -> {:error, :not_found}
382 _ -> json(conn, [])
383 end
384 end
385
386 @doc "GET /api/v1/statuses/:id/context"
387 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
388 with %Activity{} = activity <- Activity.get_by_id(id) do
389 activities =
390 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
391 blocking_user: user,
392 user: user,
393 exclude_id: activity.id
394 })
395
396 render(conn, "context.json", activity: activity, activities: activities, user: user)
397 end
398 end
399
400 @doc "GET /api/v1/favourites"
401 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
402 activities = ActivityPub.fetch_favourites(user, params)
403
404 conn
405 |> add_link_headers(activities)
406 |> render("index.json",
407 activities: activities,
408 for: user,
409 as: :activity
410 )
411 end
412
413 @doc "GET /api/v1/bookmarks"
414 def bookmarks(%{assigns: %{user: user}} = conn, params) do
415 user = User.get_cached_by_id(user.id)
416
417 bookmarks =
418 user.id
419 |> Bookmark.for_user_query()
420 |> Pleroma.Pagination.fetch_paginated(params)
421
422 activities =
423 bookmarks
424 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
425
426 conn
427 |> add_link_headers(bookmarks)
428 |> render("index.json",
429 activities: activities,
430 for: user,
431 as: :activity
432 )
433 end
434
435 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
436 if user.disclose_client do
437 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
438 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
439 else
440 Map.put(params, :generator, nil)
441 end
442 end
443
444 defp put_application(params, _), do: Map.put(params, :generator, nil)
445 end