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