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