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