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