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]
46 %{scopes: ["write:accounts"], admin: true}
51 :user_toggle_activation,
61 plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
65 %{scopes: ["write:invites"], admin: true}
66 when action in [:create_invite_token, :revoke_invite, :email_invite]
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]
89 %{scopes: ["read:statuses"], admin: true}
90 when action == :list_user_statuses
95 %{scopes: ["write:statuses"], admin: true}
96 when action in [:status_update, :status_delete]
101 %{scopes: ["read"], admin: true}
102 when action in [:config_show, :list_log, :stats]
107 %{scopes: ["write"], admin: true}
108 when action == :config_update
111 action_fallback(:errors)
113 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
114 user = User.get_cached_by_nickname(nickname)
117 ModerationLog.insert_log(%{
127 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
128 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
131 ModerationLog.insert_log(%{
141 def user_follow(%{assigns: %{user: admin}} = conn, %{
142 "follower" => follower_nick,
143 "followed" => followed_nick
145 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
146 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
147 User.follow(follower, followed)
149 ModerationLog.insert_log(%{
161 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
162 "follower" => follower_nick,
163 "followed" => followed_nick
165 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
166 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
167 User.unfollow(follower, followed)
169 ModerationLog.insert_log(%{
181 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
183 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
189 password_confirmation: password,
193 User.register_changeset(%User{}, user_data, need_confirmation: false)
195 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
196 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
199 case Pleroma.Repo.transaction(changesets) do
204 |> Enum.map(fn user ->
205 {:ok, user} = User.post_register_action(user)
209 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
211 ModerationLog.insert_log(%{
213 subjects: Map.values(users),
220 {:error, id, changeset, _} ->
222 Enum.map(changesets.operations, fn
223 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
224 AccountView.render("create-error.json", %{changeset: changeset})
226 {_, {:changeset, current_changeset, _}} ->
227 AccountView.render("create-error.json", %{changeset: current_changeset})
231 |> put_status(:conflict)
236 def user_show(conn, %{"nickname" => nickname}) do
237 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
239 |> put_view(AccountView)
240 |> render("show.json", %{user: user})
242 _ -> {:error, :not_found}
246 def list_instance_statuses(conn, %{"instance" => instance} = params) do
247 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
248 {page, page_size} = page_params(params)
251 ActivityPub.fetch_statuses(nil, %{
252 "instance" => instance,
253 "limit" => page_size,
254 "offset" => (page - 1) * page_size,
255 "exclude_reblogs" => !with_reblogs && "true"
259 |> put_view(Pleroma.Web.AdminAPI.StatusView)
260 |> render("index.json", %{activities: activities, as: :activity})
263 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
264 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
265 godmode = params["godmode"] == "true" || params["godmode"] == true
267 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
268 {_, page_size} = page_params(params)
271 ActivityPub.fetch_user_activities(user, nil, %{
272 "limit" => page_size,
273 "godmode" => godmode,
274 "exclude_reblogs" => !with_reblogs && "true"
278 |> put_view(StatusView)
279 |> render("index.json", %{activities: activities, as: :activity})
281 _ -> {:error, :not_found}
285 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
286 user = User.get_cached_by_nickname(nickname)
288 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
290 action = if user.deactivated, do: "activate", else: "deactivate"
292 ModerationLog.insert_log(%{
299 |> put_view(AccountView)
300 |> render("show.json", %{user: updated_user})
303 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
304 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
305 {:ok, updated_users} = User.deactivate(users, false)
307 ModerationLog.insert_log(%{
314 |> put_view(AccountView)
315 |> render("index.json", %{users: Keyword.values(updated_users)})
318 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
319 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
320 {:ok, updated_users} = User.deactivate(users, true)
322 ModerationLog.insert_log(%{
329 |> put_view(AccountView)
330 |> render("index.json", %{users: Keyword.values(updated_users)})
333 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
334 with {:ok, _} <- User.tag(nicknames, tags) do
335 ModerationLog.insert_log(%{
337 nicknames: nicknames,
342 json_response(conn, :no_content, "")
346 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
347 with {:ok, _} <- User.untag(nicknames, tags) do
348 ModerationLog.insert_log(%{
350 nicknames: nicknames,
355 json_response(conn, :no_content, "")
359 def list_users(conn, params) do
360 {page, page_size} = page_params(params)
361 filters = maybe_parse_filters(params["filters"])
364 query: params["query"],
366 page_size: page_size,
367 tags: params["tags"],
368 name: params["name"],
369 email: params["email"]
372 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
373 {:ok, users, count} <- filter_service_users(users, count),
377 AccountView.render("index.json",
385 defp filter_service_users(users, count) do
386 filtered_users = Enum.reject(users, &service_user?/1)
387 count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
389 {:ok, filtered_users, count}
392 defp service_user?(user) do
393 String.match?(user.ap_id, ~r/.*\/relay$/) or
394 String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
397 @filters ~w(local external active deactivated is_admin is_moderator)
399 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
400 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
402 defp maybe_parse_filters(filters) do
405 |> Enum.filter(&Enum.member?(@filters, &1))
406 |> Enum.map(&String.to_atom(&1))
407 |> Enum.into(%{}, &{&1, true})
410 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
411 "permission_group" => permission_group,
412 "nicknames" => nicknames
414 when permission_group in ["moderator", "admin"] do
415 update = %{:"is_#{permission_group}" => true}
417 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
419 for u <- users, do: User.admin_api_update(u, update)
421 ModerationLog.insert_log(%{
425 permission: permission_group
431 def right_add_multiple(conn, _) do
432 render_error(conn, :not_found, "No such permission_group")
435 def right_add(%{assigns: %{user: admin}} = conn, %{
436 "permission_group" => permission_group,
437 "nickname" => nickname
439 when permission_group in ["moderator", "admin"] do
440 fields = %{:"is_#{permission_group}" => true}
444 |> User.get_cached_by_nickname()
445 |> User.admin_api_update(fields)
447 ModerationLog.insert_log(%{
451 permission: permission_group
457 def right_add(conn, _) do
458 render_error(conn, :not_found, "No such permission_group")
461 def right_get(conn, %{"nickname" => nickname}) do
462 user = User.get_cached_by_nickname(nickname)
466 is_moderator: user.is_moderator,
467 is_admin: user.is_admin
471 def right_delete_multiple(
472 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
474 "permission_group" => permission_group,
475 "nicknames" => nicknames
478 when permission_group in ["moderator", "admin"] do
479 with false <- Enum.member?(nicknames, admin_nickname) do
480 update = %{:"is_#{permission_group}" => false}
482 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
484 for u <- users, do: User.admin_api_update(u, update)
486 ModerationLog.insert_log(%{
490 permission: permission_group
495 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
499 def right_delete_multiple(conn, _) do
500 render_error(conn, :not_found, "No such permission_group")
504 %{assigns: %{user: admin}} = conn,
506 "permission_group" => permission_group,
507 "nickname" => nickname
510 when permission_group in ["moderator", "admin"] do
511 fields = %{:"is_#{permission_group}" => false}
515 |> User.get_cached_by_nickname()
516 |> User.admin_api_update(fields)
518 ModerationLog.insert_log(%{
522 permission: permission_group
528 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
529 render_error(conn, :forbidden, "You can't revoke your own admin status.")
532 def relay_list(conn, _params) do
533 with {:ok, list} <- Relay.list() do
534 json(conn, %{relays: list})
542 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
543 with {:ok, _message} <- Relay.follow(target) do
544 ModerationLog.insert_log(%{
545 action: "relay_follow",
559 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
560 with {:ok, _message} <- Relay.unfollow(target) do
561 ModerationLog.insert_log(%{
562 action: "relay_unfollow",
576 @doc "Sends registration invite via email"
577 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
579 Config.get([:instance, :invites_enabled]) &&
580 !Config.get([:instance, :registrations_open]),
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, "")
594 @doc "Create an account registration invite token"
595 def create_invite_token(conn, params) do
599 if params["max_use"],
600 do: Map.put(opts, :max_use, params["max_use"]),
604 if params["expires_at"],
605 do: Map.put(opts, :expires_at, params["expires_at"]),
608 {:ok, invite} = UserInviteToken.create_invite(opts)
610 json(conn, AccountView.render("invite.json", %{invite: invite}))
613 @doc "Get list of created invites"
614 def invites(conn, _params) do
615 invites = UserInviteToken.list_invites()
618 |> put_view(AccountView)
619 |> render("invites.json", %{invites: invites})
622 @doc "Revokes invite by token"
623 def revoke_invite(conn, %{"token" => token}) do
624 with {:ok, invite} <- UserInviteToken.find_by_token(token),
625 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
627 |> put_view(AccountView)
628 |> render("invite.json", %{invite: updated_invite})
630 nil -> {:error, :not_found}
634 @doc "Get a password reset token (base64 string) for given nickname"
635 def get_password_reset(conn, %{"nickname" => nickname}) do
636 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
637 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
642 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
646 @doc "Force password reset for a given user"
647 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
648 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
650 Enum.each(users, &User.force_password_reset_async/1)
652 ModerationLog.insert_log(%{
655 action: "force_password_reset"
658 json_response(conn, :no_content, "")
661 @doc "Changes password for a given user"
662 def change_password(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do
663 with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
665 User.reset_password(user, %{
666 password: params["new_password"],
667 password_confirmation: params["new_password"]
669 ModerationLog.insert_log(%{
672 action: "change_password"
675 User.force_password_reset_async(user)
677 ModerationLog.insert_log(%{
680 action: "force_password_reset"
683 json(conn, %{status: "success"})
685 {:error, changeset} ->
686 {_, {error, _}} = Enum.at(changeset.errors, 0)
687 json(conn, %{error: "New password #{error}."})
690 json(conn, %{error: "Unable to change password."})
694 def list_reports(conn, params) do
695 {page, page_size} = page_params(params)
697 reports = Utils.get_reports(params, page, page_size)
700 |> put_view(ReportView)
701 |> render("index.json", %{reports: reports})
704 def list_grouped_reports(conn, _params) do
705 statuses = Utils.get_reported_activities()
708 |> put_view(ReportView)
709 |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
712 def report_show(conn, %{"id" => id}) do
713 with %Activity{} = report <- Activity.get_by_id(id) do
715 |> put_view(ReportView)
716 |> render("show.json", Report.extract_report_info(report))
718 _ -> {:error, :not_found}
722 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
725 |> Enum.map(fn report ->
726 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
727 ModerationLog.insert_log(%{
728 action: "report_update",
735 {:error, message} -> %{id: report["id"], error: message}
739 case Enum.any?(result, &Map.has_key?(&1, :error)) do
740 true -> json_response(conn, :bad_request, result)
741 false -> json_response(conn, :no_content, "")
745 def report_notes_create(%{assigns: %{user: user}} = conn, %{
749 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
750 ModerationLog.insert_log(%{
751 action: "report_note",
753 subject: Activity.get_by_id(report_id),
757 json_response(conn, :no_content, "")
759 _ -> json_response(conn, :bad_request, "")
763 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
765 "report_id" => report_id
767 with {:ok, note} <- ReportNote.destroy(note_id) do
768 ModerationLog.insert_log(%{
769 action: "report_note_delete",
771 subject: Activity.get_by_id(report_id),
775 json_response(conn, :no_content, "")
777 _ -> json_response(conn, :bad_request, "")
781 def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
782 godmode = params["godmode"] == "true" || params["godmode"] == true
783 local_only = params["local_only"] == "true" || params["local_only"] == true
784 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
785 {page, page_size} = page_params(params)
788 ActivityPub.fetch_statuses(nil, %{
789 "godmode" => godmode,
790 "local_only" => local_only,
791 "limit" => page_size,
792 "offset" => (page - 1) * page_size,
793 "exclude_reblogs" => !with_reblogs && "true"
797 |> put_view(Pleroma.Web.AdminAPI.StatusView)
798 |> render("index.json", %{activities: activities, as: :activity})
801 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
802 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
803 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
805 ModerationLog.insert_log(%{
806 action: "status_update",
809 sensitive: sensitive,
810 visibility: params["visibility"]
814 |> put_view(StatusView)
815 |> render("show.json", %{activity: activity})
819 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
820 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
821 ModerationLog.insert_log(%{
822 action: "status_delete",
831 def list_log(conn, params) do
832 {page, page_size} = page_params(params)
835 ModerationLog.get_all(%{
837 page_size: page_size,
838 start_date: params["start_date"],
839 end_date: params["end_date"],
840 user_id: params["user_id"],
841 search: params["search"]
845 |> put_view(ModerationLogView)
846 |> render("index.json", %{log: log})
849 def config_descriptions(conn, _params) do
851 |> Plug.Conn.put_resp_content_type("application/json")
852 |> Plug.Conn.send_resp(200, @descriptions_json)
855 def config_show(conn, %{"only_db" => true}) do
856 with :ok <- configurable_from_database(conn) do
857 configs = Pleroma.Repo.all(ConfigDB)
860 |> put_view(ConfigView)
861 |> render("index.json", %{configs: configs})
865 def config_show(conn, _params) do
866 with :ok <- configurable_from_database(conn) do
867 configs = ConfigDB.get_all_as_keyword()
870 Config.Holder.default_config()
871 |> ConfigDB.merge(configs)
872 |> Enum.map(fn {group, values} ->
873 Enum.map(values, fn {key, value} ->
875 if configs[group][key] do
876 ConfigDB.get_db_keys(configs[group][key], key)
879 db_value = configs[group][key]
882 if !is_nil(db_value) and Keyword.keyword?(db_value) and
883 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
884 ConfigDB.merge_group(group, key, value, db_value)
890 group: ConfigDB.convert(group),
891 key: ConfigDB.convert(key),
892 value: ConfigDB.convert(merged_value)
895 if db, do: Map.put(setting, :db, db), else: setting
900 response = %{configs: merged}
903 if Restarter.Pleroma.need_reboot?() do
904 Map.put(response, :need_reboot, true)
913 def config_update(conn, %{"configs" => configs}) do
914 with :ok <- configurable_from_database(conn) do
917 %{"group" => group, "key" => key, "delete" => true} = params ->
918 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
920 %{"group" => group, "key" => key, "value" => value} ->
921 ConfigDB.update_or_create(%{group: group, key: key, value: value})
923 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
927 |> Enum.map(fn {:ok, config} ->
928 Map.put(config, :db, ConfigDB.get_db_keys(config))
930 |> Enum.split_with(fn config ->
931 Ecto.get_meta(config, :state) == :deleted
934 Config.TransferTask.load_and_update_env(deleted, false)
937 Restarter.Pleroma.need_reboot?() ||
938 Enum.any?(updated, fn config ->
939 group = ConfigDB.from_string(config.group)
940 key = ConfigDB.from_string(config.key)
941 value = ConfigDB.from_binary(config.value)
942 Config.TransferTask.pleroma_need_restart?(group, key, value)
945 response = %{configs: updated}
949 Restarter.Pleroma.need_reboot()
950 Map.put(response, :need_reboot, need_reboot?)
956 |> put_view(ConfigView)
957 |> render("index.json", response)
961 def restart(conn, _params) do
962 with :ok <- configurable_from_database(conn) do
963 Restarter.Pleroma.restart(Config.get(:env), 50)
969 defp configurable_from_database(conn) do
970 if Config.get(:configurable_from_database) do
975 {:error, "To use this endpoint you need to enable configuration from database."}
980 def reload_emoji(conn, _params) do
981 Pleroma.Emoji.reload()
986 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
987 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
989 User.toggle_confirmation(users)
991 ModerationLog.insert_log(%{
994 action: "confirm_email"
1000 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
1001 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
1003 User.try_send_confirmation_email(users)
1005 ModerationLog.insert_log(%{
1008 action: "resend_confirmation_email"
1014 def stats(conn, _) do
1015 count = Stats.get_status_visibility_count()
1018 |> json(%{"status_visibility" => count})
1021 def errors(conn, {:error, :not_found}) do
1023 |> put_status(:not_found)
1024 |> json(dgettext("errors", "Not found"))
1027 def errors(conn, {:error, reason}) do
1029 |> put_status(:bad_request)
1033 def errors(conn, {:param_cast, _}) do
1035 |> put_status(:bad_request)
1036 |> json(dgettext("errors", "Invalid parameters"))
1039 def errors(conn, _) do
1041 |> put_status(:internal_server_error)
1042 |> json(dgettext("errors", "Something went wrong"))
1045 defp page_params(params) do
1046 {get_page(params["page"]), get_page_size(params["page_size"])}
1049 defp get_page(page_string) when is_nil(page_string), do: 1
1051 defp get_page(page_string) do
1052 case Integer.parse(page_string) do
1058 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1060 defp get_page_size(page_size_string) do
1061 case Integer.parse(page_size_string) do
1062 {page_size, _} -> page_size
1063 :error -> @users_page_size