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
13 alias Pleroma.ModerationLog
14 alias Pleroma.Plugs.OAuthScopesPlug
15 alias Pleroma.ReportNote
18 alias Pleroma.UserInviteToken
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Relay
21 alias Pleroma.Web.ActivityPub.Utils
22 alias Pleroma.Web.AdminAPI.AccountView
23 alias Pleroma.Web.AdminAPI.ConfigView
24 alias Pleroma.Web.AdminAPI.ModerationLogView
25 alias Pleroma.Web.AdminAPI.Report
26 alias Pleroma.Web.AdminAPI.ReportView
27 alias Pleroma.Web.AdminAPI.Search
28 alias Pleroma.Web.CommonAPI
29 alias Pleroma.Web.Endpoint
30 alias Pleroma.Web.MastodonAPI.StatusView
31 alias Pleroma.Web.Router
35 @descriptions_json Pleroma.Docs.JSON.compile()
40 %{scopes: ["read:accounts"], admin: true}
41 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
46 %{scopes: ["write:accounts"], admin: true}
51 :user_toggle_activation,
58 :update_user_credentials
62 plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
66 %{scopes: ["write:invites"], admin: true}
67 when action in [:create_invite_token, :revoke_invite, :email_invite]
72 %{scopes: ["write:follows"], admin: true}
73 when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
78 %{scopes: ["read:reports"], admin: true}
79 when action in [:list_reports, :report_show]
84 %{scopes: ["write:reports"], admin: true}
85 when action in [:reports_update]
90 %{scopes: ["read:statuses"], admin: true}
91 when action == :list_user_statuses
96 %{scopes: ["write:statuses"], admin: true}
97 when action in [:status_update, :status_delete]
102 %{scopes: ["read"], admin: true}
103 when action in [:config_show, :list_log, :stats]
108 %{scopes: ["write"], admin: true}
109 when action == :config_update
112 action_fallback(:errors)
114 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
115 user = User.get_cached_by_nickname(nickname)
118 ModerationLog.insert_log(%{
128 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
129 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
132 ModerationLog.insert_log(%{
142 def user_follow(%{assigns: %{user: admin}} = conn, %{
143 "follower" => follower_nick,
144 "followed" => followed_nick
146 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
147 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
148 User.follow(follower, followed)
150 ModerationLog.insert_log(%{
162 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
163 "follower" => follower_nick,
164 "followed" => followed_nick
166 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
167 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
168 User.unfollow(follower, followed)
170 ModerationLog.insert_log(%{
182 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
184 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
190 password_confirmation: password,
194 User.register_changeset(%User{}, user_data, need_confirmation: false)
196 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
197 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
200 case Pleroma.Repo.transaction(changesets) do
205 |> Enum.map(fn user ->
206 {:ok, user} = User.post_register_action(user)
210 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
212 ModerationLog.insert_log(%{
214 subjects: Map.values(users),
221 {:error, id, changeset, _} ->
223 Enum.map(changesets.operations, fn
224 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
225 AccountView.render("create-error.json", %{changeset: changeset})
227 {_, {:changeset, current_changeset, _}} ->
228 AccountView.render("create-error.json", %{changeset: current_changeset})
232 |> put_status(:conflict)
237 def user_show(conn, %{"nickname" => nickname}) do
238 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
240 |> put_view(AccountView)
241 |> render("show.json", %{user: user})
243 _ -> {:error, :not_found}
247 def list_instance_statuses(conn, %{"instance" => instance} = params) do
248 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
249 {page, page_size} = page_params(params)
252 ActivityPub.fetch_statuses(nil, %{
253 "instance" => instance,
254 "limit" => page_size,
255 "offset" => (page - 1) * page_size,
256 "exclude_reblogs" => !with_reblogs && "true"
260 |> put_view(Pleroma.Web.AdminAPI.StatusView)
261 |> render("index.json", %{activities: activities, as: :activity})
264 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
265 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
266 godmode = params["godmode"] == "true" || params["godmode"] == true
268 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
269 {_, page_size} = page_params(params)
272 ActivityPub.fetch_user_activities(user, nil, %{
273 "limit" => page_size,
274 "godmode" => godmode,
275 "exclude_reblogs" => !with_reblogs && "true"
279 |> put_view(StatusView)
280 |> render("index.json", %{activities: activities, as: :activity})
282 _ -> {:error, :not_found}
286 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
287 user = User.get_cached_by_nickname(nickname)
289 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
291 action = if user.deactivated, do: "activate", else: "deactivate"
293 ModerationLog.insert_log(%{
300 |> put_view(AccountView)
301 |> render("show.json", %{user: updated_user})
304 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
305 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
306 {:ok, updated_users} = User.deactivate(users, false)
308 ModerationLog.insert_log(%{
315 |> put_view(AccountView)
316 |> render("index.json", %{users: Keyword.values(updated_users)})
319 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
320 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
321 {:ok, updated_users} = User.deactivate(users, true)
323 ModerationLog.insert_log(%{
330 |> put_view(AccountView)
331 |> render("index.json", %{users: Keyword.values(updated_users)})
334 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
335 with {:ok, _} <- User.tag(nicknames, tags) do
336 ModerationLog.insert_log(%{
338 nicknames: nicknames,
343 json_response(conn, :no_content, "")
347 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
348 with {:ok, _} <- User.untag(nicknames, tags) do
349 ModerationLog.insert_log(%{
351 nicknames: nicknames,
356 json_response(conn, :no_content, "")
360 def list_users(conn, params) do
361 {page, page_size} = page_params(params)
362 filters = maybe_parse_filters(params["filters"])
365 query: params["query"],
367 page_size: page_size,
368 tags: params["tags"],
369 name: params["name"],
370 email: params["email"]
373 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
374 {:ok, users, count} <- filter_service_users(users, count),
378 AccountView.render("index.json",
386 defp filter_service_users(users, count) do
387 filtered_users = Enum.reject(users, &service_user?/1)
388 count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
390 {:ok, filtered_users, count}
393 defp service_user?(user) do
394 String.match?(user.ap_id, ~r/.*\/relay$/) or
395 String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
398 @filters ~w(local external active deactivated is_admin is_moderator)
400 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
401 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
403 defp maybe_parse_filters(filters) do
406 |> Enum.filter(&Enum.member?(@filters, &1))
407 |> Enum.map(&String.to_atom(&1))
408 |> Enum.into(%{}, &{&1, true})
411 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
412 "permission_group" => permission_group,
413 "nicknames" => nicknames
415 when permission_group in ["moderator", "admin"] do
416 update = %{:"is_#{permission_group}" => true}
418 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
420 for u <- users, do: User.admin_api_update(u, update)
422 ModerationLog.insert_log(%{
426 permission: permission_group
432 def right_add_multiple(conn, _) do
433 render_error(conn, :not_found, "No such permission_group")
436 def right_add(%{assigns: %{user: admin}} = conn, %{
437 "permission_group" => permission_group,
438 "nickname" => nickname
440 when permission_group in ["moderator", "admin"] do
441 fields = %{:"is_#{permission_group}" => true}
445 |> User.get_cached_by_nickname()
446 |> User.admin_api_update(fields)
448 ModerationLog.insert_log(%{
452 permission: permission_group
458 def right_add(conn, _) do
459 render_error(conn, :not_found, "No such permission_group")
462 def right_get(conn, %{"nickname" => nickname}) do
463 user = User.get_cached_by_nickname(nickname)
467 is_moderator: user.is_moderator,
468 is_admin: user.is_admin
472 def right_delete_multiple(
473 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
475 "permission_group" => permission_group,
476 "nicknames" => nicknames
479 when permission_group in ["moderator", "admin"] do
480 with false <- Enum.member?(nicknames, admin_nickname) do
481 update = %{:"is_#{permission_group}" => false}
483 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
485 for u <- users, do: User.admin_api_update(u, update)
487 ModerationLog.insert_log(%{
491 permission: permission_group
496 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
500 def right_delete_multiple(conn, _) do
501 render_error(conn, :not_found, "No such permission_group")
505 %{assigns: %{user: admin}} = conn,
507 "permission_group" => permission_group,
508 "nickname" => nickname
511 when permission_group in ["moderator", "admin"] do
512 fields = %{:"is_#{permission_group}" => false}
516 |> User.get_cached_by_nickname()
517 |> User.admin_api_update(fields)
519 ModerationLog.insert_log(%{
523 permission: permission_group
529 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
530 render_error(conn, :forbidden, "You can't revoke your own admin status.")
533 def relay_list(conn, _params) do
534 with {:ok, list} <- Relay.list() do
535 json(conn, %{relays: list})
543 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
544 with {:ok, _message} <- Relay.follow(target) do
545 ModerationLog.insert_log(%{
546 action: "relay_follow",
560 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
561 with {:ok, _message} <- Relay.unfollow(target) do
562 ModerationLog.insert_log(%{
563 action: "relay_unfollow",
577 @doc "Sends registration invite via email"
578 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
579 with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
580 {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
581 {:ok, invite_token} <- UserInviteToken.create_invite(),
583 Pleroma.Emails.UserEmail.user_invitation_email(
589 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
590 json_response(conn, :no_content, "")
592 {:registrations_open, _} ->
595 {:error, "To send invites you need to set the `registrations_open` option to false."}
598 {:invites_enabled, _} ->
601 {:error, "To send invites you need to set the `invites_enabled` option to true."}
606 @doc "Create an account registration invite token"
607 def create_invite_token(conn, params) do
611 if params["max_use"],
612 do: Map.put(opts, :max_use, params["max_use"]),
616 if params["expires_at"],
617 do: Map.put(opts, :expires_at, params["expires_at"]),
620 {:ok, invite} = UserInviteToken.create_invite(opts)
622 json(conn, AccountView.render("invite.json", %{invite: invite}))
625 @doc "Get list of created invites"
626 def invites(conn, _params) do
627 invites = UserInviteToken.list_invites()
630 |> put_view(AccountView)
631 |> render("invites.json", %{invites: invites})
634 @doc "Revokes invite by token"
635 def revoke_invite(conn, %{"token" => token}) do
636 with {:ok, invite} <- UserInviteToken.find_by_token(token),
637 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
639 |> put_view(AccountView)
640 |> render("invite.json", %{invite: updated_invite})
642 nil -> {:error, :not_found}
646 @doc "Get a password reset token (base64 string) for given nickname"
647 def get_password_reset(conn, %{"nickname" => nickname}) do
648 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
649 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
654 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
658 @doc "Force password reset for a given user"
659 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
660 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
662 Enum.each(users, &User.force_password_reset_async/1)
664 ModerationLog.insert_log(%{
667 action: "force_password_reset"
670 json_response(conn, :no_content, "")
673 @doc "Show a given user's credentials"
674 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
675 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
677 |> put_view(AccountView)
678 |> render("credentials.json", %{user: user, for: admin})
680 _ -> {:error, :not_found}
684 @doc "Updates a given user"
685 def update_user_credentials(
686 %{assigns: %{user: admin}} = conn,
687 %{"nickname" => nickname} = params
689 with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
691 User.update_as_admin(user, params) do
692 ModerationLog.insert_log(%{
695 action: "updated_users"
698 if params["password"] do
699 User.force_password_reset_async(user)
702 ModerationLog.insert_log(%{
705 action: "force_password_reset"
708 json(conn, %{status: "success"})
710 {:error, changeset} ->
711 {_, {error, _}} = Enum.at(changeset.errors, 0)
712 json(conn, %{error: "New password #{error}."})
715 json(conn, %{error: "Unable to change password."})
719 def list_reports(conn, params) do
720 {page, page_size} = page_params(params)
722 reports = Utils.get_reports(params, page, page_size)
725 |> put_view(ReportView)
726 |> render("index.json", %{reports: reports})
729 def report_show(conn, %{"id" => id}) do
730 with %Activity{} = report <- Activity.get_by_id(id) do
732 |> put_view(ReportView)
733 |> render("show.json", Report.extract_report_info(report))
735 _ -> {:error, :not_found}
739 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
742 |> Enum.map(fn report ->
743 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
744 ModerationLog.insert_log(%{
745 action: "report_update",
752 {:error, message} -> %{id: report["id"], error: message}
756 case Enum.any?(result, &Map.has_key?(&1, :error)) do
757 true -> json_response(conn, :bad_request, result)
758 false -> json_response(conn, :no_content, "")
762 def report_notes_create(%{assigns: %{user: user}} = conn, %{
766 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
767 ModerationLog.insert_log(%{
768 action: "report_note",
770 subject: Activity.get_by_id(report_id),
774 json_response(conn, :no_content, "")
776 _ -> json_response(conn, :bad_request, "")
780 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
782 "report_id" => report_id
784 with {:ok, note} <- ReportNote.destroy(note_id) do
785 ModerationLog.insert_log(%{
786 action: "report_note_delete",
788 subject: Activity.get_by_id(report_id),
792 json_response(conn, :no_content, "")
794 _ -> json_response(conn, :bad_request, "")
798 def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
799 godmode = params["godmode"] == "true" || params["godmode"] == true
800 local_only = params["local_only"] == "true" || params["local_only"] == true
801 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
802 {page, page_size} = page_params(params)
805 ActivityPub.fetch_statuses(nil, %{
806 "godmode" => godmode,
807 "local_only" => local_only,
808 "limit" => page_size,
809 "offset" => (page - 1) * page_size,
810 "exclude_reblogs" => !with_reblogs && "true"
814 |> put_view(Pleroma.Web.AdminAPI.StatusView)
815 |> render("index.json", %{activities: activities, as: :activity})
818 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
819 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
820 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
822 ModerationLog.insert_log(%{
823 action: "status_update",
826 sensitive: sensitive,
827 visibility: params["visibility"]
831 |> put_view(StatusView)
832 |> render("show.json", %{activity: activity})
836 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
837 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
838 ModerationLog.insert_log(%{
839 action: "status_delete",
848 def list_log(conn, params) do
849 {page, page_size} = page_params(params)
852 ModerationLog.get_all(%{
854 page_size: page_size,
855 start_date: params["start_date"],
856 end_date: params["end_date"],
857 user_id: params["user_id"],
858 search: params["search"]
862 |> put_view(ModerationLogView)
863 |> render("index.json", %{log: log})
866 def config_descriptions(conn, _params) do
868 |> Plug.Conn.put_resp_content_type("application/json")
869 |> Plug.Conn.send_resp(200, @descriptions_json)
872 def config_show(conn, %{"only_db" => true}) do
873 with :ok <- configurable_from_database(conn) do
874 configs = Pleroma.Repo.all(ConfigDB)
877 |> put_view(ConfigView)
878 |> render("index.json", %{configs: configs})
882 def config_show(conn, _params) do
883 with :ok <- configurable_from_database(conn) do
884 configs = ConfigDB.get_all_as_keyword()
887 Config.Holder.default_config()
888 |> ConfigDB.merge(configs)
889 |> Enum.map(fn {group, values} ->
890 Enum.map(values, fn {key, value} ->
892 if configs[group][key] do
893 ConfigDB.get_db_keys(configs[group][key], key)
896 db_value = configs[group][key]
899 if !is_nil(db_value) and Keyword.keyword?(db_value) and
900 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
901 ConfigDB.merge_group(group, key, value, db_value)
907 group: ConfigDB.convert(group),
908 key: ConfigDB.convert(key),
909 value: ConfigDB.convert(merged_value)
912 if db, do: Map.put(setting, :db, db), else: setting
917 response = %{configs: merged}
920 if Restarter.Pleroma.need_reboot?() do
921 Map.put(response, :need_reboot, true)
930 def config_update(conn, %{"configs" => configs}) do
931 with :ok <- configurable_from_database(conn) do
934 %{"group" => group, "key" => key, "delete" => true} = params ->
935 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
937 %{"group" => group, "key" => key, "value" => value} ->
938 ConfigDB.update_or_create(%{group: group, key: key, value: value})
940 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
944 |> Enum.map(fn {:ok, config} ->
945 Map.put(config, :db, ConfigDB.get_db_keys(config))
947 |> Enum.split_with(fn config ->
948 Ecto.get_meta(config, :state) == :deleted
951 Config.TransferTask.load_and_update_env(deleted, false)
954 Restarter.Pleroma.need_reboot?() ||
955 Enum.any?(updated, fn config ->
956 group = ConfigDB.from_string(config.group)
957 key = ConfigDB.from_string(config.key)
958 value = ConfigDB.from_binary(config.value)
959 Config.TransferTask.pleroma_need_restart?(group, key, value)
962 response = %{configs: updated}
966 Restarter.Pleroma.need_reboot()
967 Map.put(response, :need_reboot, need_reboot?)
973 |> put_view(ConfigView)
974 |> render("index.json", response)
978 def restart(conn, _params) do
979 with :ok <- configurable_from_database(conn) do
980 Restarter.Pleroma.restart(Config.get(:env), 50)
986 defp configurable_from_database(conn) do
987 if Config.get(:configurable_from_database) do
992 {:error, "To use this endpoint you need to enable configuration from database."}
997 def reload_emoji(conn, _params) do
998 Pleroma.Emoji.reload()
1003 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
1004 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
1006 User.toggle_confirmation(users)
1008 ModerationLog.insert_log(%{
1011 action: "confirm_email"
1017 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
1018 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
1020 User.try_send_confirmation_email(users)
1022 ModerationLog.insert_log(%{
1025 action: "resend_confirmation_email"
1031 def stats(conn, _) do
1032 count = Stats.get_status_visibility_count()
1035 |> json(%{"status_visibility" => count})
1038 def errors(conn, {:error, :not_found}) do
1040 |> put_status(:not_found)
1041 |> json(dgettext("errors", "Not found"))
1044 def errors(conn, {:error, reason}) do
1046 |> put_status(:bad_request)
1050 def errors(conn, {:param_cast, _}) do
1052 |> put_status(:bad_request)
1053 |> json(dgettext("errors", "Invalid parameters"))
1056 def errors(conn, _) do
1058 |> put_status(:internal_server_error)
1059 |> json(dgettext("errors", "Something went wrong"))
1062 defp page_params(params) do
1063 {get_page(params["page"]), get_page_size(params["page_size"])}
1066 defp get_page(page_string) when is_nil(page_string), do: 1
1068 defp get_page(page_string) do
1069 case Integer.parse(page_string) do
1075 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1077 defp get_page_size(page_size_string) do
1078 case Integer.parse(page_size_string) do
1079 {page_size, _} -> page_size
1080 :error -> @users_page_size