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 list_grouped_reports(conn, _params) do
719 statuses = Utils.get_reported_activities()
722 |> put_view(ReportView)
723 |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
726 def report_show(conn, %{"id" => id}) do
727 with %Activity{} = report <- Activity.get_by_id(id) do
729 |> put_view(ReportView)
730 |> render("show.json", Report.extract_report_info(report))
732 _ -> {:error, :not_found}
736 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
739 |> Enum.map(fn report ->
740 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
741 ModerationLog.insert_log(%{
742 action: "report_update",
749 {:error, message} -> %{id: report["id"], error: message}
753 case Enum.any?(result, &Map.has_key?(&1, :error)) do
754 true -> json_response(conn, :bad_request, result)
755 false -> json_response(conn, :no_content, "")
759 def report_notes_create(%{assigns: %{user: user}} = conn, %{
763 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
764 ModerationLog.insert_log(%{
765 action: "report_note",
767 subject: Activity.get_by_id(report_id),
771 json_response(conn, :no_content, "")
773 _ -> json_response(conn, :bad_request, "")
777 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
779 "report_id" => report_id
781 with {:ok, note} <- ReportNote.destroy(note_id) do
782 ModerationLog.insert_log(%{
783 action: "report_note_delete",
785 subject: Activity.get_by_id(report_id),
789 json_response(conn, :no_content, "")
791 _ -> json_response(conn, :bad_request, "")
795 def list_statuses(%{assigns: %{user: admin}} = conn, params) do
796 godmode = params["godmode"] == "true" || params["godmode"] == true
797 local_only = params["local_only"] == "true" || params["local_only"] == true
798 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
799 {page, page_size} = page_params(params)
802 ActivityPub.fetch_statuses(admin, %{
803 "godmode" => godmode,
804 "local_only" => local_only,
805 "limit" => page_size,
806 "offset" => (page - 1) * page_size,
807 "exclude_reblogs" => !with_reblogs && "true"
811 |> put_view(Pleroma.Web.AdminAPI.StatusView)
812 |> render("index.json", %{activities: activities, as: :activity})
815 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
816 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
817 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
819 ModerationLog.insert_log(%{
820 action: "status_update",
823 sensitive: sensitive,
824 visibility: params["visibility"]
828 |> put_view(StatusView)
829 |> render("show.json", %{activity: activity})
833 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
834 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
835 ModerationLog.insert_log(%{
836 action: "status_delete",
845 def list_log(conn, params) do
846 {page, page_size} = page_params(params)
849 ModerationLog.get_all(%{
851 page_size: page_size,
852 start_date: params["start_date"],
853 end_date: params["end_date"],
854 user_id: params["user_id"],
855 search: params["search"]
859 |> put_view(ModerationLogView)
860 |> render("index.json", %{log: log})
863 def config_descriptions(conn, _params) do
865 |> Plug.Conn.put_resp_content_type("application/json")
866 |> Plug.Conn.send_resp(200, @descriptions_json)
869 def config_show(conn, %{"only_db" => true}) do
870 with :ok <- configurable_from_database(conn) do
871 configs = Pleroma.Repo.all(ConfigDB)
874 |> put_view(ConfigView)
875 |> render("index.json", %{configs: configs})
879 def config_show(conn, _params) do
880 with :ok <- configurable_from_database(conn) do
881 configs = ConfigDB.get_all_as_keyword()
884 Config.Holder.default_config()
885 |> ConfigDB.merge(configs)
886 |> Enum.map(fn {group, values} ->
887 Enum.map(values, fn {key, value} ->
889 if configs[group][key] do
890 ConfigDB.get_db_keys(configs[group][key], key)
893 db_value = configs[group][key]
896 if !is_nil(db_value) and Keyword.keyword?(db_value) and
897 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
898 ConfigDB.merge_group(group, key, value, db_value)
904 group: ConfigDB.convert(group),
905 key: ConfigDB.convert(key),
906 value: ConfigDB.convert(merged_value)
909 if db, do: Map.put(setting, :db, db), else: setting
914 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
918 def config_update(conn, %{"configs" => configs}) do
919 with :ok <- configurable_from_database(conn) do
922 %{"group" => group, "key" => key, "delete" => true} = params ->
923 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
925 %{"group" => group, "key" => key, "value" => value} ->
926 ConfigDB.update_or_create(%{group: group, key: key, value: value})
928 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
932 |> Enum.map(fn {:ok, config} ->
933 Map.put(config, :db, ConfigDB.get_db_keys(config))
935 |> Enum.split_with(fn config ->
936 Ecto.get_meta(config, :state) == :deleted
939 Config.TransferTask.load_and_update_env(deleted, false)
941 if !Restarter.Pleroma.need_reboot?() do
942 changed_reboot_settings? =
944 |> Enum.any?(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 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
955 |> put_view(ConfigView)
956 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
960 def restart(conn, _params) do
961 with :ok <- configurable_from_database(conn) do
962 Restarter.Pleroma.restart(Config.get(:env), 50)
968 def need_reboot(conn, _params) do
969 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
972 defp configurable_from_database(conn) do
973 if Config.get(:configurable_from_database) do
978 {:error, "To use this endpoint you need to enable configuration from database."}
983 def reload_emoji(conn, _params) do
984 Pleroma.Emoji.reload()
989 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
990 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
992 User.toggle_confirmation(users)
994 ModerationLog.insert_log(%{
997 action: "confirm_email"
1003 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
1004 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
1006 User.try_send_confirmation_email(users)
1008 ModerationLog.insert_log(%{
1011 action: "resend_confirmation_email"
1017 def stats(conn, _) do
1018 count = Stats.get_status_visibility_count()
1021 |> json(%{"status_visibility" => count})
1024 def errors(conn, {:error, :not_found}) do
1026 |> put_status(:not_found)
1027 |> json(dgettext("errors", "Not found"))
1030 def errors(conn, {:error, reason}) do
1032 |> put_status(:bad_request)
1036 def errors(conn, {:param_cast, _}) do
1038 |> put_status(:bad_request)
1039 |> json(dgettext("errors", "Invalid parameters"))
1042 def errors(conn, _) do
1044 |> put_status(:internal_server_error)
1045 |> json(dgettext("errors", "Something went wrong"))
1048 defp page_params(params) do
1049 {get_page(params["page"]), get_page_size(params["page_size"])}
1052 defp get_page(page_string) when is_nil(page_string), do: 1
1054 defp get_page(page_string) do
1055 case Integer.parse(page_string) do
1061 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1063 defp get_page_size(page_size_string) do
1064 case Integer.parse(page_size_string) do
1065 {page_size, _} -> page_size
1066 :error -> @users_page_size