MastoAPI: Show source field when deleting
[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 """
129 def create(
130 %{
131 assigns: %{user: user},
132 body_params: %{status: _, scheduled_at: scheduled_at} = params
133 } = conn,
134 _
135 )
136 when not is_nil(scheduled_at) do
137 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
138
139 attrs = %{
140 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
141 scheduled_at: scheduled_at
142 }
143
144 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
145 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
146 conn
147 |> put_view(ScheduledActivityView)
148 |> render("show.json", scheduled_activity: scheduled_activity)
149 else
150 {:far_enough, _} ->
151 params = Map.drop(params, [:scheduled_at])
152 create(%Plug.Conn{conn | body_params: params}, %{})
153
154 error ->
155 error
156 end
157 end
158
159 @doc """
160 POST /api/v1/statuses
161
162 Creates a regular status
163 """
164 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
165 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
166
167 with {:ok, activity} <- CommonAPI.post(user, params) do
168 try_render(conn, "show.json",
169 activity: activity,
170 for: user,
171 as: :activity,
172 with_direct_conversation_id: true
173 )
174 else
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 render <-
205 try_render(conn, "show.json",
206 activity: activity,
207 for: user,
208 with_direct_conversation_id: true,
209 with_source: true
210 ),
211 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
212 render
213 else
214 _e -> {:error, :not_found}
215 end
216 end
217
218 @doc "POST /api/v1/statuses/:id/reblog"
219 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
220 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
221 %Activity{} = announce <- Activity.normalize(announce.data) do
222 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
223 end
224 end
225
226 @doc "POST /api/v1/statuses/:id/unreblog"
227 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
228 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
229 %Activity{} = activity <- Activity.get_by_id(activity_id) do
230 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
231 end
232 end
233
234 @doc "POST /api/v1/statuses/:id/favourite"
235 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
236 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
237 %Activity{} = activity <- Activity.get_by_id(activity_id) do
238 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
239 end
240 end
241
242 @doc "POST /api/v1/statuses/:id/unfavourite"
243 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
244 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
245 %Activity{} = activity <- Activity.get_by_id(activity_id) do
246 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
247 end
248 end
249
250 @doc "POST /api/v1/statuses/:id/pin"
251 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
252 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) 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/unpin"
258 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
259 with {:ok, activity} <- CommonAPI.unpin(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/bookmark"
265 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
266 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
267 %User{} = user <- User.get_cached_by_nickname(user.nickname),
268 true <- Visibility.visible_for_user?(activity, user),
269 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
270 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
271 end
272 end
273
274 @doc "POST /api/v1/statuses/:id/unbookmark"
275 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
276 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
277 %User{} = user <- User.get_cached_by_nickname(user.nickname),
278 true <- Visibility.visible_for_user?(activity, user),
279 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
280 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
281 end
282 end
283
284 @doc "POST /api/v1/statuses/:id/mute"
285 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
286 with %Activity{} = activity <- Activity.get_by_id(id),
287 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
288 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
289 end
290 end
291
292 @doc "POST /api/v1/statuses/:id/unmute"
293 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
294 with %Activity{} = activity <- Activity.get_by_id(id),
295 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
296 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
297 end
298 end
299
300 @doc "GET /api/v1/statuses/:id/card"
301 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
302 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
303 with %Activity{} = activity <- Activity.get_by_id(status_id),
304 true <- Visibility.visible_for_user?(activity, user) do
305 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
306 render(conn, "card.json", data)
307 else
308 _ -> render_error(conn, :not_found, "Record not found")
309 end
310 end
311
312 @doc "GET /api/v1/statuses/:id/favourited_by"
313 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
314 with %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