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