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