Merge branch 'hide-reactions' into 'develop'
[akkoma] / lib / pleroma / web / mastodon_api / controllers / status_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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.Plugs.OAuthScopesPlug
17 alias Pleroma.Plugs.RateLimiter
18 alias Pleroma.Repo
19 alias Pleroma.ScheduledActivity
20 alias Pleroma.User
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.ActivityPub.Visibility
23 alias Pleroma.Web.CommonAPI
24 alias Pleroma.Web.MastodonAPI.AccountView
25 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
26
27 plug(Pleroma.Web.ApiSpec.CastAndValidate)
28 plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
29
30 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
31
32 plug(
33 OAuthScopesPlug,
34 %{@unauthenticated_access | scopes: ["read:statuses"]}
35 when action in [
36 :index,
37 :show,
38 :card,
39 :context
40 ]
41 )
42
43 plug(
44 OAuthScopesPlug,
45 %{scopes: ["write:statuses"]}
46 when action in [
47 :create,
48 :delete,
49 :reblog,
50 :unreblog
51 ]
52 )
53
54 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
55
56 plug(
57 OAuthScopesPlug,
58 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
59 )
60
61 plug(
62 OAuthScopesPlug,
63 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
64 )
65
66 plug(
67 OAuthScopesPlug,
68 %{@unauthenticated_access | scopes: ["read:accounts"]}
69 when action in [:favourited_by, :reblogged_by]
70 )
71
72 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
73
74 # Note: scope not present in Mastodon: read:bookmarks
75 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
76
77 # Note: scope not present in Mastodon: write:bookmarks
78 plug(
79 OAuthScopesPlug,
80 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
81 )
82
83 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
84
85 plug(
86 RateLimiter,
87 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
88 when action in ~w(reblog unreblog)a
89 )
90
91 plug(
92 RateLimiter,
93 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
94 when action in ~w(favourite unfavourite)a
95 )
96
97 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
98
99 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
100
101 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
102
103 @doc """
104 GET `/api/v1/statuses?ids[]=1&ids[]=2`
105
106 `ids` query param is required
107 """
108 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
109 limit = 100
110
111 activities =
112 ids
113 |> Enum.take(limit)
114 |> Activity.all_by_ids_with_object()
115 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
116
117 render(conn, "index.json",
118 activities: activities,
119 for: user,
120 as: :activity
121 )
122 end
123
124 @doc """
125 POST /api/v1/statuses
126
127 Creates a scheduled status when `scheduled_at` param is present and it's far enough
128 """
129 def create(
130 %{
131 assigns: %{user: user},
132 body_params: %{status: _, scheduled_at: scheduled_at} = params
133 } = conn,
134 _
135 )
136 when not is_nil(scheduled_at) do
137 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
138
139 attrs = %{
140 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
141 scheduled_at: scheduled_at
142 }
143
144 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
145 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
146 conn
147 |> put_view(ScheduledActivityView)
148 |> render("show.json", scheduled_activity: scheduled_activity)
149 else
150 {:far_enough, _} ->
151 params = Map.drop(params, [:scheduled_at])
152 create(%Plug.Conn{conn | body_params: params}, %{})
153
154 error ->
155 error
156 end
157 end
158
159 @doc """
160 POST /api/v1/statuses
161
162 Creates a regular status
163 """
164 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
165 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
166
167 with {:ok, activity} <- CommonAPI.post(user, params) do
168 try_render(conn, "show.json",
169 activity: activity,
170 for: user,
171 as: :activity,
172 with_direct_conversation_id: true
173 )
174 else
175 {:error, {:reject, message}} ->
176 conn
177 |> put_status(:unprocessable_entity)
178 |> json(%{error: message})
179
180 {:error, message} ->
181 conn
182 |> put_status(:unprocessable_entity)
183 |> json(%{error: message})
184 end
185 end
186
187 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
188 params = Map.put(params, :status, "")
189 create(%Plug.Conn{conn | body_params: params}, %{})
190 end
191
192 @doc "GET /api/v1/statuses/:id"
193 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
194 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
195 true <- Visibility.visible_for_user?(activity, user) do
196 try_render(conn, "show.json",
197 activity: activity,
198 for: user,
199 with_direct_conversation_id: true
200 )
201 else
202 _ -> {:error, :not_found}
203 end
204 end
205
206 @doc "DELETE /api/v1/statuses/:id"
207 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
208 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
209 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
210 try_render(conn, "show.json",
211 activity: activity,
212 for: user,
213 with_direct_conversation_id: true,
214 with_source: true
215 )
216 else
217 _e -> {:error, :not_found}
218 end
219 end
220
221 @doc "POST /api/v1/statuses/:id/reblog"
222 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
223 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
224 %Activity{} = announce <- Activity.normalize(announce.data) do
225 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
226 end
227 end
228
229 @doc "POST /api/v1/statuses/:id/unreblog"
230 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
231 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
232 %Activity{} = activity <- Activity.get_by_id(activity_id) do
233 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
234 end
235 end
236
237 @doc "POST /api/v1/statuses/:id/favourite"
238 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
239 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
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/unfavourite"
246 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
247 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
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/pin"
254 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
255 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
256 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
257 end
258 end
259
260 @doc "POST /api/v1/statuses/:id/unpin"
261 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
262 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
263 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
264 end
265 end
266
267 @doc "POST /api/v1/statuses/:id/bookmark"
268 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
269 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
270 %User{} = user <- User.get_cached_by_nickname(user.nickname),
271 true <- Visibility.visible_for_user?(activity, user),
272 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
273 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
274 end
275 end
276
277 @doc "POST /api/v1/statuses/:id/unbookmark"
278 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
279 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
280 %User{} = user <- User.get_cached_by_nickname(user.nickname),
281 true <- Visibility.visible_for_user?(activity, user),
282 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
283 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
284 end
285 end
286
287 @doc "POST /api/v1/statuses/:id/mute"
288 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
289 with %Activity{} = activity <- Activity.get_by_id(id),
290 {:ok, activity} <- CommonAPI.add_mute(user, activity) 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/unmute"
296 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
297 with %Activity{} = activity <- Activity.get_by_id(id),
298 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
299 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
300 end
301 end
302
303 @doc "GET /api/v1/statuses/:id/card"
304 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
305 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
306 with %Activity{} = activity <- Activity.get_by_id(status_id),
307 true <- Visibility.visible_for_user?(activity, user) do
308 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
309 render(conn, "card.json", data)
310 else
311 _ -> render_error(conn, :not_found, "Record not found")
312 end
313 end
314
315 @doc "GET /api/v1/statuses/:id/favourited_by"
316 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
317 with true <- Pleroma.Config.get([:instance, :show_reactions]),
318 %Activity{} = activity <- Activity.get_by_id_with_object(id),
319 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
320 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
321 users =
322 User
323 |> Ecto.Query.where([u], u.ap_id in ^likes)
324 |> Repo.all()
325 |> Enum.filter(&(not User.blocks?(user, &1)))
326
327 conn
328 |> put_view(AccountView)
329 |> render("index.json", for: user, users: users, as: :user)
330 else
331 {:visible, false} -> {:error, :not_found}
332 _ -> json(conn, [])
333 end
334 end
335
336 @doc "GET /api/v1/statuses/:id/reblogged_by"
337 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
338 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
339 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
340 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
341 Object.normalize(activity) do
342 announces =
343 "Announce"
344 |> Activity.Queries.by_type()
345 |> Ecto.Query.where([a], a.actor in ^announces)
346 # this is to use the index
347 |> Activity.Queries.by_object_id(ap_id)
348 |> Repo.all()
349 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
350 |> Enum.map(& &1.actor)
351 |> Enum.uniq()
352
353 users =
354 User
355 |> Ecto.Query.where([u], u.ap_id in ^announces)
356 |> Repo.all()
357 |> Enum.filter(&(not User.blocks?(user, &1)))
358
359 conn
360 |> put_view(AccountView)
361 |> render("index.json", for: user, users: users, as: :user)
362 else
363 {:visible, false} -> {:error, :not_found}
364 _ -> json(conn, [])
365 end
366 end
367
368 @doc "GET /api/v1/statuses/:id/context"
369 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
370 with %Activity{} = activity <- Activity.get_by_id(id) do
371 activities =
372 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
373 blocking_user: user,
374 user: user,
375 exclude_id: activity.id
376 })
377
378 render(conn, "context.json", activity: activity, activities: activities, user: user)
379 end
380 end
381
382 @doc "GET /api/v1/favourites"
383 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
384 activities = ActivityPub.fetch_favourites(user, params)
385
386 conn
387 |> add_link_headers(activities)
388 |> render("index.json",
389 activities: activities,
390 for: user,
391 as: :activity
392 )
393 end
394
395 @doc "GET /api/v1/bookmarks"
396 def bookmarks(%{assigns: %{user: user}} = conn, params) do
397 user = User.get_cached_by_id(user.id)
398
399 bookmarks =
400 user.id
401 |> Bookmark.for_user_query()
402 |> Pleroma.Pagination.fetch_paginated(params)
403
404 activities =
405 bookmarks
406 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
407
408 conn
409 |> add_link_headers(bookmarks)
410 |> render("index.json",
411 activities: activities,
412 for: user,
413 as: :activity
414 )
415 end
416 end