Merge branch 'stable' into develop
[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.OAuth.Token
25 alias Pleroma.Web.Plugs.OAuthScopesPlug
26 alias Pleroma.Web.Plugs.RateLimiter
27
28 plug(Pleroma.Web.ApiSpec.CastAndValidate)
29
30 plug(:skip_public_check when action in [:index, :show])
31
32 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
33
34 plug(
35 OAuthScopesPlug,
36 %{@unauthenticated_access | scopes: ["read:statuses"]}
37 when action in [
38 :index,
39 :show,
40 :context
41 ]
42 )
43
44 plug(
45 OAuthScopesPlug,
46 %{scopes: ["write:statuses"]}
47 when action in [
48 :create,
49 :delete,
50 :reblog,
51 :unreblog
52 ]
53 )
54
55 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
56
57 plug(
58 OAuthScopesPlug,
59 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
60 )
61
62 plug(
63 OAuthScopesPlug,
64 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
65 )
66
67 plug(
68 OAuthScopesPlug,
69 %{@unauthenticated_access | scopes: ["read:accounts"]}
70 when action in [:favourited_by, :reblogged_by]
71 )
72
73 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
74
75 # Note: scope not present in Mastodon: read:bookmarks
76 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
77
78 # Note: scope not present in Mastodon: write:bookmarks
79 plug(
80 OAuthScopesPlug,
81 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
82 )
83
84 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
85
86 plug(
87 RateLimiter,
88 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
89 when action in ~w(reblog unreblog)a
90 )
91
92 plug(
93 RateLimiter,
94 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
95 when action in ~w(favourite unfavourite)a
96 )
97
98 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
99
100 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
101
102 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
103
104 @doc """
105 GET `/api/v1/statuses?ids[]=1&ids[]=2`
106
107 `ids` query param is required
108 """
109 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
110 limit = 100
111
112 activities =
113 ids
114 |> Enum.take(limit)
115 |> Activity.all_by_ids_with_object()
116 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
117
118 render(conn, "index.json",
119 activities: activities,
120 for: user,
121 as: :activity,
122 with_muted: Map.get(params, :with_muted, false)
123 )
124 end
125
126 @doc """
127 POST /api/v1/statuses
128 """
129 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
130 def create(
131 %{
132 assigns: %{user: user},
133 body_params: %{status: _, scheduled_at: scheduled_at} = params
134 } = conn,
135 _
136 )
137 when not is_nil(scheduled_at) do
138 params =
139 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
140 |> put_application(conn)
141
142 attrs = %{
143 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
144 scheduled_at: scheduled_at
145 }
146
147 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
148 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
149 conn
150 |> put_view(ScheduledActivityView)
151 |> render("show.json", scheduled_activity: scheduled_activity)
152 else
153 {:far_enough, _} ->
154 params = Map.drop(params, [:scheduled_at])
155 create(%Plug.Conn{conn | body_params: params}, %{})
156
157 error ->
158 error
159 end
160 end
161
162 # Creates a regular status
163 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
164 params =
165 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
166 |> put_application(conn)
167
168 with {:ok, activity} <- CommonAPI.post(user, params) do
169 try_render(conn, "show.json",
170 activity: activity,
171 for: user,
172 as: :activity,
173 with_direct_conversation_id: true
174 )
175 else
176 {:error, {:reject, message}} ->
177 conn
178 |> put_status(:unprocessable_entity)
179 |> json(%{error: message})
180
181 {:error, message} ->
182 conn
183 |> put_status(:unprocessable_entity)
184 |> json(%{error: message})
185 end
186 end
187
188 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
189 params = Map.put(params, :status, "")
190 create(%Plug.Conn{conn | body_params: params}, %{})
191 end
192
193 @doc "GET /api/v1/statuses/:id"
194 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
195 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
196 true <- Visibility.visible_for_user?(activity, user) do
197 try_render(conn, "show.json",
198 activity: activity,
199 for: user,
200 with_direct_conversation_id: true,
201 with_muted: Map.get(params, :with_muted, false)
202 )
203 else
204 _ -> {:error, :not_found}
205 end
206 end
207
208 @doc "DELETE /api/v1/statuses/:id"
209 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
210 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
211 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
212 try_render(conn, "show.json",
213 activity: activity,
214 for: user,
215 with_direct_conversation_id: true,
216 with_source: true
217 )
218 else
219 _e -> {:error, :not_found}
220 end
221 end
222
223 @doc "POST /api/v1/statuses/:id/reblog"
224 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
225 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
226 %Activity{} = announce <- Activity.normalize(announce.data) do
227 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
228 end
229 end
230
231 @doc "POST /api/v1/statuses/:id/unreblog"
232 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
233 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
234 %Activity{} = activity <- Activity.get_by_id(activity_id) do
235 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
236 end
237 end
238
239 @doc "POST /api/v1/statuses/:id/favourite"
240 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
241 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
242 %Activity{} = activity <- Activity.get_by_id(activity_id) do
243 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
244 end
245 end
246
247 @doc "POST /api/v1/statuses/:id/unfavourite"
248 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
249 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
250 %Activity{} = activity <- Activity.get_by_id(activity_id) 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/pin"
256 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
257 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
258 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
259 else
260 {:error, :pinned_statuses_limit_reached} ->
261 {:error, "You have already pinned the maximum number of statuses"}
262
263 {:error, :ownership_error} ->
264 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
265
266 {:error, :visibility_error} ->
267 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
268
269 error ->
270 error
271 end
272 end
273
274 @doc "POST /api/v1/statuses/:id/unpin"
275 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
276 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
277 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
278 end
279 end
280
281 @doc "POST /api/v1/statuses/:id/bookmark"
282 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
283 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
284 %User{} = user <- User.get_cached_by_nickname(user.nickname),
285 true <- Visibility.visible_for_user?(activity, user),
286 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
287 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
288 end
289 end
290
291 @doc "POST /api/v1/statuses/:id/unbookmark"
292 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
293 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
294 %User{} = user <- User.get_cached_by_nickname(user.nickname),
295 true <- Visibility.visible_for_user?(activity, user),
296 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
297 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
298 end
299 end
300
301 @doc "POST /api/v1/statuses/:id/mute"
302 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
303 with %Activity{} = activity <- Activity.get_by_id(id),
304 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
305 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
306 end
307 end
308
309 @doc "POST /api/v1/statuses/:id/unmute"
310 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
311 with %Activity{} = activity <- Activity.get_by_id(id),
312 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
313 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
314 end
315 end
316
317 @doc "GET /api/v1/statuses/:id/favourited_by"
318 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
319 with true <- Pleroma.Config.get([:instance, :show_reactions]),
320 %Activity{} = activity <- Activity.get_by_id_with_object(id),
321 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
322 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
323 users =
324 User
325 |> Ecto.Query.where([u], u.ap_id in ^likes)
326 |> Repo.all()
327 |> Enum.filter(&(not User.blocks?(user, &1)))
328
329 conn
330 |> put_view(AccountView)
331 |> render("index.json", for: user, users: users, as: :user)
332 else
333 {:visible, false} -> {:error, :not_found}
334 _ -> json(conn, [])
335 end
336 end
337
338 @doc "GET /api/v1/statuses/:id/reblogged_by"
339 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
340 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
341 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
342 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
343 Object.normalize(activity, fetch: false) do
344 announces =
345 "Announce"
346 |> Activity.Queries.by_type()
347 |> Ecto.Query.where([a], a.actor in ^announces)
348 # this is to use the index
349 |> Activity.Queries.by_object_id(ap_id)
350 |> Repo.all()
351 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
352 |> Enum.map(& &1.actor)
353 |> Enum.uniq()
354
355 users =
356 User
357 |> Ecto.Query.where([u], u.ap_id in ^announces)
358 |> Repo.all()
359 |> Enum.filter(&(not User.blocks?(user, &1)))
360
361 conn
362 |> put_view(AccountView)
363 |> render("index.json", for: user, users: users, as: :user)
364 else
365 {:visible, false} -> {:error, :not_found}
366 _ -> json(conn, [])
367 end
368 end
369
370 @doc "GET /api/v1/statuses/:id/context"
371 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
372 with %Activity{} = activity <- Activity.get_by_id(id) do
373 activities =
374 activity.data["context"]
375 |> ActivityPub.fetch_activities_for_context(%{
376 blocking_user: user,
377 user: user,
378 exclude_id: activity.id
379 })
380 |> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
381
382 render(conn, "context.json", activity: activity, activities: activities, user: user)
383 end
384 end
385
386 @doc "GET /api/v1/favourites"
387 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
388 activities = ActivityPub.fetch_favourites(user, params)
389
390 conn
391 |> add_link_headers(activities)
392 |> render("index.json",
393 activities: activities,
394 for: user,
395 as: :activity
396 )
397 end
398
399 @doc "GET /api/v1/bookmarks"
400 def bookmarks(%{assigns: %{user: user}} = conn, params) do
401 user = User.get_cached_by_id(user.id)
402
403 bookmarks =
404 user.id
405 |> Bookmark.for_user_query()
406 |> Pleroma.Pagination.fetch_paginated(params)
407
408 activities =
409 bookmarks
410 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
411
412 conn
413 |> add_link_headers(bookmarks)
414 |> render("index.json",
415 activities: activities,
416 for: user,
417 as: :activity
418 )
419 end
420
421 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
422 if user.disclose_client do
423 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
424 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
425 else
426 Map.put(params, :generator, nil)
427 end
428 end
429
430 defp put_application(params, _), do: Map.put(params, :generator, nil)
431 end