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