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