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