Merge branch 'feature/1822-files-consistency' 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.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 )
126 end
127
128 @doc """
129 POST /api/v1/statuses
130
131 Creates a scheduled status when `scheduled_at` param is present and it's far enough
132 """
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 = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
142
143 attrs = %{
144 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
145 scheduled_at: scheduled_at
146 }
147
148 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
149 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
150 conn
151 |> put_view(ScheduledActivityView)
152 |> render("show.json", scheduled_activity: scheduled_activity)
153 else
154 {:far_enough, _} ->
155 params = Map.drop(params, [:scheduled_at])
156 create(%Plug.Conn{conn | body_params: params}, %{})
157
158 error ->
159 error
160 end
161 end
162
163 @doc """
164 POST /api/v1/statuses
165
166 Creates a regular status
167 """
168 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
169 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
170
171 with {:ok, activity} <- CommonAPI.post(user, params) do
172 try_render(conn, "show.json",
173 activity: activity,
174 for: user,
175 as: :activity,
176 with_direct_conversation_id: true
177 )
178 else
179 {:error, {:reject, message}} ->
180 conn
181 |> put_status(:unprocessable_entity)
182 |> json(%{error: message})
183
184 {:error, message} ->
185 conn
186 |> put_status(:unprocessable_entity)
187 |> json(%{error: message})
188 end
189 end
190
191 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
192 params = Map.put(params, :status, "")
193 create(%Plug.Conn{conn | body_params: params}, %{})
194 end
195
196 @doc "GET /api/v1/statuses/:id"
197 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
198 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
199 true <- Visibility.visible_for_user?(activity, user) do
200 try_render(conn, "show.json",
201 activity: activity,
202 for: user,
203 with_direct_conversation_id: true
204 )
205 else
206 _ -> {:error, :not_found}
207 end
208 end
209
210 @doc "DELETE /api/v1/statuses/:id"
211 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
212 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
213 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
214 try_render(conn, "show.json",
215 activity: activity,
216 for: user,
217 with_direct_conversation_id: true,
218 with_source: true
219 )
220 else
221 _e -> {:error, :not_found}
222 end
223 end
224
225 @doc "POST /api/v1/statuses/:id/reblog"
226 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
227 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
228 %Activity{} = announce <- Activity.normalize(announce.data) do
229 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
230 end
231 end
232
233 @doc "POST /api/v1/statuses/:id/unreblog"
234 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
235 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
236 %Activity{} = activity <- Activity.get_by_id(activity_id) do
237 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
238 end
239 end
240
241 @doc "POST /api/v1/statuses/:id/favourite"
242 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
243 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
244 %Activity{} = activity <- Activity.get_by_id(activity_id) do
245 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
246 end
247 end
248
249 @doc "POST /api/v1/statuses/:id/unfavourite"
250 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
251 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
252 %Activity{} = activity <- Activity.get_by_id(activity_id) do
253 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
254 end
255 end
256
257 @doc "POST /api/v1/statuses/:id/pin"
258 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
259 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
260 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
261 end
262 end
263
264 @doc "POST /api/v1/statuses/:id/unpin"
265 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
266 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
267 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
268 end
269 end
270
271 @doc "POST /api/v1/statuses/:id/bookmark"
272 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
273 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
274 %User{} = user <- User.get_cached_by_nickname(user.nickname),
275 true <- Visibility.visible_for_user?(activity, user),
276 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
277 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
278 end
279 end
280
281 @doc "POST /api/v1/statuses/:id/unbookmark"
282 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
283 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
284 %User{} = user <- User.get_cached_by_nickname(user.nickname),
285 true <- Visibility.visible_for_user?(activity, user),
286 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
287 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
288 end
289 end
290
291 @doc "POST /api/v1/statuses/:id/mute"
292 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
293 with %Activity{} = activity <- Activity.get_by_id(id),
294 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
295 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
296 end
297 end
298
299 @doc "POST /api/v1/statuses/:id/unmute"
300 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
301 with %Activity{} = activity <- Activity.get_by_id(id),
302 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
303 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
304 end
305 end
306
307 @doc "GET /api/v1/statuses/:id/card"
308 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
309 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
310 with %Activity{} = activity <- Activity.get_by_id(status_id),
311 true <- Visibility.visible_for_user?(activity, user) do
312 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
313 render(conn, "card.json", data)
314 else
315 _ -> render_error(conn, :not_found, "Record not found")
316 end
317 end
318
319 @doc "GET /api/v1/statuses/:id/favourited_by"
320 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
321 with true <- Pleroma.Config.get([:instance, :show_reactions]),
322 %Activity{} = activity <- Activity.get_by_id_with_object(id),
323 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
324 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
325 users =
326 User
327 |> Ecto.Query.where([u], u.ap_id in ^likes)
328 |> Repo.all()
329 |> Enum.filter(&(not User.blocks?(user, &1)))
330
331 conn
332 |> put_view(AccountView)
333 |> render("index.json", for: user, users: users, as: :user)
334 else
335 {:visible, false} -> {:error, :not_found}
336 _ -> json(conn, [])
337 end
338 end
339
340 @doc "GET /api/v1/statuses/:id/reblogged_by"
341 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
342 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
343 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
344 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
345 Object.normalize(activity) do
346 announces =
347 "Announce"
348 |> Activity.Queries.by_type()
349 |> Ecto.Query.where([a], a.actor in ^announces)
350 # this is to use the index
351 |> Activity.Queries.by_object_id(ap_id)
352 |> Repo.all()
353 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
354 |> Enum.map(& &1.actor)
355 |> Enum.uniq()
356
357 users =
358 User
359 |> Ecto.Query.where([u], u.ap_id in ^announces)
360 |> Repo.all()
361 |> Enum.filter(&(not User.blocks?(user, &1)))
362
363 conn
364 |> put_view(AccountView)
365 |> render("index.json", for: user, users: users, as: :user)
366 else
367 {:visible, false} -> {:error, :not_found}
368 _ -> json(conn, [])
369 end
370 end
371
372 @doc "GET /api/v1/statuses/:id/context"
373 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
374 with %Activity{} = activity <- Activity.get_by_id(id) do
375 activities =
376 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
377 blocking_user: user,
378 user: user,
379 exclude_id: activity.id
380 })
381
382 render(conn, "context.json", activity: activity, activities: activities, user: user)
383 end
384 end
385
386 @doc "GET /api/v1/favourites"
387 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
388 activities = ActivityPub.fetch_favourites(user, params)
389
390 conn
391 |> add_link_headers(activities)
392 |> render("index.json",
393 activities: activities,
394 for: user,
395 as: :activity
396 )
397 end
398
399 @doc "GET /api/v1/bookmarks"
400 def bookmarks(%{assigns: %{user: user}} = conn, params) do
401 user = User.get_cached_by_id(user.id)
402
403 bookmarks =
404 user.id
405 |> Bookmark.for_user_query()
406 |> Pleroma.Pagination.fetch_paginated(params)
407
408 activities =
409 bookmarks
410 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
411
412 conn
413 |> add_link_headers(bookmarks)
414 |> render("index.json",
415 activities: activities,
416 for: user,
417 as: :activity
418 )
419 end
420 end