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