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