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