1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.AdminAPI.AdminAPIController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
10 alias Pleroma.Activity
12 alias Pleroma.ConfigDB
14 alias Pleroma.ModerationLog
15 alias Pleroma.Plugs.OAuthScopesPlug
16 alias Pleroma.ReportNote
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Builder
21 alias Pleroma.Web.ActivityPub.Pipeline
22 alias Pleroma.Web.ActivityPub.Relay
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.AdminAPI
25 alias Pleroma.Web.AdminAPI.AccountView
26 alias Pleroma.Web.AdminAPI.ConfigView
27 alias Pleroma.Web.AdminAPI.ModerationLogView
28 alias Pleroma.Web.AdminAPI.Report
29 alias Pleroma.Web.AdminAPI.ReportView
30 alias Pleroma.Web.AdminAPI.Search
31 alias Pleroma.Web.CommonAPI
32 alias Pleroma.Web.Endpoint
33 alias Pleroma.Web.Router
37 @descriptions Pleroma.Docs.JSON.compile()
42 %{scopes: ["read:accounts"], admin: true}
43 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
48 %{scopes: ["write:accounts"], admin: true}
51 :force_password_reset,
54 :user_toggle_activation,
63 :right_delete_multiple,
64 :update_user_credentials
70 %{scopes: ["write:follows"], admin: true}
71 when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
76 %{scopes: ["read:reports"], admin: true}
77 when action in [:list_reports, :report_show]
82 %{scopes: ["write:reports"], admin: true}
83 when action in [:reports_update, :report_notes_create, :report_notes_delete]
88 %{scopes: ["read:statuses"], admin: true}
89 when action in [:list_user_statuses, :list_instance_statuses]
94 %{scopes: ["read"], admin: true}
100 :config_descriptions,
107 %{scopes: ["write"], admin: true}
111 :resend_confirmation_email,
117 action_fallback(AdminAPI.FallbackController)
119 def user_delete(conn, %{"nickname" => nickname}) do
120 user_delete(conn, %{"nicknames" => [nickname]})
123 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
126 |> Enum.map(&User.get_cached_by_nickname/1)
129 |> Enum.each(fn user ->
130 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
131 Pipeline.common_pipeline(delete_data, local: true)
134 ModerationLog.insert_log(%{
144 def user_follow(%{assigns: %{user: admin}} = conn, %{
145 "follower" => follower_nick,
146 "followed" => followed_nick
148 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
149 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
150 User.follow(follower, followed)
152 ModerationLog.insert_log(%{
164 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
165 "follower" => follower_nick,
166 "followed" => followed_nick
168 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
169 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
170 User.unfollow(follower, followed)
172 ModerationLog.insert_log(%{
184 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
186 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
192 password_confirmation: password,
196 User.register_changeset(%User{}, user_data, need_confirmation: false)
198 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
199 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
202 case Pleroma.Repo.transaction(changesets) do
207 |> Enum.map(fn user ->
208 {:ok, user} = User.post_register_action(user)
212 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
214 ModerationLog.insert_log(%{
216 subjects: Map.values(users),
223 {:error, id, changeset, _} ->
225 Enum.map(changesets.operations, fn
226 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
227 AccountView.render("create-error.json", %{changeset: changeset})
229 {_, {:changeset, current_changeset, _}} ->
230 AccountView.render("create-error.json", %{changeset: current_changeset})
234 |> put_status(:conflict)
239 def user_show(conn, %{"nickname" => nickname}) do
240 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
242 |> put_view(AccountView)
243 |> render("show.json", %{user: user})
245 _ -> {:error, :not_found}
249 def list_instance_statuses(conn, %{"instance" => instance} = params) do
250 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
251 {page, page_size} = page_params(params)
254 ActivityPub.fetch_statuses(nil, %{
255 "instance" => instance,
256 "limit" => page_size,
257 "offset" => (page - 1) * page_size,
258 "exclude_reblogs" => !with_reblogs && "true"
262 |> put_view(AdminAPI.StatusView)
263 |> render("index.json", %{activities: activities, as: :activity})
266 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
267 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
268 godmode = params["godmode"] == "true" || params["godmode"] == true
270 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
271 {_, page_size} = page_params(params)
274 ActivityPub.fetch_user_activities(user, nil, %{
275 "limit" => page_size,
276 "godmode" => godmode,
277 "exclude_reblogs" => !with_reblogs && "true"
281 |> put_view(AdminAPI.StatusView)
282 |> render("index.json", %{activities: activities, as: :activity})
284 _ -> {:error, :not_found}
288 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
289 user = User.get_cached_by_nickname(nickname)
291 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
293 action = if user.deactivated, do: "activate", else: "deactivate"
295 ModerationLog.insert_log(%{
302 |> put_view(AccountView)
303 |> render("show.json", %{user: updated_user})
306 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
307 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
308 {:ok, updated_users} = User.deactivate(users, false)
310 ModerationLog.insert_log(%{
317 |> put_view(AccountView)
318 |> render("index.json", %{users: Keyword.values(updated_users)})
321 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
322 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
323 {:ok, updated_users} = User.deactivate(users, true)
325 ModerationLog.insert_log(%{
332 |> put_view(AccountView)
333 |> render("index.json", %{users: Keyword.values(updated_users)})
336 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
337 with {:ok, _} <- User.tag(nicknames, tags) do
338 ModerationLog.insert_log(%{
340 nicknames: nicknames,
345 json_response(conn, :no_content, "")
349 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
350 with {:ok, _} <- User.untag(nicknames, tags) do
351 ModerationLog.insert_log(%{
353 nicknames: nicknames,
358 json_response(conn, :no_content, "")
362 def list_users(conn, params) do
363 {page, page_size} = page_params(params)
364 filters = maybe_parse_filters(params["filters"])
367 query: params["query"],
369 page_size: page_size,
370 tags: params["tags"],
371 name: params["name"],
372 email: params["email"]
375 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
378 AccountView.render("index.json", users: users, count: count, page_size: page_size)
383 @filters ~w(local external active deactivated is_admin is_moderator)
385 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
386 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
388 defp maybe_parse_filters(filters) do
391 |> Enum.filter(&Enum.member?(@filters, &1))
392 |> Enum.map(&String.to_atom(&1))
393 |> Enum.into(%{}, &{&1, true})
396 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
397 "permission_group" => permission_group,
398 "nicknames" => nicknames
400 when permission_group in ["moderator", "admin"] do
401 update = %{:"is_#{permission_group}" => true}
403 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
405 for u <- users, do: User.admin_api_update(u, update)
407 ModerationLog.insert_log(%{
411 permission: permission_group
417 def right_add_multiple(conn, _) do
418 render_error(conn, :not_found, "No such permission_group")
421 def right_add(%{assigns: %{user: admin}} = conn, %{
422 "permission_group" => permission_group,
423 "nickname" => nickname
425 when permission_group in ["moderator", "admin"] do
426 fields = %{:"is_#{permission_group}" => true}
430 |> User.get_cached_by_nickname()
431 |> User.admin_api_update(fields)
433 ModerationLog.insert_log(%{
437 permission: permission_group
443 def right_add(conn, _) do
444 render_error(conn, :not_found, "No such permission_group")
447 def right_get(conn, %{"nickname" => nickname}) do
448 user = User.get_cached_by_nickname(nickname)
452 is_moderator: user.is_moderator,
453 is_admin: user.is_admin
457 def right_delete_multiple(
458 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
460 "permission_group" => permission_group,
461 "nicknames" => nicknames
464 when permission_group in ["moderator", "admin"] do
465 with false <- Enum.member?(nicknames, admin_nickname) do
466 update = %{:"is_#{permission_group}" => false}
468 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
470 for u <- users, do: User.admin_api_update(u, update)
472 ModerationLog.insert_log(%{
476 permission: permission_group
481 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
485 def right_delete_multiple(conn, _) do
486 render_error(conn, :not_found, "No such permission_group")
490 %{assigns: %{user: admin}} = conn,
492 "permission_group" => permission_group,
493 "nickname" => nickname
496 when permission_group in ["moderator", "admin"] do
497 fields = %{:"is_#{permission_group}" => false}
501 |> User.get_cached_by_nickname()
502 |> User.admin_api_update(fields)
504 ModerationLog.insert_log(%{
508 permission: permission_group
514 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
515 render_error(conn, :forbidden, "You can't revoke your own admin status.")
518 def relay_list(conn, _params) do
519 with {:ok, list} <- Relay.list() do
520 json(conn, %{relays: list})
528 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
529 with {:ok, _message} <- Relay.follow(target) do
530 ModerationLog.insert_log(%{
531 action: "relay_follow",
545 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
546 with {:ok, _message} <- Relay.unfollow(target) do
547 ModerationLog.insert_log(%{
548 action: "relay_unfollow",
562 @doc "Get a password reset token (base64 string) for given nickname"
563 def get_password_reset(conn, %{"nickname" => nickname}) do
564 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
565 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
570 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
574 @doc "Force password reset for a given user"
575 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
576 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
578 Enum.each(users, &User.force_password_reset_async/1)
580 ModerationLog.insert_log(%{
583 action: "force_password_reset"
586 json_response(conn, :no_content, "")
589 @doc "Disable mfa for user's account."
590 def disable_mfa(conn, %{"nickname" => nickname}) do
591 case User.get_by_nickname(nickname) do
601 @doc "Show a given user's credentials"
602 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
603 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
605 |> put_view(AccountView)
606 |> render("credentials.json", %{user: user, for: admin})
608 _ -> {:error, :not_found}
612 @doc "Updates a given user"
613 def update_user_credentials(
614 %{assigns: %{user: admin}} = conn,
615 %{"nickname" => nickname} = params
617 with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
619 User.update_as_admin(user, params) do
620 ModerationLog.insert_log(%{
623 action: "updated_users"
626 if params["password"] do
627 User.force_password_reset_async(user)
630 ModerationLog.insert_log(%{
633 action: "force_password_reset"
636 json(conn, %{status: "success"})
638 {:error, changeset} ->
639 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
641 json(conn, %{errors: errors})
644 json(conn, %{error: "Unable to update user."})
648 def list_reports(conn, params) do
649 {page, page_size} = page_params(params)
651 reports = Utils.get_reports(params, page, page_size)
654 |> put_view(ReportView)
655 |> render("index.json", %{reports: reports})
658 def report_show(conn, %{"id" => id}) do
659 with %Activity{} = report <- Activity.get_by_id(id) do
661 |> put_view(ReportView)
662 |> render("show.json", Report.extract_report_info(report))
664 _ -> {:error, :not_found}
668 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
671 |> Enum.map(fn report ->
672 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
673 ModerationLog.insert_log(%{
674 action: "report_update",
681 {:error, message} -> %{id: report["id"], error: message}
685 case Enum.any?(result, &Map.has_key?(&1, :error)) do
686 true -> json_response(conn, :bad_request, result)
687 false -> json_response(conn, :no_content, "")
691 def report_notes_create(%{assigns: %{user: user}} = conn, %{
695 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
696 ModerationLog.insert_log(%{
697 action: "report_note",
699 subject: Activity.get_by_id(report_id),
703 json_response(conn, :no_content, "")
705 _ -> json_response(conn, :bad_request, "")
709 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
711 "report_id" => report_id
713 with {:ok, note} <- ReportNote.destroy(note_id) do
714 ModerationLog.insert_log(%{
715 action: "report_note_delete",
717 subject: Activity.get_by_id(report_id),
721 json_response(conn, :no_content, "")
723 _ -> json_response(conn, :bad_request, "")
727 def list_log(conn, params) do
728 {page, page_size} = page_params(params)
731 ModerationLog.get_all(%{
733 page_size: page_size,
734 start_date: params["start_date"],
735 end_date: params["end_date"],
736 user_id: params["user_id"],
737 search: params["search"]
741 |> put_view(ModerationLogView)
742 |> render("index.json", %{log: log})
745 def config_descriptions(conn, _params) do
746 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
748 json(conn, descriptions)
751 def config_show(conn, %{"only_db" => true}) do
752 with :ok <- configurable_from_database() do
753 configs = Pleroma.Repo.all(ConfigDB)
756 |> put_view(ConfigView)
757 |> render("index.json", %{configs: configs})
761 def config_show(conn, _params) do
762 with :ok <- configurable_from_database() do
763 configs = ConfigDB.get_all_as_keyword()
766 Config.Holder.default_config()
767 |> ConfigDB.merge(configs)
768 |> Enum.map(fn {group, values} ->
769 Enum.map(values, fn {key, value} ->
771 if configs[group][key] do
772 ConfigDB.get_db_keys(configs[group][key], key)
775 db_value = configs[group][key]
778 if !is_nil(db_value) and Keyword.keyword?(db_value) and
779 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
780 ConfigDB.merge_group(group, key, value, db_value)
786 group: ConfigDB.convert(group),
787 key: ConfigDB.convert(key),
788 value: ConfigDB.convert(merged_value)
791 if db, do: Map.put(setting, :db, db), else: setting
796 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
800 def config_update(conn, %{"configs" => configs}) do
801 with :ok <- configurable_from_database() do
804 |> Enum.filter(&whitelisted_config?/1)
806 %{"group" => group, "key" => key, "delete" => true} = params ->
807 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
809 %{"group" => group, "key" => key, "value" => value} ->
810 ConfigDB.update_or_create(%{group: group, key: key, value: value})
812 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
816 |> Enum.map(fn {:ok, config} ->
817 Map.put(config, :db, ConfigDB.get_db_keys(config))
819 |> Enum.split_with(fn config ->
820 Ecto.get_meta(config, :state) == :deleted
823 Config.TransferTask.load_and_update_env(deleted, false)
825 if !Restarter.Pleroma.need_reboot?() do
826 changed_reboot_settings? =
828 |> Enum.any?(fn config ->
829 group = ConfigDB.from_string(config.group)
830 key = ConfigDB.from_string(config.key)
831 value = ConfigDB.from_binary(config.value)
832 Config.TransferTask.pleroma_need_restart?(group, key, value)
835 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
839 |> put_view(ConfigView)
840 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
844 def restart(conn, _params) do
845 with :ok <- configurable_from_database() do
846 Restarter.Pleroma.restart(Config.get(:env), 50)
852 def need_reboot(conn, _params) do
853 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
856 defp configurable_from_database do
857 if Config.get(:configurable_from_database) do
860 {:error, "To use this endpoint you need to enable configuration from database."}
864 defp whitelisted_config?(group, key) do
865 if whitelisted_configs = Config.get(:database_config_whitelist) do
866 Enum.any?(whitelisted_configs, fn
867 {whitelisted_group} ->
868 group == inspect(whitelisted_group)
870 {whitelisted_group, whitelisted_key} ->
871 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
878 defp whitelisted_config?(%{"group" => group, "key" => key}) do
879 whitelisted_config?(group, key)
882 defp whitelisted_config?(%{:group => group} = config) do
883 whitelisted_config?(group, config[:key])
886 def reload_emoji(conn, _params) do
887 Pleroma.Emoji.reload()
892 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
893 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
895 User.toggle_confirmation(users)
897 ModerationLog.insert_log(%{
900 action: "confirm_email"
906 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
907 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
909 User.try_send_confirmation_email(users)
911 ModerationLog.insert_log(%{
914 action: "resend_confirmation_email"
920 def stats(conn, _) do
921 count = Stats.get_status_visibility_count()
924 |> json(%{"status_visibility" => count})
927 defp page_params(params) do
928 {get_page(params["page"]), get_page_size(params["page_size"])}
931 defp get_page(page_string) when is_nil(page_string), do: 1
933 defp get_page(page_string) do
934 case Integer.parse(page_string) do
940 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
942 defp get_page_size(page_size_string) do
943 case Integer.parse(page_size_string) do
944 {page_size, _} -> page_size
945 :error -> @users_page_size