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
580 Config.get([:instance, :invites_enabled]) &&
581 !Config.get([:instance, :registrations_open]),
582 {:ok, invite_token} <- UserInviteToken.create_invite(),
584 Pleroma.Emails.UserEmail.user_invitation_email(
590 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
591 json_response(conn, :no_content, "")
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 "Show a given user's credentials"
663 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
664 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
666 |> put_view(AccountView)
667 |> render("credentials.json", %{user: user, for: admin})
669 _ -> {:error, :not_found}
673 @doc "Updates a given user"
674 def update_user_credentials(
675 %{assigns: %{user: admin}} = conn,
676 %{"nickname" => nickname} = params
678 with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
680 User.update_as_admin(user, params) do
681 ModerationLog.insert_log(%{
684 action: "updated_users"
687 if params["password"] do
688 User.force_password_reset_async(user)
691 ModerationLog.insert_log(%{
694 action: "force_password_reset"
697 json(conn, %{status: "success"})
699 {:error, changeset} ->
700 {_, {error, _}} = Enum.at(changeset.errors, 0)
701 json(conn, %{error: "New password #{error}."})
704 json(conn, %{error: "Unable to change password."})
708 def list_reports(conn, params) do
709 {page, page_size} = page_params(params)
711 reports = Utils.get_reports(params, page, page_size)
714 |> put_view(ReportView)
715 |> render("index.json", %{reports: reports})
718 def report_show(conn, %{"id" => id}) do
719 with %Activity{} = report <- Activity.get_by_id(id) do
721 |> put_view(ReportView)
722 |> render("show.json", Report.extract_report_info(report))
724 _ -> {:error, :not_found}
728 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
731 |> Enum.map(fn report ->
732 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
733 ModerationLog.insert_log(%{
734 action: "report_update",
741 {:error, message} -> %{id: report["id"], error: message}
745 case Enum.any?(result, &Map.has_key?(&1, :error)) do
746 true -> json_response(conn, :bad_request, result)
747 false -> json_response(conn, :no_content, "")
751 def report_notes_create(%{assigns: %{user: user}} = conn, %{
755 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
756 ModerationLog.insert_log(%{
757 action: "report_note",
759 subject: Activity.get_by_id(report_id),
763 json_response(conn, :no_content, "")
765 _ -> json_response(conn, :bad_request, "")
769 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
771 "report_id" => report_id
773 with {:ok, note} <- ReportNote.destroy(note_id) do
774 ModerationLog.insert_log(%{
775 action: "report_note_delete",
777 subject: Activity.get_by_id(report_id),
781 json_response(conn, :no_content, "")
783 _ -> json_response(conn, :bad_request, "")
787 def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
788 godmode = params["godmode"] == "true" || params["godmode"] == true
789 local_only = params["local_only"] == "true" || params["local_only"] == true
790 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
791 {page, page_size} = page_params(params)
794 ActivityPub.fetch_statuses(nil, %{
795 "godmode" => godmode,
796 "local_only" => local_only,
797 "limit" => page_size,
798 "offset" => (page - 1) * page_size,
799 "exclude_reblogs" => !with_reblogs && "true"
803 |> put_view(Pleroma.Web.AdminAPI.StatusView)
804 |> render("index.json", %{activities: activities, as: :activity})
807 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
808 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
809 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
811 ModerationLog.insert_log(%{
812 action: "status_update",
815 sensitive: sensitive,
816 visibility: params["visibility"]
820 |> put_view(StatusView)
821 |> render("show.json", %{activity: activity})
825 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
826 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
827 ModerationLog.insert_log(%{
828 action: "status_delete",
837 def list_log(conn, params) do
838 {page, page_size} = page_params(params)
841 ModerationLog.get_all(%{
843 page_size: page_size,
844 start_date: params["start_date"],
845 end_date: params["end_date"],
846 user_id: params["user_id"],
847 search: params["search"]
851 |> put_view(ModerationLogView)
852 |> render("index.json", %{log: log})
855 def config_descriptions(conn, _params) do
857 |> Plug.Conn.put_resp_content_type("application/json")
858 |> Plug.Conn.send_resp(200, @descriptions_json)
861 def config_show(conn, %{"only_db" => true}) do
862 with :ok <- configurable_from_database(conn) do
863 configs = Pleroma.Repo.all(ConfigDB)
866 |> put_view(ConfigView)
867 |> render("index.json", %{configs: configs})
871 def config_show(conn, _params) do
872 with :ok <- configurable_from_database(conn) do
873 configs = ConfigDB.get_all_as_keyword()
876 Config.Holder.default_config()
877 |> ConfigDB.merge(configs)
878 |> Enum.map(fn {group, values} ->
879 Enum.map(values, fn {key, value} ->
881 if configs[group][key] do
882 ConfigDB.get_db_keys(configs[group][key], key)
885 db_value = configs[group][key]
888 if !is_nil(db_value) and Keyword.keyword?(db_value) and
889 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
890 ConfigDB.merge_group(group, key, value, db_value)
896 group: ConfigDB.convert(group),
897 key: ConfigDB.convert(key),
898 value: ConfigDB.convert(merged_value)
901 if db, do: Map.put(setting, :db, db), else: setting
906 response = %{configs: merged}
909 if Restarter.Pleroma.need_reboot?() do
910 Map.put(response, :need_reboot, true)
919 def config_update(conn, %{"configs" => configs}) do
920 with :ok <- configurable_from_database(conn) do
923 %{"group" => group, "key" => key, "delete" => true} = params ->
924 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
926 %{"group" => group, "key" => key, "value" => value} ->
927 ConfigDB.update_or_create(%{group: group, key: key, value: value})
929 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
933 |> Enum.map(fn {:ok, config} ->
934 Map.put(config, :db, ConfigDB.get_db_keys(config))
936 |> Enum.split_with(fn config ->
937 Ecto.get_meta(config, :state) == :deleted
940 Config.TransferTask.load_and_update_env(deleted, false)
943 Restarter.Pleroma.need_reboot?() ||
944 Enum.any?(updated, fn config ->
945 group = ConfigDB.from_string(config.group)
946 key = ConfigDB.from_string(config.key)
947 value = ConfigDB.from_binary(config.value)
948 Config.TransferTask.pleroma_need_restart?(group, key, value)
951 response = %{configs: updated}
955 Restarter.Pleroma.need_reboot()
956 Map.put(response, :need_reboot, need_reboot?)
962 |> put_view(ConfigView)
963 |> render("index.json", response)
967 def restart(conn, _params) do
968 with :ok <- configurable_from_database(conn) do
969 Restarter.Pleroma.restart(Config.get(:env), 50)
975 defp configurable_from_database(conn) do
976 if Config.get(:configurable_from_database) do
981 {:error, "To use this endpoint you need to enable configuration from database."}
986 def reload_emoji(conn, _params) do
987 Pleroma.Emoji.reload()
992 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
993 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
995 User.toggle_confirmation(users)
997 ModerationLog.insert_log(%{
1000 action: "confirm_email"
1006 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
1007 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
1009 User.try_send_confirmation_email(users)
1011 ModerationLog.insert_log(%{
1014 action: "resend_confirmation_email"
1020 def stats(conn, _) do
1021 count = Stats.get_status_visibility_count()
1024 |> json(%{"status_visibility" => count})
1027 def errors(conn, {:error, :not_found}) do
1029 |> put_status(:not_found)
1030 |> json(dgettext("errors", "Not found"))
1033 def errors(conn, {:error, reason}) do
1035 |> put_status(:bad_request)
1039 def errors(conn, {:param_cast, _}) do
1041 |> put_status(:bad_request)
1042 |> json(dgettext("errors", "Invalid parameters"))
1045 def errors(conn, _) do
1047 |> put_status(:internal_server_error)
1048 |> json(dgettext("errors", "Something went wrong"))
1051 defp page_params(params) do
1052 {get_page(params["page"]), get_page_size(params["page_size"])}
1055 defp get_page(page_string) when is_nil(page_string), do: 1
1057 defp get_page(page_string) do
1058 case Integer.parse(page_string) do
1064 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1066 defp get_page_size(page_size_string) do
1067 case Integer.parse(page_size_string) do
1068 {page_size, _} -> page_size
1069 :error -> @users_page_size