Merge branch 'develop' into 'remove-twitter-api'
[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 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 skip_relationships: skip_relationships?(params)
122 )
123 end
124
125 @doc """
126 POST /api/v1/statuses
127
128 Creates a scheduled status when `scheduled_at` param is present and it's far enough
129 """
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 = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
139
140 attrs = %{
141 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
142 scheduled_at: scheduled_at
143 }
144
145 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
146 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
147 conn
148 |> put_view(ScheduledActivityView)
149 |> render("show.json", scheduled_activity: scheduled_activity)
150 else
151 {:far_enough, _} ->
152 params = Map.drop(params, [:scheduled_at])
153 create(%Plug.Conn{conn | body_params: params}, %{})
154
155 error ->
156 error
157 end
158 end
159
160 @doc """
161 POST /api/v1/statuses
162
163 Creates a regular status
164 """
165 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
166 params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
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, message} ->
177 conn
178 |> put_status(:unprocessable_entity)
179 |> json(%{error: message})
180 end
181 end
182
183 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
184 params = Map.put(params, :status, "")
185 create(%Plug.Conn{conn | body_params: params}, %{})
186 end
187
188 @doc "GET /api/v1/statuses/:id"
189 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
190 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
191 true <- Visibility.visible_for_user?(activity, user) do
192 try_render(conn, "show.json",
193 activity: activity,
194 for: user,
195 with_direct_conversation_id: true
196 )
197 else
198 _ -> {:error, :not_found}
199 end
200 end
201
202 @doc "DELETE /api/v1/statuses/:id"
203 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
204 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
205 json(conn, %{})
206 else
207 {:error, :not_found} = e -> e
208 _e -> render_error(conn, :forbidden, "Can't delete this post")
209 end
210 end
211
212 @doc "POST /api/v1/statuses/:id/reblog"
213 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
214 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
215 %Activity{} = announce <- Activity.normalize(announce.data) do
216 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
217 end
218 end
219
220 @doc "POST /api/v1/statuses/:id/unreblog"
221 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
222 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
223 %Activity{} = activity <- Activity.get_by_id(activity_id) do
224 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
225 end
226 end
227
228 @doc "POST /api/v1/statuses/:id/favourite"
229 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
230 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
231 %Activity{} = activity <- Activity.get_by_id(activity_id) do
232 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
233 end
234 end
235
236 @doc "POST /api/v1/statuses/:id/unfavourite"
237 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
238 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
239 %Activity{} = activity <- Activity.get_by_id(activity_id) do
240 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
241 end
242 end
243
244 @doc "POST /api/v1/statuses/:id/pin"
245 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
246 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
247 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
248 end
249 end
250
251 @doc "POST /api/v1/statuses/:id/unpin"
252 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
253 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
254 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
255 end
256 end
257
258 @doc "POST /api/v1/statuses/:id/bookmark"
259 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
260 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
261 %User{} = user <- User.get_cached_by_nickname(user.nickname),
262 true <- Visibility.visible_for_user?(activity, user),
263 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
264 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
265 end
266 end
267
268 @doc "POST /api/v1/statuses/:id/unbookmark"
269 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
270 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
271 %User{} = user <- User.get_cached_by_nickname(user.nickname),
272 true <- Visibility.visible_for_user?(activity, user),
273 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
274 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
275 end
276 end
277
278 @doc "POST /api/v1/statuses/:id/mute"
279 def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
280 with %Activity{} = activity <- Activity.get_by_id(id),
281 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
282 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
283 end
284 end
285
286 @doc "POST /api/v1/statuses/:id/unmute"
287 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
288 with %Activity{} = activity <- Activity.get_by_id(id),
289 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
290 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
291 end
292 end
293
294 @doc "GET /api/v1/statuses/:id/card"
295 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
296 def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
297 with %Activity{} = activity <- Activity.get_by_id(status_id),
298 true <- Visibility.visible_for_user?(activity, user) do
299 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
300 render(conn, "card.json", data)
301 else
302 _ -> render_error(conn, :not_found, "Record not found")
303 end
304 end
305
306 @doc "GET /api/v1/statuses/:id/favourited_by"
307 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
308 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
309 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
310 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
311 users =
312 User
313 |> Ecto.Query.where([u], u.ap_id in ^likes)
314 |> Repo.all()
315 |> Enum.filter(&(not User.blocks?(user, &1)))
316
317 conn
318 |> put_view(AccountView)
319 |> render("index.json", for: user, users: users, as: :user)
320 else
321 {:visible, false} -> {:error, :not_found}
322 _ -> json(conn, [])
323 end
324 end
325
326 @doc "GET /api/v1/statuses/:id/reblogged_by"
327 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
328 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
329 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
330 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
331 Object.normalize(activity) do
332 announces =
333 "Announce"
334 |> Activity.Queries.by_type()
335 |> Ecto.Query.where([a], a.actor in ^announces)
336 # this is to use the index
337 |> Activity.Queries.by_object_id(ap_id)
338 |> Repo.all()
339 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
340 |> Enum.map(& &1.actor)
341 |> Enum.uniq()
342
343 users =
344 User
345 |> Ecto.Query.where([u], u.ap_id in ^announces)
346 |> Repo.all()
347 |> Enum.filter(&(not User.blocks?(user, &1)))
348
349 conn
350 |> put_view(AccountView)
351 |> render("index.json", for: user, users: users, as: :user)
352 else
353 {:visible, false} -> {:error, :not_found}
354 _ -> json(conn, [])
355 end
356 end
357
358 @doc "GET /api/v1/statuses/:id/context"
359 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
360 with %Activity{} = activity <- Activity.get_by_id(id) do
361 activities =
362 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
363 "blocking_user" => user,
364 "user" => user,
365 "exclude_id" => activity.id
366 })
367
368 render(conn, "context.json", activity: activity, activities: activities, user: user)
369 end
370 end
371
372 @doc "GET /api/v1/favourites"
373 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
374 params =
375 params
376 |> Map.new(fn {key, value} -> {to_string(key), value} end)
377 |> Map.take(Pleroma.Pagination.page_keys())
378
379 activities = ActivityPub.fetch_favourites(user, params)
380
381 conn
382 |> add_link_headers(activities)
383 |> render("index.json",
384 activities: activities,
385 for: user,
386 as: :activity,
387 skip_relationships: skip_relationships?(params)
388 )
389 end
390
391 @doc "GET /api/v1/bookmarks"
392 def bookmarks(%{assigns: %{user: user}} = conn, params) do
393 user = User.get_cached_by_id(user.id)
394
395 bookmarks =
396 user.id
397 |> Bookmark.for_user_query()
398 |> Pleroma.Pagination.fetch_paginated(params)
399
400 activities =
401 bookmarks
402 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
403
404 conn
405 |> add_link_headers(bookmarks)
406 |> render("index.json",
407 activities: activities,
408 for: user,
409 as: :activity,
410 skip_relationships: skip_relationships?(params)
411 )
412 end
413 end