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