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.MastodonAPI
34 alias Pleroma.Web.Router
38 @descriptions Pleroma.Docs.JSON.compile()
43 %{scopes: ["read:accounts"], admin: true}
44 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
49 %{scopes: ["write:accounts"], admin: true}
52 :force_password_reset,
55 :user_toggle_activation,
64 :right_delete_multiple,
65 :update_user_credentials
71 %{scopes: ["write:follows"], admin: true}
72 when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
77 %{scopes: ["read:reports"], admin: true}
78 when action in [:list_reports, :report_show]
83 %{scopes: ["write:reports"], admin: true}
84 when action in [:reports_update, :report_notes_create, :report_notes_delete]
89 %{scopes: ["read:statuses"], admin: true}
90 when action in [:list_user_statuses, :list_instance_statuses]
95 %{scopes: ["read"], admin: true}
101 :config_descriptions,
108 %{scopes: ["write"], admin: true}
112 :resend_confirmation_email,
118 action_fallback(AdminAPI.FallbackController)
120 def user_delete(conn, %{"nickname" => nickname}) do
121 user_delete(conn, %{"nicknames" => [nickname]})
124 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
127 |> Enum.map(&User.get_cached_by_nickname/1)
130 |> Enum.each(fn user ->
131 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
132 Pipeline.common_pipeline(delete_data, local: true)
135 ModerationLog.insert_log(%{
145 def user_follow(%{assigns: %{user: admin}} = conn, %{
146 "follower" => follower_nick,
147 "followed" => followed_nick
149 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
150 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
151 User.follow(follower, followed)
153 ModerationLog.insert_log(%{
165 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
166 "follower" => follower_nick,
167 "followed" => followed_nick
169 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
170 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
171 User.unfollow(follower, followed)
173 ModerationLog.insert_log(%{
185 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
187 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
193 password_confirmation: password,
197 User.register_changeset(%User{}, user_data, need_confirmation: false)
199 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
200 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
203 case Pleroma.Repo.transaction(changesets) do
208 |> Enum.map(fn user ->
209 {:ok, user} = User.post_register_action(user)
213 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
215 ModerationLog.insert_log(%{
217 subjects: Map.values(users),
224 {:error, id, changeset, _} ->
226 Enum.map(changesets.operations, fn
227 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
228 AccountView.render("create-error.json", %{changeset: changeset})
230 {_, {:changeset, current_changeset, _}} ->
231 AccountView.render("create-error.json", %{changeset: current_changeset})
235 |> put_status(:conflict)
240 def user_show(conn, %{"nickname" => nickname}) do
241 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
243 |> put_view(AccountView)
244 |> render("show.json", %{user: user})
246 _ -> {:error, :not_found}
250 def list_instance_statuses(conn, %{"instance" => instance} = params) do
251 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
252 {page, page_size} = page_params(params)
255 ActivityPub.fetch_statuses(nil, %{
256 "instance" => instance,
257 "limit" => page_size,
258 "offset" => (page - 1) * page_size,
259 "exclude_reblogs" => !with_reblogs && "true"
263 |> put_view(AdminAPI.StatusView)
264 |> render("index.json", %{activities: activities, as: :activity})
267 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
268 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
269 godmode = params["godmode"] == "true" || params["godmode"] == true
271 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
272 {_, page_size} = page_params(params)
275 ActivityPub.fetch_user_activities(user, nil, %{
276 "limit" => page_size,
277 "godmode" => godmode,
278 "exclude_reblogs" => !with_reblogs && "true"
282 |> put_view(MastodonAPI.StatusView)
283 |> render("index.json", %{activities: activities, as: :activity})
285 _ -> {:error, :not_found}
289 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
290 user = User.get_cached_by_nickname(nickname)
292 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
294 action = if user.deactivated, do: "activate", else: "deactivate"
296 ModerationLog.insert_log(%{
303 |> put_view(AccountView)
304 |> render("show.json", %{user: updated_user})
307 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
308 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
309 {:ok, updated_users} = User.deactivate(users, false)
311 ModerationLog.insert_log(%{
318 |> put_view(AccountView)
319 |> render("index.json", %{users: Keyword.values(updated_users)})
322 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
323 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
324 {:ok, updated_users} = User.deactivate(users, true)
326 ModerationLog.insert_log(%{
333 |> put_view(AccountView)
334 |> render("index.json", %{users: Keyword.values(updated_users)})
337 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
338 with {:ok, _} <- User.tag(nicknames, tags) do
339 ModerationLog.insert_log(%{
341 nicknames: nicknames,
346 json_response(conn, :no_content, "")
350 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
351 with {:ok, _} <- User.untag(nicknames, tags) do
352 ModerationLog.insert_log(%{
354 nicknames: nicknames,
359 json_response(conn, :no_content, "")
363 def list_users(conn, params) do
364 {page, page_size} = page_params(params)
365 filters = maybe_parse_filters(params["filters"])
368 query: params["query"],
370 page_size: page_size,
371 tags: params["tags"],
372 name: params["name"],
373 email: params["email"]
376 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
379 AccountView.render("index.json", users: users, count: count, page_size: page_size)
384 @filters ~w(local external active deactivated is_admin is_moderator)
386 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
387 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
389 defp maybe_parse_filters(filters) do
392 |> Enum.filter(&Enum.member?(@filters, &1))
393 |> Enum.map(&String.to_atom(&1))
394 |> Enum.into(%{}, &{&1, true})
397 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
398 "permission_group" => permission_group,
399 "nicknames" => nicknames
401 when permission_group in ["moderator", "admin"] do
402 update = %{:"is_#{permission_group}" => true}
404 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
406 for u <- users, do: User.admin_api_update(u, update)
408 ModerationLog.insert_log(%{
412 permission: permission_group
418 def right_add_multiple(conn, _) do
419 render_error(conn, :not_found, "No such permission_group")
422 def right_add(%{assigns: %{user: admin}} = conn, %{
423 "permission_group" => permission_group,
424 "nickname" => nickname
426 when permission_group in ["moderator", "admin"] do
427 fields = %{:"is_#{permission_group}" => true}
431 |> User.get_cached_by_nickname()
432 |> User.admin_api_update(fields)
434 ModerationLog.insert_log(%{
438 permission: permission_group
444 def right_add(conn, _) do
445 render_error(conn, :not_found, "No such permission_group")
448 def right_get(conn, %{"nickname" => nickname}) do
449 user = User.get_cached_by_nickname(nickname)
453 is_moderator: user.is_moderator,
454 is_admin: user.is_admin
458 def right_delete_multiple(
459 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
461 "permission_group" => permission_group,
462 "nicknames" => nicknames
465 when permission_group in ["moderator", "admin"] do
466 with false <- Enum.member?(nicknames, admin_nickname) do
467 update = %{:"is_#{permission_group}" => false}
469 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
471 for u <- users, do: User.admin_api_update(u, update)
473 ModerationLog.insert_log(%{
477 permission: permission_group
482 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
486 def right_delete_multiple(conn, _) do
487 render_error(conn, :not_found, "No such permission_group")
491 %{assigns: %{user: admin}} = conn,
493 "permission_group" => permission_group,
494 "nickname" => nickname
497 when permission_group in ["moderator", "admin"] do
498 fields = %{:"is_#{permission_group}" => false}
502 |> User.get_cached_by_nickname()
503 |> User.admin_api_update(fields)
505 ModerationLog.insert_log(%{
509 permission: permission_group
515 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
516 render_error(conn, :forbidden, "You can't revoke your own admin status.")
519 def relay_list(conn, _params) do
520 with {:ok, list} <- Relay.list() do
521 json(conn, %{relays: list})
529 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
530 with {:ok, _message} <- Relay.follow(target) do
531 ModerationLog.insert_log(%{
532 action: "relay_follow",
546 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
547 with {:ok, _message} <- Relay.unfollow(target) do
548 ModerationLog.insert_log(%{
549 action: "relay_unfollow",
563 @doc "Get a password reset token (base64 string) for given nickname"
564 def get_password_reset(conn, %{"nickname" => nickname}) do
565 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
566 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
571 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
575 @doc "Force password reset for a given user"
576 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
577 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
579 Enum.each(users, &User.force_password_reset_async/1)
581 ModerationLog.insert_log(%{
584 action: "force_password_reset"
587 json_response(conn, :no_content, "")
590 @doc "Disable mfa for user's account."
591 def disable_mfa(conn, %{"nickname" => nickname}) do
592 case User.get_by_nickname(nickname) do
602 @doc "Show a given user's credentials"
603 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
604 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
606 |> put_view(AccountView)
607 |> render("credentials.json", %{user: user, for: admin})
609 _ -> {:error, :not_found}
613 @doc "Updates a given user"
614 def update_user_credentials(
615 %{assigns: %{user: admin}} = conn,
616 %{"nickname" => nickname} = params
618 with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
620 User.update_as_admin(user, params) do
621 ModerationLog.insert_log(%{
624 action: "updated_users"
627 if params["password"] do
628 User.force_password_reset_async(user)
631 ModerationLog.insert_log(%{
634 action: "force_password_reset"
637 json(conn, %{status: "success"})
639 {:error, changeset} ->
640 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
642 json(conn, %{errors: errors})
645 json(conn, %{error: "Unable to update user."})
649 def list_reports(conn, params) do
650 {page, page_size} = page_params(params)
652 reports = Utils.get_reports(params, page, page_size)
655 |> put_view(ReportView)
656 |> render("index.json", %{reports: reports})
659 def report_show(conn, %{"id" => id}) do
660 with %Activity{} = report <- Activity.get_by_id(id) do
662 |> put_view(ReportView)
663 |> render("show.json", Report.extract_report_info(report))
665 _ -> {:error, :not_found}
669 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
672 |> Enum.map(fn report ->
673 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
674 ModerationLog.insert_log(%{
675 action: "report_update",
682 {:error, message} -> %{id: report["id"], error: message}
686 case Enum.any?(result, &Map.has_key?(&1, :error)) do
687 true -> json_response(conn, :bad_request, result)
688 false -> json_response(conn, :no_content, "")
692 def report_notes_create(%{assigns: %{user: user}} = conn, %{
696 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
697 ModerationLog.insert_log(%{
698 action: "report_note",
700 subject: Activity.get_by_id(report_id),
704 json_response(conn, :no_content, "")
706 _ -> json_response(conn, :bad_request, "")
710 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
712 "report_id" => report_id
714 with {:ok, note} <- ReportNote.destroy(note_id) do
715 ModerationLog.insert_log(%{
716 action: "report_note_delete",
718 subject: Activity.get_by_id(report_id),
722 json_response(conn, :no_content, "")
724 _ -> json_response(conn, :bad_request, "")
728 def list_log(conn, params) do
729 {page, page_size} = page_params(params)
732 ModerationLog.get_all(%{
734 page_size: page_size,
735 start_date: params["start_date"],
736 end_date: params["end_date"],
737 user_id: params["user_id"],
738 search: params["search"]
742 |> put_view(ModerationLogView)
743 |> render("index.json", %{log: log})
746 def config_descriptions(conn, _params) do
747 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
749 json(conn, descriptions)
752 def config_show(conn, %{"only_db" => true}) do
753 with :ok <- configurable_from_database() do
754 configs = Pleroma.Repo.all(ConfigDB)
757 |> put_view(ConfigView)
758 |> render("index.json", %{configs: configs})
762 def config_show(conn, _params) do
763 with :ok <- configurable_from_database() do
764 configs = ConfigDB.get_all_as_keyword()
767 Config.Holder.default_config()
768 |> ConfigDB.merge(configs)
769 |> Enum.map(fn {group, values} ->
770 Enum.map(values, fn {key, value} ->
772 if configs[group][key] do
773 ConfigDB.get_db_keys(configs[group][key], key)
776 db_value = configs[group][key]
779 if !is_nil(db_value) and Keyword.keyword?(db_value) and
780 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
781 ConfigDB.merge_group(group, key, value, db_value)
787 group: ConfigDB.convert(group),
788 key: ConfigDB.convert(key),
789 value: ConfigDB.convert(merged_value)
792 if db, do: Map.put(setting, :db, db), else: setting
797 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
801 def config_update(conn, %{"configs" => configs}) do
802 with :ok <- configurable_from_database() do
805 |> Enum.filter(&whitelisted_config?/1)
807 %{"group" => group, "key" => key, "delete" => true} = params ->
808 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
810 %{"group" => group, "key" => key, "value" => value} ->
811 ConfigDB.update_or_create(%{group: group, key: key, value: value})
813 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
817 |> Enum.map(fn {:ok, config} ->
818 Map.put(config, :db, ConfigDB.get_db_keys(config))
820 |> Enum.split_with(fn config ->
821 Ecto.get_meta(config, :state) == :deleted
824 Config.TransferTask.load_and_update_env(deleted, false)
826 if !Restarter.Pleroma.need_reboot?() do
827 changed_reboot_settings? =
829 |> Enum.any?(fn config ->
830 group = ConfigDB.from_string(config.group)
831 key = ConfigDB.from_string(config.key)
832 value = ConfigDB.from_binary(config.value)
833 Config.TransferTask.pleroma_need_restart?(group, key, value)
836 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
840 |> put_view(ConfigView)
841 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
845 def restart(conn, _params) do
846 with :ok <- configurable_from_database() do
847 Restarter.Pleroma.restart(Config.get(:env), 50)
853 def need_reboot(conn, _params) do
854 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
857 defp configurable_from_database do
858 if Config.get(:configurable_from_database) do
861 {:error, "To use this endpoint you need to enable configuration from database."}
865 defp whitelisted_config?(group, key) do
866 if whitelisted_configs = Config.get(:database_config_whitelist) do
867 Enum.any?(whitelisted_configs, fn
868 {whitelisted_group} ->
869 group == inspect(whitelisted_group)
871 {whitelisted_group, whitelisted_key} ->
872 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
879 defp whitelisted_config?(%{"group" => group, "key" => key}) do
880 whitelisted_config?(group, key)
883 defp whitelisted_config?(%{:group => group} = config) do
884 whitelisted_config?(group, config[:key])
887 def reload_emoji(conn, _params) do
888 Pleroma.Emoji.reload()
893 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
894 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
896 User.toggle_confirmation(users)
898 ModerationLog.insert_log(%{
901 action: "confirm_email"
907 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
908 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
910 User.try_send_confirmation_email(users)
912 ModerationLog.insert_log(%{
915 action: "resend_confirmation_email"
921 def stats(conn, _) do
922 count = Stats.get_status_visibility_count()
925 |> json(%{"status_visibility" => count})
928 defp page_params(params) do
929 {get_page(params["page"]), get_page_size(params["page_size"])}
932 defp get_page(page_string) when is_nil(page_string), do: 1
934 defp get_page(page_string) do
935 case Integer.parse(page_string) do
941 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
943 defp get_page_size(page_size_string) do
944 case Integer.parse(page_size_string) do
945 {page_size, _} -> page_size
946 :error -> @users_page_size