Use ModerationLog instead of Logger
[akkoma] / lib / pleroma / web / admin_api / controllers / admin_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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
8 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
9
10 alias Pleroma.Config
11 alias Pleroma.MFA
12 alias Pleroma.ModerationLog
13 alias Pleroma.Plugs.OAuthScopesPlug
14 alias Pleroma.Stats
15 alias Pleroma.User
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Builder
18 alias Pleroma.Web.ActivityPub.Pipeline
19 alias Pleroma.Web.AdminAPI
20 alias Pleroma.Web.AdminAPI.AccountView
21 alias Pleroma.Web.AdminAPI.ModerationLogView
22 alias Pleroma.Web.AdminAPI.Search
23 alias Pleroma.Web.Endpoint
24 alias Pleroma.Web.Router
25
26 require Logger
27
28 @users_page_size 50
29
30 plug(
31 OAuthScopesPlug,
32 %{scopes: ["read:accounts"], admin: true}
33 when action in [:list_users, :user_show, :right_get, :show_user_credentials, :create_backup]
34 )
35
36 plug(
37 OAuthScopesPlug,
38 %{scopes: ["write:accounts"], admin: true}
39 when action in [
40 :get_password_reset,
41 :force_password_reset,
42 :user_delete,
43 :users_create,
44 :user_toggle_activation,
45 :user_activate,
46 :user_deactivate,
47 :user_approve,
48 :tag_users,
49 :untag_users,
50 :right_add,
51 :right_add_multiple,
52 :right_delete,
53 :disable_mfa,
54 :right_delete_multiple,
55 :update_user_credentials
56 ]
57 )
58
59 plug(
60 OAuthScopesPlug,
61 %{scopes: ["write:follows"], admin: true}
62 when action in [:user_follow, :user_unfollow]
63 )
64
65 plug(
66 OAuthScopesPlug,
67 %{scopes: ["read:statuses"], admin: true}
68 when action in [:list_user_statuses, :list_instance_statuses]
69 )
70
71 plug(
72 OAuthScopesPlug,
73 %{scopes: ["read:chats"], admin: true}
74 when action in [:list_user_chats]
75 )
76
77 plug(
78 OAuthScopesPlug,
79 %{scopes: ["read"], admin: true}
80 when action in [
81 :list_log,
82 :stats,
83 :need_reboot
84 ]
85 )
86
87 plug(
88 OAuthScopesPlug,
89 %{scopes: ["write"], admin: true}
90 when action in [
91 :restart,
92 :resend_confirmation_email,
93 :confirm_email,
94 :reload_emoji
95 ]
96 )
97
98 action_fallback(AdminAPI.FallbackController)
99
100 def user_delete(conn, %{"nickname" => nickname}) do
101 user_delete(conn, %{"nicknames" => [nickname]})
102 end
103
104 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
105 users =
106 nicknames
107 |> Enum.map(&User.get_cached_by_nickname/1)
108
109 users
110 |> Enum.each(fn user ->
111 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
112 Pipeline.common_pipeline(delete_data, local: true)
113 end)
114
115 ModerationLog.insert_log(%{
116 actor: admin,
117 subject: users,
118 action: "delete"
119 })
120
121 json(conn, 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 json(conn, "ok")
141 end
142
143 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
144 "follower" => follower_nick,
145 "followed" => followed_nick
146 }) do
147 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
148 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
149 User.unfollow(follower, followed)
150
151 ModerationLog.insert_log(%{
152 actor: admin,
153 followed: followed,
154 follower: follower,
155 action: "unfollow"
156 })
157 end
158
159 json(conn, "ok")
160 end
161
162 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
163 changesets =
164 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
165 user_data = %{
166 nickname: nickname,
167 name: nickname,
168 email: email,
169 password: password,
170 password_confirmation: password,
171 bio: "."
172 }
173
174 User.register_changeset(%User{}, user_data, need_confirmation: false)
175 end)
176 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
177 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
178 end)
179
180 case Pleroma.Repo.transaction(changesets) do
181 {:ok, users} ->
182 res =
183 users
184 |> Map.values()
185 |> Enum.map(fn user ->
186 {:ok, user} = User.post_register_action(user)
187
188 user
189 end)
190 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
191
192 ModerationLog.insert_log(%{
193 actor: admin,
194 subjects: Map.values(users),
195 action: "create"
196 })
197
198 json(conn, res)
199
200 {:error, id, changeset, _} ->
201 res =
202 Enum.map(changesets.operations, fn
203 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
204 AccountView.render("create-error.json", %{changeset: changeset})
205
206 {_, {:changeset, current_changeset, _}} ->
207 AccountView.render("create-error.json", %{changeset: current_changeset})
208 end)
209
210 conn
211 |> put_status(:conflict)
212 |> json(res)
213 end
214 end
215
216 def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
217 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
218 conn
219 |> put_view(AccountView)
220 |> render("show.json", %{user: user})
221 else
222 _ -> {:error, :not_found}
223 end
224 end
225
226 def list_instance_statuses(conn, %{"instance" => instance} = params) do
227 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
228 {page, page_size} = page_params(params)
229
230 activities =
231 ActivityPub.fetch_statuses(nil, %{
232 instance: instance,
233 limit: page_size,
234 offset: (page - 1) * page_size,
235 exclude_reblogs: not with_reblogs
236 })
237
238 conn
239 |> put_view(AdminAPI.StatusView)
240 |> render("index.json", %{activities: activities, as: :activity})
241 end
242
243 def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do
244 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
245 godmode = params["godmode"] == "true" || params["godmode"] == true
246
247 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
248 {_, page_size} = page_params(params)
249
250 activities =
251 ActivityPub.fetch_user_activities(user, nil, %{
252 limit: page_size,
253 godmode: godmode,
254 exclude_reblogs: not with_reblogs
255 })
256
257 conn
258 |> put_view(AdminAPI.StatusView)
259 |> render("index.json", %{activities: activities, as: :activity})
260 else
261 _ -> {:error, :not_found}
262 end
263 end
264
265 def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = _params) do
266 with %User{id: user_id} <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
267 chats =
268 Pleroma.Chat.for_user_query(user_id)
269 |> Pleroma.Repo.all()
270
271 conn
272 |> put_view(AdminAPI.ChatView)
273 |> render("index.json", chats: chats)
274 else
275 _ -> {:error, :not_found}
276 end
277 end
278
279 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
280 user = User.get_cached_by_nickname(nickname)
281
282 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
283
284 action = if user.deactivated, do: "activate", else: "deactivate"
285
286 ModerationLog.insert_log(%{
287 actor: admin,
288 subject: [user],
289 action: action
290 })
291
292 conn
293 |> put_view(AccountView)
294 |> render("show.json", %{user: updated_user})
295 end
296
297 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
298 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
299 {:ok, updated_users} = User.deactivate(users, false)
300
301 ModerationLog.insert_log(%{
302 actor: admin,
303 subject: users,
304 action: "activate"
305 })
306
307 conn
308 |> put_view(AccountView)
309 |> render("index.json", %{users: Keyword.values(updated_users)})
310 end
311
312 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
313 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
314 {:ok, updated_users} = User.deactivate(users, true)
315
316 ModerationLog.insert_log(%{
317 actor: admin,
318 subject: users,
319 action: "deactivate"
320 })
321
322 conn
323 |> put_view(AccountView)
324 |> render("index.json", %{users: Keyword.values(updated_users)})
325 end
326
327 def user_approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
328 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
329 {:ok, updated_users} = User.approve(users)
330
331 ModerationLog.insert_log(%{
332 actor: admin,
333 subject: users,
334 action: "approve"
335 })
336
337 conn
338 |> put_view(AccountView)
339 |> render("index.json", %{users: updated_users})
340 end
341
342 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
343 with {:ok, _} <- User.tag(nicknames, tags) do
344 ModerationLog.insert_log(%{
345 actor: admin,
346 nicknames: nicknames,
347 tags: tags,
348 action: "tag"
349 })
350
351 json_response(conn, :no_content, "")
352 end
353 end
354
355 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
356 with {:ok, _} <- User.untag(nicknames, tags) do
357 ModerationLog.insert_log(%{
358 actor: admin,
359 nicknames: nicknames,
360 tags: tags,
361 action: "untag"
362 })
363
364 json_response(conn, :no_content, "")
365 end
366 end
367
368 def list_users(conn, params) do
369 {page, page_size} = page_params(params)
370 filters = maybe_parse_filters(params["filters"])
371
372 search_params = %{
373 query: params["query"],
374 page: page,
375 page_size: page_size,
376 tags: params["tags"],
377 name: params["name"],
378 email: params["email"]
379 }
380
381 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
382 json(
383 conn,
384 AccountView.render("index.json",
385 users: users,
386 count: count,
387 page_size: page_size
388 )
389 )
390 end
391 end
392
393 @filters ~w(local external active deactivated need_approval is_admin is_moderator)
394
395 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
396 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
397
398 defp maybe_parse_filters(filters) do
399 filters
400 |> String.split(",")
401 |> Enum.filter(&Enum.member?(@filters, &1))
402 |> Map.new(&{String.to_existing_atom(&1), true})
403 end
404
405 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
406 "permission_group" => permission_group,
407 "nicknames" => nicknames
408 })
409 when permission_group in ["moderator", "admin"] do
410 update = %{:"is_#{permission_group}" => true}
411
412 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
413
414 for u <- users, do: User.admin_api_update(u, update)
415
416 ModerationLog.insert_log(%{
417 action: "grant",
418 actor: admin,
419 subject: users,
420 permission: permission_group
421 })
422
423 json(conn, update)
424 end
425
426 def right_add_multiple(conn, _) do
427 render_error(conn, :not_found, "No such permission_group")
428 end
429
430 def right_add(%{assigns: %{user: admin}} = conn, %{
431 "permission_group" => permission_group,
432 "nickname" => nickname
433 })
434 when permission_group in ["moderator", "admin"] do
435 fields = %{:"is_#{permission_group}" => true}
436
437 {:ok, user} =
438 nickname
439 |> User.get_cached_by_nickname()
440 |> User.admin_api_update(fields)
441
442 ModerationLog.insert_log(%{
443 action: "grant",
444 actor: admin,
445 subject: [user],
446 permission: permission_group
447 })
448
449 json(conn, fields)
450 end
451
452 def right_add(conn, _) do
453 render_error(conn, :not_found, "No such permission_group")
454 end
455
456 def right_get(conn, %{"nickname" => nickname}) do
457 user = User.get_cached_by_nickname(nickname)
458
459 conn
460 |> json(%{
461 is_moderator: user.is_moderator,
462 is_admin: user.is_admin
463 })
464 end
465
466 def right_delete_multiple(
467 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
468 %{
469 "permission_group" => permission_group,
470 "nicknames" => nicknames
471 }
472 )
473 when permission_group in ["moderator", "admin"] do
474 with false <- Enum.member?(nicknames, admin_nickname) do
475 update = %{:"is_#{permission_group}" => false}
476
477 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
478
479 for u <- users, do: User.admin_api_update(u, update)
480
481 ModerationLog.insert_log(%{
482 action: "revoke",
483 actor: admin,
484 subject: users,
485 permission: permission_group
486 })
487
488 json(conn, update)
489 else
490 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
491 end
492 end
493
494 def right_delete_multiple(conn, _) do
495 render_error(conn, :not_found, "No such permission_group")
496 end
497
498 def right_delete(
499 %{assigns: %{user: admin}} = conn,
500 %{
501 "permission_group" => permission_group,
502 "nickname" => nickname
503 }
504 )
505 when permission_group in ["moderator", "admin"] do
506 fields = %{:"is_#{permission_group}" => false}
507
508 {:ok, user} =
509 nickname
510 |> User.get_cached_by_nickname()
511 |> User.admin_api_update(fields)
512
513 ModerationLog.insert_log(%{
514 action: "revoke",
515 actor: admin,
516 subject: [user],
517 permission: permission_group
518 })
519
520 json(conn, fields)
521 end
522
523 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
524 render_error(conn, :forbidden, "You can't revoke your own admin status.")
525 end
526
527 @doc "Get a password reset token (base64 string) for given nickname"
528 def get_password_reset(conn, %{"nickname" => nickname}) do
529 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
530 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
531
532 conn
533 |> json(%{
534 token: token.token,
535 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
536 })
537 end
538
539 @doc "Force password reset for a given user"
540 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
541 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
542
543 Enum.each(users, &User.force_password_reset_async/1)
544
545 ModerationLog.insert_log(%{
546 actor: admin,
547 subject: users,
548 action: "force_password_reset"
549 })
550
551 json_response(conn, :no_content, "")
552 end
553
554 @doc "Disable mfa for user's account."
555 def disable_mfa(conn, %{"nickname" => nickname}) do
556 case User.get_by_nickname(nickname) do
557 %User{} = user ->
558 MFA.disable(user)
559 json(conn, nickname)
560
561 _ ->
562 {:error, :not_found}
563 end
564 end
565
566 @doc "Show a given user's credentials"
567 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
568 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
569 conn
570 |> put_view(AccountView)
571 |> render("credentials.json", %{user: user, for: admin})
572 else
573 _ -> {:error, :not_found}
574 end
575 end
576
577 @doc "Updates a given user"
578 def update_user_credentials(
579 %{assigns: %{user: admin}} = conn,
580 %{"nickname" => nickname} = params
581 ) do
582 with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
583 {:ok, _user} <-
584 User.update_as_admin(user, params) do
585 ModerationLog.insert_log(%{
586 actor: admin,
587 subject: [user],
588 action: "updated_users"
589 })
590
591 if params["password"] do
592 User.force_password_reset_async(user)
593 end
594
595 ModerationLog.insert_log(%{
596 actor: admin,
597 subject: [user],
598 action: "force_password_reset"
599 })
600
601 json(conn, %{status: "success"})
602 else
603 {:error, changeset} ->
604 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
605
606 {:errors, errors}
607
608 _ ->
609 {:error, :not_found}
610 end
611 end
612
613 def list_log(conn, params) do
614 {page, page_size} = page_params(params)
615
616 log =
617 ModerationLog.get_all(%{
618 page: page,
619 page_size: page_size,
620 start_date: params["start_date"],
621 end_date: params["end_date"],
622 user_id: params["user_id"],
623 search: params["search"]
624 })
625
626 conn
627 |> put_view(ModerationLogView)
628 |> render("index.json", %{log: log})
629 end
630
631 def restart(conn, _params) do
632 with :ok <- configurable_from_database() do
633 Restarter.Pleroma.restart(Config.get(:env), 50)
634
635 json(conn, %{})
636 end
637 end
638
639 def need_reboot(conn, _params) do
640 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
641 end
642
643 defp configurable_from_database do
644 if Config.get(:configurable_from_database) do
645 :ok
646 else
647 {:error, "To use this endpoint you need to enable configuration from database."}
648 end
649 end
650
651 def reload_emoji(conn, _params) do
652 Pleroma.Emoji.reload()
653
654 json(conn, "ok")
655 end
656
657 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
658 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
659
660 User.toggle_confirmation(users)
661
662 ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
663
664 json(conn, "")
665 end
666
667 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
668 users =
669 Enum.map(nicknames, fn nickname ->
670 nickname
671 |> User.get_cached_by_nickname()
672 |> User.send_confirmation_email()
673 end)
674
675 ModerationLog.insert_log(%{actor: admin, subject: users, action: "resend_confirmation_email"})
676
677 json(conn, "")
678 end
679
680 def stats(conn, params) do
681 counters = Stats.get_status_visibility_count(params["instance"])
682
683 json(conn, %{"status_visibility" => counters})
684 end
685
686 def create_backup(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
687 with %User{} = user <- User.get_by_nickname(nickname),
688 {:ok, _} <- Pleroma.Backup.create(user, admin.id) do
689 ModerationLog.insert_log(%{actor: admin, subject: user, action: "create_backup"})
690
691 json(conn, "")
692 end
693 end
694
695 defp page_params(params) do
696 {get_page(params["page"]), get_page_size(params["page_size"])}
697 end
698
699 defp get_page(page_string) when is_nil(page_string), do: 1
700
701 defp get_page(page_string) do
702 case Integer.parse(page_string) do
703 {page, _} -> page
704 :error -> 1
705 end
706 end
707
708 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
709
710 defp get_page_size(page_size_string) do
711 case Integer.parse(page_size_string) do
712 {page_size, _} -> page_size
713 :error -> @users_page_size
714 end
715 end
716 end