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