Add ability to set a default post expiry (#321)
[akkoma] / lib / pleroma / web / mastodon_api / controllers / status_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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]
10
11 require Ecto.Query
12
13 alias Pleroma.Activity
14 alias Pleroma.Bookmark
15 alias Pleroma.Object
16 alias Pleroma.Repo
17 alias Pleroma.Config
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 alias Pleroma.Web.OAuth.Token
26 alias Pleroma.Web.Plugs.OAuthScopesPlug
27 alias Pleroma.Web.Plugs.RateLimiter
28
29 plug(Pleroma.Web.ApiSpec.CastAndValidate)
30
31 plug(:skip_public_check when action in [:index, :show])
32
33 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
34 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
35
36 plug(
37 OAuthScopesPlug,
38 %{@unauthenticated_access | scopes: ["read:statuses"]}
39 when action in [
40 :index,
41 :show,
42 :context,
43 :translate,
44 :show_history,
45 :show_source
46 ]
47 )
48
49 plug(
50 OAuthScopesPlug,
51 %{scopes: ["write:statuses"]}
52 when action in [
53 :create,
54 :delete,
55 :reblog,
56 :unreblog,
57 :update
58 ]
59 )
60
61 plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
62
63 plug(
64 OAuthScopesPlug,
65 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
66 )
67
68 plug(
69 OAuthScopesPlug,
70 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
71 )
72
73 plug(
74 OAuthScopesPlug,
75 %{@unauthenticated_access | scopes: ["read:accounts"]}
76 when action in [:favourited_by, :reblogged_by]
77 )
78
79 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
80
81 # Note: scope not present in Mastodon: read:bookmarks
82 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
83
84 # Note: scope not present in Mastodon: write:bookmarks
85 plug(
86 OAuthScopesPlug,
87 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
88 )
89
90 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
91
92 plug(
93 RateLimiter,
94 [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
95 when action in ~w(reblog unreblog)a
96 )
97
98 plug(
99 RateLimiter,
100 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
101 when action in ~w(favourite unfavourite)a
102 )
103
104 plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
105
106 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
107
108 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
109
110 @doc """
111 GET `/api/v1/statuses?ids[]=1&ids[]=2`
112
113 `ids` query param is required
114 """
115 def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
116 limit = 100
117
118 activities =
119 ids
120 |> Enum.take(limit)
121 |> Activity.all_by_ids_with_object()
122 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
123
124 render(conn, "index.json",
125 activities: activities,
126 for: user,
127 as: :activity,
128 with_muted: Map.get(params, :with_muted, false)
129 )
130 end
131
132 @doc """
133 POST /api/v1/statuses
134 """
135 # Creates a scheduled status when `scheduled_at` param is present and it's far enough
136 def create(
137 %{
138 assigns: %{user: user},
139 body_params: %{status: _, scheduled_at: scheduled_at} = params
140 } = conn,
141 _
142 )
143 when not is_nil(scheduled_at) do
144 params =
145 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
146 |> put_application(conn)
147
148 attrs = %{
149 params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
150 scheduled_at: scheduled_at
151 }
152
153 with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
154 {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
155 conn
156 |> put_view(ScheduledActivityView)
157 |> render("show.json", scheduled_activity: scheduled_activity)
158 else
159 {:far_enough, _} ->
160 params = Map.drop(params, [:scheduled_at])
161 create(%Plug.Conn{conn | body_params: params}, %{})
162
163 error ->
164 error
165 end
166 end
167
168 # Creates a regular status
169 def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
170 params =
171 Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
172 |> put_application(conn)
173
174 expires_in_seconds =
175 if is_nil(user.status_ttl_days),
176 do: nil,
177 else: 60 * 60 * 24 * user.status_ttl_days
178
179 params =
180 if is_nil(expires_in_seconds),
181 do: params,
182 else: Map.put(params, :expires_in, expires_in_seconds)
183
184 with {:ok, activity} <- CommonAPI.post(user, params) do
185 try_render(conn, "show.json",
186 activity: activity,
187 for: user,
188 as: :activity,
189 with_direct_conversation_id: true
190 )
191 else
192 {:error, {:reject, message}} ->
193 conn
194 |> put_status(:unprocessable_entity)
195 |> json(%{error: message})
196
197 {:error, message} ->
198 conn
199 |> put_status(:unprocessable_entity)
200 |> json(%{error: message})
201 end
202 end
203
204 def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
205 params = Map.put(params, :status, "")
206 create(%Plug.Conn{conn | body_params: params}, %{})
207 end
208
209 @doc "GET /api/v1/statuses/:id/history"
210 def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
211 with user = assigns[:user],
212 %Activity{} = activity <- Activity.get_by_id_with_object(id),
213 true <- Visibility.visible_for_user?(activity, user) do
214 try_render(conn, "history.json",
215 activity: activity,
216 for: user,
217 with_direct_conversation_id: true,
218 with_muted: Map.get(params, :with_muted, false)
219 )
220 else
221 _ -> {:error, :not_found}
222 end
223 end
224
225 @doc "GET /api/v1/statuses/:id/source"
226 def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
227 with user = assigns[:user],
228 %Activity{} = activity <- Activity.get_by_id_with_object(id),
229 true <- Visibility.visible_for_user?(activity, user) do
230 try_render(conn, "source.json",
231 activity: activity,
232 for: user
233 )
234 else
235 _ -> {:error, :not_found}
236 end
237 end
238
239 @doc "PUT /api/v1/statuses/:id"
240 def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
241 with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
242 {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
243 {_, true} <- {:is_create, activity.data["type"] == "Create"},
244 actor <- Activity.user_actor(activity),
245 {_, true} <- {:own_status, actor.id == user.id},
246 changes <- body_params |> put_application(conn),
247 {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
248 {_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
249 try_render(conn, "show.json",
250 activity: activity,
251 for: user,
252 with_direct_conversation_id: true,
253 with_muted: Map.get(params, :with_muted, false)
254 )
255 else
256 {:own_status, _} -> {:error, :forbidden}
257 {:pipeline, _} -> {:error, :internal_server_error}
258 _ -> {:error, :not_found}
259 end
260 end
261
262 @doc "GET /api/v1/statuses/:id"
263 def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
264 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
265 true <- Visibility.visible_for_user?(activity, user) do
266 try_render(conn, "show.json",
267 activity: activity,
268 for: user,
269 with_direct_conversation_id: true,
270 with_muted: Map.get(params, :with_muted, false)
271 )
272 else
273 _ -> {:error, :not_found}
274 end
275 end
276
277 @doc "DELETE /api/v1/statuses/:id"
278 def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
279 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
280 {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
281 try_render(conn, "show.json",
282 activity: activity,
283 for: user,
284 with_direct_conversation_id: true,
285 with_source: true
286 )
287 else
288 _e -> {:error, :not_found}
289 end
290 end
291
292 @doc "POST /api/v1/statuses/:id/reblog"
293 def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
294 with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
295 %Activity{} = announce <- Activity.normalize(announce.data) do
296 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
297 end
298 end
299
300 @doc "POST /api/v1/statuses/:id/unreblog"
301 def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
302 with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
303 %Activity{} = activity <- Activity.get_by_id(activity_id) do
304 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
305 end
306 end
307
308 @doc "POST /api/v1/statuses/:id/favourite"
309 def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
310 with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
311 %Activity{} = activity <- Activity.get_by_id(activity_id) do
312 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
313 end
314 end
315
316 @doc "POST /api/v1/statuses/:id/unfavourite"
317 def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
318 with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
319 %Activity{} = activity <- Activity.get_by_id(activity_id) do
320 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
321 end
322 end
323
324 @doc "POST /api/v1/statuses/:id/pin"
325 def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
326 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
327 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
328 else
329 {:error, :pinned_statuses_limit_reached} ->
330 {:error, "You have already pinned the maximum number of statuses"}
331
332 {:error, :ownership_error} ->
333 {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
334
335 {:error, :visibility_error} ->
336 {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
337
338 error ->
339 error
340 end
341 end
342
343 @doc "POST /api/v1/statuses/:id/unpin"
344 def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
345 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
346 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
347 end
348 end
349
350 @doc "POST /api/v1/statuses/:id/bookmark"
351 def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
352 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
353 %User{} = user <- User.get_cached_by_nickname(user.nickname),
354 true <- Visibility.visible_for_user?(activity, user),
355 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
356 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
357 end
358 end
359
360 @doc "POST /api/v1/statuses/:id/unbookmark"
361 def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
362 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
363 %User{} = user <- User.get_cached_by_nickname(user.nickname),
364 true <- Visibility.visible_for_user?(activity, user),
365 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
366 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
367 end
368 end
369
370 @doc "POST /api/v1/statuses/:id/mute"
371 def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
372 with %Activity{} = activity <- Activity.get_by_id(id),
373 {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
374 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
375 end
376 end
377
378 @doc "POST /api/v1/statuses/:id/unmute"
379 def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
380 with %Activity{} = activity <- Activity.get_by_id(id),
381 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
382 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
383 end
384 end
385
386 @doc "GET /api/v1/statuses/:id/favourited_by"
387 def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
388 with true <- Pleroma.Config.get([:instance, :show_reactions]),
389 %Activity{} = activity <- Activity.get_by_id_with_object(id),
390 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
391 %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
392 users =
393 User
394 |> Ecto.Query.where([u], u.ap_id in ^likes)
395 |> Repo.all()
396 |> Enum.filter(&(not User.blocks?(user, &1)))
397
398 conn
399 |> put_view(AccountView)
400 |> render("index.json", for: user, users: users, as: :user)
401 else
402 {:visible, false} -> {:error, :not_found}
403 _ -> json(conn, [])
404 end
405 end
406
407 @doc "GET /api/v1/statuses/:id/reblogged_by"
408 def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
409 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
410 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
411 %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
412 Object.normalize(activity, fetch: false) do
413 announces =
414 "Announce"
415 |> Activity.Queries.by_type()
416 |> Ecto.Query.where([a], a.actor in ^announces)
417 # this is to use the index
418 |> Activity.Queries.by_object_id(ap_id)
419 |> Repo.all()
420 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
421 |> Enum.map(& &1.actor)
422 |> Enum.uniq()
423
424 users =
425 User
426 |> Ecto.Query.where([u], u.ap_id in ^announces)
427 |> Repo.all()
428 |> Enum.filter(&(not User.blocks?(user, &1)))
429
430 conn
431 |> put_view(AccountView)
432 |> render("index.json", for: user, users: users, as: :user)
433 else
434 {:visible, false} -> {:error, :not_found}
435 _ -> json(conn, [])
436 end
437 end
438
439 @doc "GET /api/v1/statuses/:id/context"
440 def context(%{assigns: %{user: user}} = conn, %{id: id}) do
441 with %Activity{} = activity <- Activity.get_by_id(id) do
442 activities =
443 activity.data["context"]
444 |> ActivityPub.fetch_activities_for_context(%{
445 blocking_user: user,
446 user: user,
447 exclude_id: activity.id
448 })
449 |> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
450
451 render(conn, "context.json", activity: activity, activities: activities, user: user)
452 end
453 end
454
455 @doc "GET /api/v1/favourites"
456 def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
457 activities = ActivityPub.fetch_favourites(user, params)
458
459 conn
460 |> add_link_headers(activities)
461 |> render("index.json",
462 activities: activities,
463 for: user,
464 as: :activity
465 )
466 end
467
468 @doc "GET /api/v1/bookmarks"
469 def bookmarks(%{assigns: %{user: user}} = conn, params) do
470 user = User.get_cached_by_id(user.id)
471
472 bookmarks =
473 user.id
474 |> Bookmark.for_user_query()
475 |> Pleroma.Pagination.fetch_paginated(params)
476
477 activities =
478 bookmarks
479 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
480
481 conn
482 |> add_link_headers(bookmarks)
483 |> render("index.json",
484 activities: activities,
485 for: user,
486 as: :activity
487 )
488 end
489
490 @doc "GET /api/v1/statuses/:id/translations/:language"
491 def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language} = params) do
492 with {:enabled, true} <- {:enabled, Config.get([:translator, :enabled])},
493 %Activity{} = activity <- Activity.get_by_id_with_object(id),
494 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
495 translation_module <- Config.get([:translator, :module]),
496 {:ok, detected, translation} <-
497 fetch_or_translate(
498 activity.id,
499 activity.object.data["content"],
500 Map.get(params, :from, nil),
501 language,
502 translation_module
503 ) do
504 json(conn, %{detected_language: detected, text: translation})
505 else
506 {:enabled, false} ->
507 conn
508 |> put_status(:bad_request)
509 |> json(%{"error" => "Translation is not enabled"})
510
511 {:visible, false} ->
512 {:error, :not_found}
513
514 e ->
515 e
516 end
517 end
518
519 defp fetch_or_translate(status_id, text, source_language, target_language, translation_module) do
520 @cachex.fetch!(
521 :translations_cache,
522 "translations:#{status_id}:#{source_language}:#{target_language}",
523 fn _ ->
524 value = translation_module.translate(text, source_language, target_language)
525
526 with {:ok, _, _} <- value do
527 value
528 else
529 _ -> {:ignore, value}
530 end
531 end
532 )
533 end
534
535 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
536 if user.disclose_client do
537 %{client_name: client_name, website: website} = Repo.preload(token, :app).app
538 Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
539 else
540 Map.put(params, :generator, nil)
541 end
542 end
543
544 defp put_application(params, _), do: Map.put(params, :generator, nil)
545 end