Allow activities pagination via limit/offset
[akkoma] / lib / pleroma / web / admin_api / admin_api_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.AdminAPI.AdminAPIController do
6 use Pleroma.Web, :controller
7 alias Pleroma.Activity
8 alias Pleroma.ModerationLog
9 alias Pleroma.User
10 alias Pleroma.UserInviteToken
11 alias Pleroma.Web.ActivityPub.ActivityPub
12 alias Pleroma.Web.ActivityPub.Relay
13 alias Pleroma.Web.AdminAPI.AccountView
14 alias Pleroma.Web.AdminAPI.Config
15 alias Pleroma.Web.AdminAPI.ConfigView
16 alias Pleroma.Web.AdminAPI.ModerationLogView
17 alias Pleroma.Web.AdminAPI.Report
18 alias Pleroma.Web.AdminAPI.ReportView
19 alias Pleroma.Web.AdminAPI.Search
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.MastodonAPI.StatusView
22
23 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
24
25 require Logger
26
27 @users_page_size 50
28
29 action_fallback(:errors)
30
31 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
32 user = User.get_cached_by_nickname(nickname)
33 User.delete(user)
34
35 ModerationLog.insert_log(%{
36 actor: admin,
37 subject: user,
38 action: "delete"
39 })
40
41 conn
42 |> json(nickname)
43 end
44
45 def user_follow(%{assigns: %{user: admin}} = conn, %{
46 "follower" => follower_nick,
47 "followed" => followed_nick
48 }) do
49 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
50 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
51 User.follow(follower, followed)
52
53 ModerationLog.insert_log(%{
54 actor: admin,
55 followed: followed,
56 follower: follower,
57 action: "follow"
58 })
59 end
60
61 conn
62 |> json("ok")
63 end
64
65 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
66 "follower" => follower_nick,
67 "followed" => followed_nick
68 }) do
69 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
70 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
71 User.unfollow(follower, followed)
72
73 ModerationLog.insert_log(%{
74 actor: admin,
75 followed: followed,
76 follower: follower,
77 action: "unfollow"
78 })
79 end
80
81 conn
82 |> json("ok")
83 end
84
85 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
86 changesets =
87 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
88 user_data = %{
89 nickname: nickname,
90 name: nickname,
91 email: email,
92 password: password,
93 password_confirmation: password,
94 bio: "."
95 }
96
97 User.register_changeset(%User{}, user_data, need_confirmation: false)
98 end)
99 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
100 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
101 end)
102
103 case Pleroma.Repo.transaction(changesets) do
104 {:ok, users} ->
105 res =
106 users
107 |> Map.values()
108 |> Enum.map(fn user ->
109 {:ok, user} = User.post_register_action(user)
110
111 user
112 end)
113 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
114
115 ModerationLog.insert_log(%{
116 actor: admin,
117 subjects: Map.values(users),
118 action: "create"
119 })
120
121 conn
122 |> json(res)
123
124 {:error, id, changeset, _} ->
125 res =
126 Enum.map(changesets.operations, fn
127 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
128 AccountView.render("create-error.json", %{changeset: changeset})
129
130 {_, {:changeset, current_changeset, _}} ->
131 AccountView.render("create-error.json", %{changeset: current_changeset})
132 end)
133
134 conn
135 |> put_status(:conflict)
136 |> json(res)
137 end
138 end
139
140 def user_show(conn, %{"nickname" => nickname}) do
141 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
142 conn
143 |> put_view(AccountView)
144 |> render("show.json", %{user: user})
145 else
146 _ -> {:error, :not_found}
147 end
148 end
149
150 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
151 godmode = params["godmode"] == "true" || params["godmode"] == true
152
153 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
154 {_, page_size} = page_params(params)
155
156 activities =
157 ActivityPub.fetch_user_activities(user, nil, %{
158 "limit" => page_size,
159 "godmode" => godmode
160 })
161
162 conn
163 |> put_view(StatusView)
164 |> render("index.json", %{activities: activities, as: :activity})
165 else
166 _ -> {:error, :not_found}
167 end
168 end
169
170 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
171 user = User.get_cached_by_nickname(nickname)
172
173 {:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
174
175 action = if user.info.deactivated, do: "activate", else: "deactivate"
176
177 ModerationLog.insert_log(%{
178 actor: admin,
179 subject: user,
180 action: action
181 })
182
183 conn
184 |> put_view(AccountView)
185 |> render("show.json", %{user: updated_user})
186 end
187
188 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
189 with {:ok, _} <- User.tag(nicknames, tags) do
190 ModerationLog.insert_log(%{
191 actor: admin,
192 nicknames: nicknames,
193 tags: tags,
194 action: "tag"
195 })
196
197 json_response(conn, :no_content, "")
198 end
199 end
200
201 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
202 with {:ok, _} <- User.untag(nicknames, tags) do
203 ModerationLog.insert_log(%{
204 actor: admin,
205 nicknames: nicknames,
206 tags: tags,
207 action: "untag"
208 })
209
210 json_response(conn, :no_content, "")
211 end
212 end
213
214 def list_users(conn, params) do
215 {page, page_size} = page_params(params)
216 filters = maybe_parse_filters(params["filters"])
217
218 search_params = %{
219 query: params["query"],
220 page: page,
221 page_size: page_size,
222 tags: params["tags"],
223 name: params["name"],
224 email: params["email"]
225 }
226
227 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
228 do:
229 conn
230 |> json(
231 AccountView.render("index.json",
232 users: users,
233 count: count,
234 page_size: page_size
235 )
236 )
237 end
238
239 @filters ~w(local external active deactivated is_admin is_moderator)
240
241 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
242 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
243
244 defp maybe_parse_filters(filters) do
245 filters
246 |> String.split(",")
247 |> Enum.filter(&Enum.member?(@filters, &1))
248 |> Enum.map(&String.to_atom(&1))
249 |> Enum.into(%{}, &{&1, true})
250 end
251
252 def right_add(%{assigns: %{user: admin}} = conn, %{
253 "permission_group" => permission_group,
254 "nickname" => nickname
255 })
256 when permission_group in ["moderator", "admin"] do
257 user = User.get_cached_by_nickname(nickname)
258
259 info =
260 %{}
261 |> Map.put("is_" <> permission_group, true)
262
263 info_cng = User.Info.admin_api_update(user.info, info)
264
265 cng =
266 user
267 |> Ecto.Changeset.change()
268 |> Ecto.Changeset.put_embed(:info, info_cng)
269
270 ModerationLog.insert_log(%{
271 action: "grant",
272 actor: admin,
273 subject: user,
274 permission: permission_group
275 })
276
277 {:ok, _user} = User.update_and_set_cache(cng)
278
279 json(conn, info)
280 end
281
282 def right_add(conn, _) do
283 render_error(conn, :not_found, "No such permission_group")
284 end
285
286 def right_get(conn, %{"nickname" => nickname}) do
287 user = User.get_cached_by_nickname(nickname)
288
289 conn
290 |> json(%{
291 is_moderator: user.info.is_moderator,
292 is_admin: user.info.is_admin
293 })
294 end
295
296 def right_delete(
297 %{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn,
298 %{
299 "permission_group" => permission_group,
300 "nickname" => nickname
301 }
302 )
303 when permission_group in ["moderator", "admin"] do
304 if admin_nickname == nickname do
305 render_error(conn, :forbidden, "You can't revoke your own admin status.")
306 else
307 user = User.get_cached_by_nickname(nickname)
308
309 info =
310 %{}
311 |> Map.put("is_" <> permission_group, false)
312
313 info_cng = User.Info.admin_api_update(user.info, info)
314
315 cng =
316 Ecto.Changeset.change(user)
317 |> Ecto.Changeset.put_embed(:info, info_cng)
318
319 {:ok, _user} = User.update_and_set_cache(cng)
320
321 ModerationLog.insert_log(%{
322 action: "revoke",
323 actor: admin,
324 subject: user,
325 permission: permission_group
326 })
327
328 json(conn, info)
329 end
330 end
331
332 def right_delete(conn, _) do
333 render_error(conn, :not_found, "No such permission_group")
334 end
335
336 def set_activation_status(%{assigns: %{user: admin}} = conn, %{
337 "nickname" => nickname,
338 "status" => status
339 }) do
340 with {:ok, status} <- Ecto.Type.cast(:boolean, status),
341 %User{} = user <- User.get_cached_by_nickname(nickname),
342 {:ok, _} <- User.deactivate(user, !status) do
343 action = if(user.info.deactivated, do: "activate", else: "deactivate")
344
345 ModerationLog.insert_log(%{
346 actor: admin,
347 subject: user,
348 action: action
349 })
350
351 json_response(conn, :no_content, "")
352 end
353 end
354
355 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
356 with {:ok, _message} <- Relay.follow(target) do
357 ModerationLog.insert_log(%{
358 action: "relay_follow",
359 actor: admin,
360 target: target
361 })
362
363 json(conn, target)
364 else
365 _ ->
366 conn
367 |> put_status(500)
368 |> json(target)
369 end
370 end
371
372 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
373 with {:ok, _message} <- Relay.unfollow(target) do
374 ModerationLog.insert_log(%{
375 action: "relay_unfollow",
376 actor: admin,
377 target: target
378 })
379
380 json(conn, target)
381 else
382 _ ->
383 conn
384 |> put_status(500)
385 |> json(target)
386 end
387 end
388
389 @doc "Sends registration invite via email"
390 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
391 with true <-
392 Pleroma.Config.get([:instance, :invites_enabled]) &&
393 !Pleroma.Config.get([:instance, :registrations_open]),
394 {:ok, invite_token} <- UserInviteToken.create_invite(),
395 email <-
396 Pleroma.Emails.UserEmail.user_invitation_email(
397 user,
398 invite_token,
399 email,
400 params["name"]
401 ),
402 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
403 json_response(conn, :no_content, "")
404 end
405 end
406
407 @doc "Create an account registration invite token"
408 def create_invite_token(conn, params) do
409 opts = %{}
410
411 opts =
412 if params["max_use"],
413 do: Map.put(opts, :max_use, params["max_use"]),
414 else: opts
415
416 opts =
417 if params["expires_at"],
418 do: Map.put(opts, :expires_at, params["expires_at"]),
419 else: opts
420
421 {:ok, invite} = UserInviteToken.create_invite(opts)
422
423 json(conn, AccountView.render("invite.json", %{invite: invite}))
424 end
425
426 @doc "Get list of created invites"
427 def invites(conn, _params) do
428 invites = UserInviteToken.list_invites()
429
430 conn
431 |> put_view(AccountView)
432 |> render("invites.json", %{invites: invites})
433 end
434
435 @doc "Revokes invite by token"
436 def revoke_invite(conn, %{"token" => token}) do
437 with {:ok, invite} <- UserInviteToken.find_by_token(token),
438 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
439 conn
440 |> put_view(AccountView)
441 |> render("invite.json", %{invite: updated_invite})
442 else
443 nil -> {:error, :not_found}
444 end
445 end
446
447 @doc "Get a password reset token (base64 string) for given nickname"
448 def get_password_reset(conn, %{"nickname" => nickname}) do
449 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
450 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
451
452 conn
453 |> json(token.token)
454 end
455
456 @doc "Force password reset for a given user"
457 def force_password_reset(conn, %{"nickname" => nickname}) do
458 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
459
460 User.force_password_reset_async(user)
461
462 json_response(conn, :no_content, "")
463 end
464
465 def list_reports(conn, params) do
466 {page, page_size} = page_params(params)
467
468 params =
469 params
470 |> Map.put("type", "Flag")
471 |> Map.put("skip_preload", true)
472 |> Map.put("total", true)
473 |> Map.put("limit", page_size)
474 |> Map.put("offset", (page - 1) * page_size)
475
476 reports = ActivityPub.fetch_activities([], params, :offset)
477
478 conn
479 |> put_view(ReportView)
480 |> render("index.json", %{reports: reports})
481 end
482
483 def report_show(conn, %{"id" => id}) do
484 with %Activity{} = report <- Activity.get_by_id(id) do
485 conn
486 |> put_view(ReportView)
487 |> render("show.json", Report.extract_report_info(report))
488 else
489 _ -> {:error, :not_found}
490 end
491 end
492
493 def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
494 with {:ok, report} <- CommonAPI.update_report_state(id, state) do
495 ModerationLog.insert_log(%{
496 action: "report_update",
497 actor: admin,
498 subject: report
499 })
500
501 conn
502 |> put_view(ReportView)
503 |> render("show.json", Report.extract_report_info(report))
504 end
505 end
506
507 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
508 with false <- is_nil(params["status"]),
509 %Activity{} <- Activity.get_by_id(id) do
510 params =
511 params
512 |> Map.put("in_reply_to_status_id", id)
513 |> Map.put("visibility", "direct")
514
515 {:ok, activity} = CommonAPI.post(user, params)
516
517 ModerationLog.insert_log(%{
518 action: "report_response",
519 actor: user,
520 subject: activity,
521 text: params["status"]
522 })
523
524 conn
525 |> put_view(StatusView)
526 |> render("status.json", %{activity: activity})
527 else
528 true ->
529 {:param_cast, nil}
530
531 nil ->
532 {:error, :not_found}
533 end
534 end
535
536 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
537 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
538 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
539
540 ModerationLog.insert_log(%{
541 action: "status_update",
542 actor: admin,
543 subject: activity,
544 sensitive: sensitive,
545 visibility: params["visibility"]
546 })
547
548 conn
549 |> put_view(StatusView)
550 |> render("status.json", %{activity: activity})
551 end
552 end
553
554 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
555 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
556 ModerationLog.insert_log(%{
557 action: "status_delete",
558 actor: user,
559 subject_id: id
560 })
561
562 json(conn, %{})
563 end
564 end
565
566 def list_log(conn, params) do
567 {page, page_size} = page_params(params)
568
569 log = ModerationLog.get_all(page, page_size)
570
571 conn
572 |> put_view(ModerationLogView)
573 |> render("index.json", %{log: log})
574 end
575
576 def migrate_to_db(conn, _params) do
577 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
578 json(conn, %{})
579 end
580
581 def migrate_from_db(conn, _params) do
582 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
583 json(conn, %{})
584 end
585
586 def config_show(conn, _params) do
587 configs = Pleroma.Repo.all(Config)
588
589 conn
590 |> put_view(ConfigView)
591 |> render("index.json", %{configs: configs})
592 end
593
594 def config_update(conn, %{"configs" => configs}) do
595 updated =
596 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
597 updated =
598 Enum.map(configs, fn
599 %{"group" => group, "key" => key, "delete" => "true"} = params ->
600 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
601 config
602
603 %{"group" => group, "key" => key, "value" => value} ->
604 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
605 config
606 end)
607 |> Enum.reject(&is_nil(&1))
608
609 Pleroma.Config.TransferTask.load_and_update_env()
610 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
611 updated
612 else
613 []
614 end
615
616 conn
617 |> put_view(ConfigView)
618 |> render("index.json", %{configs: updated})
619 end
620
621 def reload_emoji(conn, _params) do
622 Pleroma.Emoji.reload()
623
624 conn |> json("ok")
625 end
626
627 def errors(conn, {:error, :not_found}) do
628 conn
629 |> put_status(:not_found)
630 |> json(dgettext("errors", "Not found"))
631 end
632
633 def errors(conn, {:error, reason}) do
634 conn
635 |> put_status(:bad_request)
636 |> json(reason)
637 end
638
639 def errors(conn, {:param_cast, _}) do
640 conn
641 |> put_status(:bad_request)
642 |> json(dgettext("errors", "Invalid parameters"))
643 end
644
645 def errors(conn, _) do
646 conn
647 |> put_status(:internal_server_error)
648 |> json(dgettext("errors", "Something went wrong"))
649 end
650
651 defp page_params(params) do
652 {get_page(params["page"]), get_page_size(params["page_size"])}
653 end
654
655 defp get_page(page_string) when is_nil(page_string), do: 1
656
657 defp get_page(page_string) do
658 case Integer.parse(page_string) do
659 {page, _} -> page
660 :error -> 1
661 end
662 end
663
664 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
665
666 defp get_page_size(page_size_string) do
667 case Integer.parse(page_size_string) do
668 {page_size, _} -> page_size
669 :error -> @users_page_size
670 end
671 end
672 end