Merge branch 'develop' into issue/1383
[akkoma] / lib / pleroma / web / mastodon_api / controllers / status_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 end
179 end
180
181 @doc "DELETE /api/v1/statuses/:id"
182 def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
183 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
184 json(conn, %{})
185 else
186 _e -> render_error(conn, :forbidden, "Can't delete this post")
187 end
188 end
189
190 @doc "POST /api/v1/statuses/:id/reblog"
191 def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
192 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user, params),
193 %Activity{} = announce <- Activity.normalize(announce.data) do
194 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
195 end
196 end
197
198 @doc "POST /api/v1/statuses/:id/unreblog"
199 def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
200 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
201 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
202 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
203 end
204 end
205
206 @doc "POST /api/v1/statuses/:id/favourite"
207 def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
208 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
209 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
210 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
211 end
212 end
213
214 @doc "POST /api/v1/statuses/:id/unfavourite"
215 def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
216 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
217 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
218 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
219 end
220 end
221
222 @doc "POST /api/v1/statuses/:id/pin"
223 def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
224 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
225 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
226 end
227 end
228
229 @doc "POST /api/v1/statuses/:id/unpin"
230 def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
231 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) 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/bookmark"
237 def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
238 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
239 %User{} = user <- User.get_cached_by_nickname(user.nickname),
240 true <- Visibility.visible_for_user?(activity, user),
241 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) 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/unbookmark"
247 def unbookmark(%{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.destroy(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/mute"
257 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
258 with %Activity{} = activity <- Activity.get_by_id(id),
259 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
260 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
261 end
262 end
263
264 @doc "POST /api/v1/statuses/:id/unmute"
265 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
266 with %Activity{} = activity <- Activity.get_by_id(id),
267 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
268 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
269 end
270 end
271
272 @doc "GET /api/v1/statuses/:id/card"
273 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
274 def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
275 with %Activity{} = activity <- Activity.get_by_id(status_id),
276 true <- Visibility.visible_for_user?(activity, user) do
277 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
278 render(conn, "card.json", data)
279 else
280 _ -> render_error(conn, :not_found, "Record not found")
281 end
282 end
283
284 @doc "GET /api/v1/statuses/:id/favourited_by"
285 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
286 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
287 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
288 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
289 users =
290 User
291 |> Ecto.Query.where([u], u.ap_id in ^likes)
292 |> Repo.all()
293 |> Enum.filter(&(not User.blocks?(user, &1)))
294
295 conn
296 |> put_view(AccountView)
297 |> render("index.json", for: user, users: users, as: :user)
298 else
299 {:visible, false} -> {:error, :not_found}
300 _ -> json(conn, [])
301 end
302 end
303
304 @doc "GET /api/v1/statuses/:id/reblogged_by"
305 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
306 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
307 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
308 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
309 Object.normalize(activity) do
310 announces =
311 "Announce"
312 |> Activity.Queries.by_type()
313 |> Ecto.Query.where([a], a.actor in ^announces)
314 # this is to use the index
315 |> Activity.Queries.by_object_id(ap_id)
316 |> Repo.all()
317 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
318 |> Enum.map(& &1.actor)
319 |> Enum.uniq()
320
321 users =
322 User
323 |> Ecto.Query.where([u], u.ap_id in ^announces)
324 |> Repo.all()
325 |> Enum.filter(&(not User.blocks?(user, &1)))
326
327 conn
328 |> put_view(AccountView)
329 |> render("index.json", for: user, users: users, as: :user)
330 else
331 {:visible, false} -> {:error, :not_found}
332 _ -> json(conn, [])
333 end
334 end
335
336 @doc "GET /api/v1/statuses/:id/context"
337 def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
338 with %Activity{} = activity <- Activity.get_by_id(id) do
339 activities =
340 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
341 "blocking_user" => user,
342 "user" => user,
343 "exclude_id" => activity.id
344 })
345
346 render(conn, "context.json", activity: activity, activities: activities, user: user)
347 end
348 end
349
350 @doc "GET /api/v1/favourites"
351 def favourites(%{assigns: %{user: user}} = conn, params) do
352 activities =
353 ActivityPub.fetch_favourites(
354 user,
355 Map.take(params, Pleroma.Pagination.page_keys())
356 )
357
358 conn
359 |> add_link_headers(activities)
360 |> render("index.json", activities: activities, for: user, as: :activity)
361 end
362
363 @doc "GET /api/v1/bookmarks"
364 def bookmarks(%{assigns: %{user: user}} = conn, params) do
365 user = User.get_cached_by_id(user.id)
366
367 bookmarks =
368 user.id
369 |> Bookmark.for_user_query()
370 |> Pleroma.Pagination.fetch_paginated(params)
371
372 activities =
373 bookmarks
374 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
375
376 conn
377 |> add_link_headers(bookmarks)
378 |> render("index.json", %{activities: activities, for: user, as: :activity})
379 end
380 end