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