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.UserInviteToken
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Builder
22 alias Pleroma.Web.ActivityPub.Pipeline
23 alias Pleroma.Web.ActivityPub.Relay
24 alias Pleroma.Web.ActivityPub.Utils
25 alias Pleroma.Web.AdminAPI
26 alias Pleroma.Web.AdminAPI.AccountView
27 alias Pleroma.Web.AdminAPI.ConfigView
28 alias Pleroma.Web.AdminAPI.ModerationLogView
29 alias Pleroma.Web.AdminAPI.Report
30 alias Pleroma.Web.AdminAPI.ReportView
31 alias Pleroma.Web.AdminAPI.Search
32 alias Pleroma.Web.CommonAPI
33 alias Pleroma.Web.Endpoint
34 alias Pleroma.Web.MastodonAPI
35 alias Pleroma.Web.Router
39 @descriptions Pleroma.Docs.JSON.compile()
44 %{scopes: ["read:accounts"], admin: true}
45 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
50 %{scopes: ["write:accounts"], admin: true}
53 :force_password_reset,
56 :user_toggle_activation,
65 :right_delete_multiple,
66 :update_user_credentials
70 plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
74 %{scopes: ["write:invites"], admin: true}
75 when action in [:create_invite_token, :revoke_invite, :email_invite]
80 %{scopes: ["write:follows"], admin: true}
81 when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
86 %{scopes: ["read:reports"], admin: true}
87 when action in [:list_reports, :report_show]
92 %{scopes: ["write:reports"], admin: true}
93 when action in [:reports_update, :report_notes_create, :report_notes_delete]
98 %{scopes: ["read:statuses"], admin: true}
99 when action in [:list_user_statuses, :list_instance_statuses]
104 %{scopes: ["read"], admin: true}
110 :config_descriptions,
117 %{scopes: ["write"], admin: true}
121 :resend_confirmation_email,
127 action_fallback(AdminAPI.FallbackController)
129 def user_delete(conn, %{"nickname" => nickname}) do
130 user_delete(conn, %{"nicknames" => [nickname]})
133 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
136 |> Enum.map(&User.get_cached_by_nickname/1)
139 |> Enum.each(fn user ->
140 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
141 Pipeline.common_pipeline(delete_data, local: true)
144 ModerationLog.insert_log(%{
154 def user_follow(%{assigns: %{user: admin}} = conn, %{
155 "follower" => follower_nick,
156 "followed" => followed_nick
158 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
159 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
160 User.follow(follower, followed)
162 ModerationLog.insert_log(%{
174 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
175 "follower" => follower_nick,
176 "followed" => followed_nick
178 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
179 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
180 User.unfollow(follower, followed)
182 ModerationLog.insert_log(%{
194 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
196 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
202 password_confirmation: password,
206 User.register_changeset(%User{}, user_data, need_confirmation: false)
208 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
209 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
212 case Pleroma.Repo.transaction(changesets) do
217 |> Enum.map(fn user ->
218 {:ok, user} = User.post_register_action(user)
222 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
224 ModerationLog.insert_log(%{
226 subjects: Map.values(users),
233 {:error, id, changeset, _} ->
235 Enum.map(changesets.operations, fn
236 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
237 AccountView.render("create-error.json", %{changeset: changeset})
239 {_, {:changeset, current_changeset, _}} ->
240 AccountView.render("create-error.json", %{changeset: current_changeset})
244 |> put_status(:conflict)
249 def user_show(conn, %{"nickname" => nickname}) do
250 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
252 |> put_view(AccountView)
253 |> render("show.json", %{user: user})
255 _ -> {:error, :not_found}
259 def list_instance_statuses(conn, %{"instance" => instance} = params) do
260 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
261 {page, page_size} = page_params(params)
264 ActivityPub.fetch_statuses(nil, %{
265 "instance" => instance,
266 "limit" => page_size,
267 "offset" => (page - 1) * page_size,
268 "exclude_reblogs" => !with_reblogs && "true"
272 |> put_view(AdminAPI.StatusView)
273 |> render("index.json", %{activities: activities, as: :activity})
276 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
277 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
278 godmode = params["godmode"] == "true" || params["godmode"] == true
280 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
281 {_, page_size} = page_params(params)
284 ActivityPub.fetch_user_activities(user, nil, %{
285 "limit" => page_size,
286 "godmode" => godmode,
287 "exclude_reblogs" => !with_reblogs && "true"
291 |> put_view(MastodonAPI.StatusView)
292 |> render("index.json", %{activities: activities, as: :activity})
294 _ -> {:error, :not_found}
298 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
299 user = User.get_cached_by_nickname(nickname)
301 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
303 action = if user.deactivated, do: "activate", else: "deactivate"
305 ModerationLog.insert_log(%{
312 |> put_view(AccountView)
313 |> render("show.json", %{user: updated_user})
316 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
317 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
318 {:ok, updated_users} = User.deactivate(users, false)
320 ModerationLog.insert_log(%{
327 |> put_view(AccountView)
328 |> render("index.json", %{users: Keyword.values(updated_users)})
331 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
332 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
333 {:ok, updated_users} = User.deactivate(users, true)
335 ModerationLog.insert_log(%{
342 |> put_view(AccountView)
343 |> render("index.json", %{users: Keyword.values(updated_users)})
346 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
347 with {:ok, _} <- User.tag(nicknames, tags) do
348 ModerationLog.insert_log(%{
350 nicknames: nicknames,
355 json_response(conn, :no_content, "")
359 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
360 with {:ok, _} <- User.untag(nicknames, tags) do
361 ModerationLog.insert_log(%{
363 nicknames: nicknames,
368 json_response(conn, :no_content, "")
372 def list_users(conn, params) do
373 {page, page_size} = page_params(params)
374 filters = maybe_parse_filters(params["filters"])
377 query: params["query"],
379 page_size: page_size,
380 tags: params["tags"],
381 name: params["name"],
382 email: params["email"]
385 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
388 AccountView.render("index.json", users: users, count: count, page_size: page_size)
393 @filters ~w(local external active deactivated is_admin is_moderator)
395 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
396 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
398 defp maybe_parse_filters(filters) do
401 |> Enum.filter(&Enum.member?(@filters, &1))
402 |> Enum.map(&String.to_atom(&1))
403 |> Enum.into(%{}, &{&1, true})
406 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
407 "permission_group" => permission_group,
408 "nicknames" => nicknames
410 when permission_group in ["moderator", "admin"] do
411 update = %{:"is_#{permission_group}" => true}
413 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
415 for u <- users, do: User.admin_api_update(u, update)
417 ModerationLog.insert_log(%{
421 permission: permission_group
427 def right_add_multiple(conn, _) do
428 render_error(conn, :not_found, "No such permission_group")
431 def right_add(%{assigns: %{user: admin}} = conn, %{
432 "permission_group" => permission_group,
433 "nickname" => nickname
435 when permission_group in ["moderator", "admin"] do
436 fields = %{:"is_#{permission_group}" => true}
440 |> User.get_cached_by_nickname()
441 |> User.admin_api_update(fields)
443 ModerationLog.insert_log(%{
447 permission: permission_group
453 def right_add(conn, _) do
454 render_error(conn, :not_found, "No such permission_group")
457 def right_get(conn, %{"nickname" => nickname}) do
458 user = User.get_cached_by_nickname(nickname)
462 is_moderator: user.is_moderator,
463 is_admin: user.is_admin
467 def right_delete_multiple(
468 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
470 "permission_group" => permission_group,
471 "nicknames" => nicknames
474 when permission_group in ["moderator", "admin"] do
475 with false <- Enum.member?(nicknames, admin_nickname) do
476 update = %{:"is_#{permission_group}" => false}
478 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
480 for u <- users, do: User.admin_api_update(u, update)
482 ModerationLog.insert_log(%{
486 permission: permission_group
491 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
495 def right_delete_multiple(conn, _) do
496 render_error(conn, :not_found, "No such permission_group")
500 %{assigns: %{user: admin}} = conn,
502 "permission_group" => permission_group,
503 "nickname" => nickname
506 when permission_group in ["moderator", "admin"] do
507 fields = %{:"is_#{permission_group}" => false}
511 |> User.get_cached_by_nickname()
512 |> User.admin_api_update(fields)
514 ModerationLog.insert_log(%{
518 permission: permission_group
524 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
525 render_error(conn, :forbidden, "You can't revoke your own admin status.")
528 def relay_list(conn, _params) do
529 with {:ok, list} <- Relay.list() do
530 json(conn, %{relays: list})
538 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
539 with {:ok, _message} <- Relay.follow(target) do
540 ModerationLog.insert_log(%{
541 action: "relay_follow",
555 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
556 with {:ok, _message} <- Relay.unfollow(target) do
557 ModerationLog.insert_log(%{
558 action: "relay_unfollow",
572 @doc "Sends registration invite via email"
573 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
574 with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
575 {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
576 {:ok, invite_token} <- UserInviteToken.create_invite(),
578 Pleroma.Emails.UserEmail.user_invitation_email(
584 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
585 json_response(conn, :no_content, "")
587 {:registrations_open, _} ->
588 {:error, "To send invites you need to set the `registrations_open` option to false."}
590 {:invites_enabled, _} ->
591 {:error, "To send invites you need to set the `invites_enabled` option to true."}
595 @doc "Create an account registration invite token"
596 def create_invite_token(conn, params) do
600 if params["max_use"],
601 do: Map.put(opts, :max_use, params["max_use"]),
605 if params["expires_at"],
606 do: Map.put(opts, :expires_at, params["expires_at"]),
609 {:ok, invite} = UserInviteToken.create_invite(opts)
611 json(conn, AccountView.render("invite.json", %{invite: invite}))
614 @doc "Get list of created invites"
615 def invites(conn, _params) do
616 invites = UserInviteToken.list_invites()
619 |> put_view(AccountView)
620 |> render("invites.json", %{invites: invites})
623 @doc "Revokes invite by token"
624 def revoke_invite(conn, %{"token" => token}) do
625 with {:ok, invite} <- UserInviteToken.find_by_token(token),
626 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
628 |> put_view(AccountView)
629 |> render("invite.json", %{invite: updated_invite})
631 nil -> {:error, :not_found}
635 @doc "Get a password reset token (base64 string) for given nickname"
636 def get_password_reset(conn, %{"nickname" => nickname}) do
637 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
638 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
643 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
647 @doc "Force password reset for a given user"
648 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
649 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
651 Enum.each(users, &User.force_password_reset_async/1)
653 ModerationLog.insert_log(%{
656 action: "force_password_reset"
659 json_response(conn, :no_content, "")
662 @doc "Disable mfa for user's account."
663 def disable_mfa(conn, %{"nickname" => nickname}) do
664 case User.get_by_nickname(nickname) do
674 @doc "Show a given user's credentials"
675 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
676 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
678 |> put_view(AccountView)
679 |> render("credentials.json", %{user: user, for: admin})
681 _ -> {:error, :not_found}
685 @doc "Updates a given user"
686 def update_user_credentials(
687 %{assigns: %{user: admin}} = conn,
688 %{"nickname" => nickname} = params
690 with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
692 User.update_as_admin(user, params) do
693 ModerationLog.insert_log(%{
696 action: "updated_users"
699 if params["password"] do
700 User.force_password_reset_async(user)
703 ModerationLog.insert_log(%{
706 action: "force_password_reset"
709 json(conn, %{status: "success"})
711 {:error, changeset} ->
712 {_, {error, _}} = Enum.at(changeset.errors, 0)
713 json(conn, %{error: "New password #{error}."})
716 json(conn, %{error: "Unable to change password."})
720 def list_reports(conn, params) do
721 {page, page_size} = page_params(params)
723 reports = Utils.get_reports(params, page, page_size)
726 |> put_view(ReportView)
727 |> render("index.json", %{reports: reports})
730 def report_show(conn, %{"id" => id}) do
731 with %Activity{} = report <- Activity.get_by_id(id) do
733 |> put_view(ReportView)
734 |> render("show.json", Report.extract_report_info(report))
736 _ -> {:error, :not_found}
740 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
743 |> Enum.map(fn report ->
744 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
745 ModerationLog.insert_log(%{
746 action: "report_update",
753 {:error, message} -> %{id: report["id"], error: message}
757 case Enum.any?(result, &Map.has_key?(&1, :error)) do
758 true -> json_response(conn, :bad_request, result)
759 false -> json_response(conn, :no_content, "")
763 def report_notes_create(%{assigns: %{user: user}} = conn, %{
767 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
768 ModerationLog.insert_log(%{
769 action: "report_note",
771 subject: Activity.get_by_id(report_id),
775 json_response(conn, :no_content, "")
777 _ -> json_response(conn, :bad_request, "")
781 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
783 "report_id" => report_id
785 with {:ok, note} <- ReportNote.destroy(note_id) do
786 ModerationLog.insert_log(%{
787 action: "report_note_delete",
789 subject: Activity.get_by_id(report_id),
793 json_response(conn, :no_content, "")
795 _ -> json_response(conn, :bad_request, "")
799 def list_log(conn, params) do
800 {page, page_size} = page_params(params)
803 ModerationLog.get_all(%{
805 page_size: page_size,
806 start_date: params["start_date"],
807 end_date: params["end_date"],
808 user_id: params["user_id"],
809 search: params["search"]
813 |> put_view(ModerationLogView)
814 |> render("index.json", %{log: log})
817 def config_descriptions(conn, _params) do
818 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
820 json(conn, descriptions)
823 def config_show(conn, %{"only_db" => true}) do
824 with :ok <- configurable_from_database() do
825 configs = Pleroma.Repo.all(ConfigDB)
828 |> put_view(ConfigView)
829 |> render("index.json", %{configs: configs})
833 def config_show(conn, _params) do
834 with :ok <- configurable_from_database() do
835 configs = ConfigDB.get_all_as_keyword()
838 Config.Holder.default_config()
839 |> ConfigDB.merge(configs)
840 |> Enum.map(fn {group, values} ->
841 Enum.map(values, fn {key, value} ->
843 if configs[group][key] do
844 ConfigDB.get_db_keys(configs[group][key], key)
847 db_value = configs[group][key]
850 if !is_nil(db_value) and Keyword.keyword?(db_value) and
851 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
852 ConfigDB.merge_group(group, key, value, db_value)
858 group: ConfigDB.convert(group),
859 key: ConfigDB.convert(key),
860 value: ConfigDB.convert(merged_value)
863 if db, do: Map.put(setting, :db, db), else: setting
868 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
872 def config_update(conn, %{"configs" => configs}) do
873 with :ok <- configurable_from_database() do
876 |> Enum.filter(&whitelisted_config?/1)
878 %{"group" => group, "key" => key, "delete" => true} = params ->
879 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
881 %{"group" => group, "key" => key, "value" => value} ->
882 ConfigDB.update_or_create(%{group: group, key: key, value: value})
884 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
888 |> Enum.map(fn {:ok, config} ->
889 Map.put(config, :db, ConfigDB.get_db_keys(config))
891 |> Enum.split_with(fn config ->
892 Ecto.get_meta(config, :state) == :deleted
895 Config.TransferTask.load_and_update_env(deleted, false)
897 if !Restarter.Pleroma.need_reboot?() do
898 changed_reboot_settings? =
900 |> Enum.any?(fn config ->
901 group = ConfigDB.from_string(config.group)
902 key = ConfigDB.from_string(config.key)
903 value = ConfigDB.from_binary(config.value)
904 Config.TransferTask.pleroma_need_restart?(group, key, value)
907 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
911 |> put_view(ConfigView)
912 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
916 def restart(conn, _params) do
917 with :ok <- configurable_from_database() do
918 Restarter.Pleroma.restart(Config.get(:env), 50)
924 def need_reboot(conn, _params) do
925 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
928 defp configurable_from_database do
929 if Config.get(:configurable_from_database) do
932 {:error, "To use this endpoint you need to enable configuration from database."}
936 defp whitelisted_config?(group, key) do
937 if whitelisted_configs = Config.get(:database_config_whitelist) do
938 Enum.any?(whitelisted_configs, fn
939 {whitelisted_group} ->
940 group == inspect(whitelisted_group)
942 {whitelisted_group, whitelisted_key} ->
943 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
950 defp whitelisted_config?(%{"group" => group, "key" => key}) do
951 whitelisted_config?(group, key)
954 defp whitelisted_config?(%{:group => group} = config) do
955 whitelisted_config?(group, config[:key])
958 def reload_emoji(conn, _params) do
959 Pleroma.Emoji.reload()
964 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
965 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
967 User.toggle_confirmation(users)
969 ModerationLog.insert_log(%{
972 action: "confirm_email"
978 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
979 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
981 User.try_send_confirmation_email(users)
983 ModerationLog.insert_log(%{
986 action: "resend_confirmation_email"
992 def stats(conn, _) do
993 count = Stats.get_status_visibility_count()
996 |> json(%{"status_visibility" => count})
999 defp page_params(params) do
1000 {get_page(params["page"]), get_page_size(params["page_size"])}
1003 defp get_page(page_string) when is_nil(page_string), do: 1
1005 defp get_page(page_string) do
1006 case Integer.parse(page_string) do
1012 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1014 defp get_page_size(page_size_string) do
1015 case Integer.parse(page_size_string) do
1016 {page_size, _} -> page_size
1017 :error -> @users_page_size