Merge branch 'develop' into refactor/notification_settings
[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 {: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 %Activity{} = activity <- Activity.get_by_id_with_object(id),
313 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
314 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
315 users =
316 User
317 |> Ecto.Query.where([u], u.ap_id in ^likes)
318 |> Repo.all()
319 |> Enum.filter(&(not User.blocks?(user, &1)))
320
321 conn
322 |> put_view(AccountView)
323 |> render("index.json", for: user, users: users, as: :user)
324 else
325 {:visible, false} -> {:error, :not_found}
326 _ -> json(conn, [])
327 end
328 end
329
330 @doc "GET /api/v1/statuses/:id/reblogged_by"
331 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
332 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
333 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
334 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
335 Object.normalize(activity) do
336 announces =
337 "Announce"
338 |> Activity.Queries.by_type()
339 |> Ecto.Query.where([a], a.actor in ^announces)
340 # this is to use the index
341 |> Activity.Queries.by_object_id(ap_id)
342 |> Repo.all()
343 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
344 |> Enum.map(& &1.actor)
345 |> Enum.uniq()
346
347 users =
348 User
349 |> Ecto.Query.where([u], u.ap_id in ^announces)
350 |> Repo.all()
351 |> Enum.filter(&(not User.blocks?(user, &1)))
352
353 conn
354 |> put_view(AccountView)
355 |> render("index.json", for: user, users: users, as: :user)
356 else
357 {:visible, false} -> {:error, :not_found}
358 _ -> json(conn, [])
359 end
360 end
361
362 @doc "GET /api/v1/statuses/:id/context"
363 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
364 with %Activity{} = activity <- Activity.get_by_id(id) do
365 activities =
366 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
367 blocking_user: user,
368 user: user,
369 exclude_id: activity.id
370 })
371
372 render(conn, "context.json", activity: activity, activities: activities, user: user)
373 end
374 end
375
376 @doc "GET /api/v1/favourites"
377 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
378 activities = ActivityPub.fetch_favourites(user, params)
379
380 conn
381 |> add_link_headers(activities)
382 |> render("index.json",
383 activities: activities,
384 for: user,
385 as: :activity
386 )
387 end
388
389 @doc "GET /api/v1/bookmarks"
390 def bookmarks(%{assigns: %{user: user}} = conn, params) do
391 user = User.get_cached_by_id(user.id)
392
393 bookmarks =
394 user.id
395 |> Bookmark.for_user_query()
396 |> Pleroma.Pagination.fetch_paginated(params)
397
398 activities =
399 bookmarks
400 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
401
402 conn
403 |> add_link_headers(bookmarks)
404 |> render("index.json",
405 activities: activities,
406 for: user,
407 as: :activity
408 )
409 end
410 end