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