1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 {page, page_size} = page_params(params)
250 ActivityPub.fetch_instance_activities(%{
251 "instance" => instance,
252 "limit" => page_size,
253 "offset" => (page - 1) * page_size
257 |> put_view(Pleroma.Web.AdminAPI.StatusView)
258 |> render("index.json", %{activities: activities, as: :activity})
261 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
262 godmode = params["godmode"] == "true" || params["godmode"] == true
264 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
265 {_, page_size} = page_params(params)
268 ActivityPub.fetch_user_activities(user, nil, %{
269 "limit" => page_size,
274 |> put_view(StatusView)
275 |> render("index.json", %{activities: activities, as: :activity})
277 _ -> {:error, :not_found}
281 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
282 user = User.get_cached_by_nickname(nickname)
284 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
286 action = if user.deactivated, do: "activate", else: "deactivate"
288 ModerationLog.insert_log(%{
295 |> put_view(AccountView)
296 |> render("show.json", %{user: updated_user})
299 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
300 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
301 {:ok, updated_users} = User.deactivate(users, false)
303 ModerationLog.insert_log(%{
310 |> put_view(AccountView)
311 |> render("index.json", %{users: Keyword.values(updated_users)})
314 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
315 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
316 {:ok, updated_users} = User.deactivate(users, true)
318 ModerationLog.insert_log(%{
325 |> put_view(AccountView)
326 |> render("index.json", %{users: Keyword.values(updated_users)})
329 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
330 with {:ok, _} <- User.tag(nicknames, tags) do
331 ModerationLog.insert_log(%{
333 nicknames: nicknames,
338 json_response(conn, :no_content, "")
342 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
343 with {:ok, _} <- User.untag(nicknames, tags) do
344 ModerationLog.insert_log(%{
346 nicknames: nicknames,
351 json_response(conn, :no_content, "")
355 def list_users(conn, params) do
356 {page, page_size} = page_params(params)
357 filters = maybe_parse_filters(params["filters"])
360 query: params["query"],
362 page_size: page_size,
363 tags: params["tags"],
364 name: params["name"],
365 email: params["email"]
368 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
369 {:ok, users, count} <- filter_service_users(users, count),
373 AccountView.render("index.json",
381 defp filter_service_users(users, count) do
382 filtered_users = Enum.reject(users, &service_user?/1)
383 count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
385 {:ok, filtered_users, count}
388 defp service_user?(user) do
389 String.match?(user.ap_id, ~r/.*\/relay$/) or
390 String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
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
575 Config.get([:instance, :invites_enabled]) &&
576 !Config.get([:instance, :registrations_open]),
577 {:ok, invite_token} <- UserInviteToken.create_invite(),
579 Pleroma.Emails.UserEmail.user_invitation_email(
585 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
586 json_response(conn, :no_content, "")
590 @doc "Create an account registration invite token"
591 def create_invite_token(conn, params) do
595 if params["max_use"],
596 do: Map.put(opts, :max_use, params["max_use"]),
600 if params["expires_at"],
601 do: Map.put(opts, :expires_at, params["expires_at"]),
604 {:ok, invite} = UserInviteToken.create_invite(opts)
606 json(conn, AccountView.render("invite.json", %{invite: invite}))
609 @doc "Get list of created invites"
610 def invites(conn, _params) do
611 invites = UserInviteToken.list_invites()
614 |> put_view(AccountView)
615 |> render("invites.json", %{invites: invites})
618 @doc "Revokes invite by token"
619 def revoke_invite(conn, %{"token" => token}) do
620 with {:ok, invite} <- UserInviteToken.find_by_token(token),
621 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
623 |> put_view(AccountView)
624 |> render("invite.json", %{invite: updated_invite})
626 nil -> {:error, :not_found}
630 @doc "Get a password reset token (base64 string) for given nickname"
631 def get_password_reset(conn, %{"nickname" => nickname}) do
632 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
633 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
638 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
642 @doc "Force password reset for a given user"
643 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
644 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
646 Enum.each(users, &User.force_password_reset_async/1)
648 ModerationLog.insert_log(%{
651 action: "force_password_reset"
654 json_response(conn, :no_content, "")
657 def list_reports(conn, params) do
658 {page, page_size} = page_params(params)
660 reports = Utils.get_reports(params, page, page_size)
663 |> put_view(ReportView)
664 |> render("index.json", %{reports: reports})
667 def list_grouped_reports(conn, _params) do
668 statuses = Utils.get_reported_activities()
671 |> put_view(ReportView)
672 |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
675 def report_show(conn, %{"id" => id}) do
676 with %Activity{} = report <- Activity.get_by_id(id) do
678 |> put_view(ReportView)
679 |> render("show.json", Report.extract_report_info(report))
681 _ -> {:error, :not_found}
685 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
688 |> Enum.map(fn report ->
689 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
690 ModerationLog.insert_log(%{
691 action: "report_update",
698 {:error, message} -> %{id: report["id"], error: message}
702 case Enum.any?(result, &Map.has_key?(&1, :error)) do
703 true -> json_response(conn, :bad_request, result)
704 false -> json_response(conn, :no_content, "")
708 def report_notes_create(%{assigns: %{user: user}} = conn, %{
712 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
713 ModerationLog.insert_log(%{
714 action: "report_note",
716 subject: Activity.get_by_id(report_id),
720 json_response(conn, :no_content, "")
722 _ -> json_response(conn, :bad_request, "")
726 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
728 "report_id" => report_id
730 with {:ok, note} <- ReportNote.destroy(note_id) do
731 ModerationLog.insert_log(%{
732 action: "report_note_delete",
734 subject: Activity.get_by_id(report_id),
738 json_response(conn, :no_content, "")
740 _ -> json_response(conn, :bad_request, "")
744 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
745 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
746 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
748 ModerationLog.insert_log(%{
749 action: "status_update",
752 sensitive: sensitive,
753 visibility: params["visibility"]
757 |> put_view(StatusView)
758 |> render("show.json", %{activity: activity})
762 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
763 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
764 ModerationLog.insert_log(%{
765 action: "status_delete",
774 def list_log(conn, params) do
775 {page, page_size} = page_params(params)
778 ModerationLog.get_all(%{
780 page_size: page_size,
781 start_date: params["start_date"],
782 end_date: params["end_date"],
783 user_id: params["user_id"],
784 search: params["search"]
788 |> put_view(ModerationLogView)
789 |> render("index.json", %{log: log})
792 def config_descriptions(conn, _params) do
794 |> Plug.Conn.put_resp_content_type("application/json")
795 |> Plug.Conn.send_resp(200, @descriptions_json)
798 def config_show(conn, %{"only_db" => true}) do
799 with :ok <- configurable_from_database(conn) do
800 configs = Pleroma.Repo.all(ConfigDB)
803 |> put_view(ConfigView)
804 |> render("index.json", %{configs: configs})
808 def config_show(conn, _params) do
809 with :ok <- configurable_from_database(conn) do
810 configs = ConfigDB.get_all_as_keyword()
813 Config.Holder.config()
814 |> ConfigDB.merge(configs)
815 |> Enum.map(fn {group, values} ->
816 Enum.map(values, fn {key, value} ->
818 if configs[group][key] do
819 ConfigDB.get_db_keys(configs[group][key], key)
822 db_value = configs[group][key]
825 if !is_nil(db_value) and Keyword.keyword?(db_value) and
826 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
827 ConfigDB.merge_group(group, key, value, db_value)
833 group: ConfigDB.convert(group),
834 key: ConfigDB.convert(key),
835 value: ConfigDB.convert(merged_value)
838 if db, do: Map.put(setting, :db, db), else: setting
843 response = %{configs: merged}
846 if Restarter.Pleroma.need_reboot?() do
847 Map.put(response, :need_reboot, true)
856 def config_update(conn, %{"configs" => configs}) do
857 with :ok <- configurable_from_database(conn) do
860 %{"group" => group, "key" => key, "delete" => true} = params ->
861 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
863 %{"group" => group, "key" => key, "value" => value} ->
864 ConfigDB.update_or_create(%{group: group, key: key, value: value})
866 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
870 |> Enum.map(fn {:ok, config} ->
871 Map.put(config, :db, ConfigDB.get_db_keys(config))
873 |> Enum.split_with(fn config ->
874 Ecto.get_meta(config, :state) == :deleted
877 Config.TransferTask.load_and_update_env(deleted, false)
880 Restarter.Pleroma.need_reboot?() ||
881 Enum.any?(updated, fn config ->
882 group = ConfigDB.from_string(config.group)
883 key = ConfigDB.from_string(config.key)
884 value = ConfigDB.from_binary(config.value)
885 Config.TransferTask.pleroma_need_restart?(group, key, value)
888 response = %{configs: updated}
892 Restarter.Pleroma.need_reboot()
893 Map.put(response, :need_reboot, need_reboot?)
899 |> put_view(ConfigView)
900 |> render("index.json", response)
904 def restart(conn, _params) do
905 with :ok <- configurable_from_database(conn) do
906 Restarter.Pleroma.restart(Config.get(:env), 50)
912 defp configurable_from_database(conn) do
913 if Config.get(:configurable_from_database) do
918 {:error, "To use this endpoint you need to enable configuration from database."}
923 def reload_emoji(conn, _params) do
924 Pleroma.Emoji.reload()
929 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
930 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
932 User.toggle_confirmation(users)
934 ModerationLog.insert_log(%{
937 action: "confirm_email"
943 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
944 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
946 User.try_send_confirmation_email(users)
948 ModerationLog.insert_log(%{
951 action: "resend_confirmation_email"
957 def stats(conn, _) do
958 count = Stats.get_status_visibility_count()
961 |> json(%{"status_visibility" => count})
964 def errors(conn, {:error, :not_found}) do
966 |> put_status(:not_found)
967 |> json(dgettext("errors", "Not found"))
970 def errors(conn, {:error, reason}) do
972 |> put_status(:bad_request)
976 def errors(conn, {:param_cast, _}) do
978 |> put_status(:bad_request)
979 |> json(dgettext("errors", "Invalid parameters"))
982 def errors(conn, _) do
984 |> put_status(:internal_server_error)
985 |> json(dgettext("errors", "Something went wrong"))
988 defp page_params(params) do
989 {get_page(params["page"]), get_page_size(params["page_size"])}
992 defp get_page(page_string) when is_nil(page_string), do: 1
994 defp get_page(page_string) do
995 case Integer.parse(page_string) do
1001 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1003 defp get_page_size(page_size_string) do
1004 case Integer.parse(page_size_string) do
1005 {page_size, _} -> page_size
1006 :error -> @users_page_size