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
11 alias Pleroma.ModerationLog
12 alias Pleroma.Plugs.OAuthScopesPlug
13 alias Pleroma.ReportNote
15 alias Pleroma.UserInviteToken
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Relay
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.AdminAPI.AccountView
20 alias Pleroma.Web.AdminAPI.Config
21 alias Pleroma.Web.AdminAPI.ConfigView
22 alias Pleroma.Web.AdminAPI.ModerationLogView
23 alias Pleroma.Web.AdminAPI.Report
24 alias Pleroma.Web.AdminAPI.ReportView
25 alias Pleroma.Web.AdminAPI.Search
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.Endpoint
28 alias Pleroma.Web.MastodonAPI.StatusView
29 alias Pleroma.Web.Router
33 @descriptions_json Pleroma.Docs.JSON.compile()
38 %{scopes: ["read:accounts"], admin: true}
39 when action in [:list_users, :user_show, :right_get, :invites]
44 %{scopes: ["write:accounts"], admin: true}
54 :user_toggle_activation,
66 %{scopes: ["read:reports"], admin: true}
67 when action in [:list_reports, :report_show]
72 %{scopes: ["write:reports"], admin: true}
73 when action in [:report_update_state, :report_respond]
78 %{scopes: ["read:statuses"], admin: true}
79 when action == :list_user_statuses
84 %{scopes: ["write:statuses"], admin: true}
85 when action in [:status_update, :status_delete]
90 %{scopes: ["read"], admin: true}
91 when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
96 %{scopes: ["write"], admin: true}
97 when action in [:relay_follow, :relay_unfollow, :config_update]
100 action_fallback(:errors)
102 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
103 user = User.get_cached_by_nickname(nickname)
106 ModerationLog.insert_log(%{
116 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
117 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
120 ModerationLog.insert_log(%{
130 def user_follow(%{assigns: %{user: admin}} = conn, %{
131 "follower" => follower_nick,
132 "followed" => followed_nick
134 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
135 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
136 User.follow(follower, followed)
138 ModerationLog.insert_log(%{
150 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
151 "follower" => follower_nick,
152 "followed" => followed_nick
154 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
155 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
156 User.unfollow(follower, followed)
158 ModerationLog.insert_log(%{
170 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
172 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
178 password_confirmation: password,
182 User.register_changeset(%User{}, user_data, need_confirmation: false)
184 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
185 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
188 case Pleroma.Repo.transaction(changesets) do
193 |> Enum.map(fn user ->
194 {:ok, user} = User.post_register_action(user)
198 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
200 ModerationLog.insert_log(%{
202 subjects: Map.values(users),
209 {:error, id, changeset, _} ->
211 Enum.map(changesets.operations, fn
212 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
213 AccountView.render("create-error.json", %{changeset: changeset})
215 {_, {:changeset, current_changeset, _}} ->
216 AccountView.render("create-error.json", %{changeset: current_changeset})
220 |> put_status(:conflict)
225 def user_show(conn, %{"nickname" => nickname}) do
226 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
228 |> put_view(AccountView)
229 |> render("show.json", %{user: user})
231 _ -> {:error, :not_found}
235 def list_instance_statuses(conn, %{"instance" => instance} = params) do
236 {page, page_size} = page_params(params)
239 ActivityPub.fetch_instance_activities(%{
240 "instance" => instance,
241 "limit" => page_size,
242 "offset" => (page - 1) * page_size
246 |> put_view(Pleroma.Web.AdminAPI.StatusView)
247 |> render("index.json", %{activities: activities, as: :activity})
250 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
251 godmode = params["godmode"] == "true" || params["godmode"] == true
253 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
254 {_, page_size} = page_params(params)
257 ActivityPub.fetch_user_activities(user, nil, %{
258 "limit" => page_size,
263 |> put_view(StatusView)
264 |> render("index.json", %{activities: activities, as: :activity})
266 _ -> {:error, :not_found}
270 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
271 user = User.get_cached_by_nickname(nickname)
273 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
275 action = if user.deactivated, do: "activate", else: "deactivate"
277 ModerationLog.insert_log(%{
284 |> put_view(AccountView)
285 |> render("show.json", %{user: updated_user})
288 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
289 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
290 {:ok, updated_users} = User.deactivate(users, false)
292 ModerationLog.insert_log(%{
299 |> put_view(AccountView)
300 |> render("index.json", %{users: Keyword.values(updated_users)})
303 def user_deactivate(%{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, true)
307 ModerationLog.insert_log(%{
314 |> put_view(AccountView)
315 |> render("index.json", %{users: Keyword.values(updated_users)})
318 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
319 with {:ok, _} <- User.tag(nicknames, tags) do
320 ModerationLog.insert_log(%{
322 nicknames: nicknames,
327 json_response(conn, :no_content, "")
331 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
332 with {:ok, _} <- User.untag(nicknames, tags) do
333 ModerationLog.insert_log(%{
335 nicknames: nicknames,
340 json_response(conn, :no_content, "")
344 def list_users(conn, params) do
345 {page, page_size} = page_params(params)
346 filters = maybe_parse_filters(params["filters"])
349 query: params["query"],
351 page_size: page_size,
352 tags: params["tags"],
353 name: params["name"],
354 email: params["email"]
357 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
358 {:ok, users, count} <- filter_service_users(users, count),
362 AccountView.render("index.json",
370 defp filter_service_users(users, count) do
371 filtered_users = Enum.reject(users, &service_user?/1)
372 count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
374 {:ok, filtered_users, count}
377 defp service_user?(user) do
378 String.match?(user.ap_id, ~r/.*\/relay$/) or
379 String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
382 @filters ~w(local external active deactivated is_admin is_moderator)
384 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
385 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
387 defp maybe_parse_filters(filters) do
390 |> Enum.filter(&Enum.member?(@filters, &1))
391 |> Enum.map(&String.to_atom(&1))
392 |> Enum.into(%{}, &{&1, true})
395 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
396 "permission_group" => permission_group,
397 "nicknames" => nicknames
399 when permission_group in ["moderator", "admin"] do
400 update = %{:"is_#{permission_group}" => true}
402 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
404 for u <- users, do: User.admin_api_update(u, update)
406 ModerationLog.insert_log(%{
410 permission: permission_group
416 def right_add_multiple(conn, _) do
417 render_error(conn, :not_found, "No such permission_group")
420 def right_add(%{assigns: %{user: admin}} = conn, %{
421 "permission_group" => permission_group,
422 "nickname" => nickname
424 when permission_group in ["moderator", "admin"] do
425 fields = %{:"is_#{permission_group}" => true}
429 |> User.get_cached_by_nickname()
430 |> User.admin_api_update(fields)
432 ModerationLog.insert_log(%{
436 permission: permission_group
442 def right_add(conn, _) do
443 render_error(conn, :not_found, "No such permission_group")
446 def right_get(conn, %{"nickname" => nickname}) do
447 user = User.get_cached_by_nickname(nickname)
451 is_moderator: user.is_moderator,
452 is_admin: user.is_admin
456 def right_delete_multiple(
457 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
459 "permission_group" => permission_group,
460 "nicknames" => nicknames
463 when permission_group in ["moderator", "admin"] do
464 with false <- Enum.member?(nicknames, admin_nickname) do
465 update = %{:"is_#{permission_group}" => false}
467 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
469 for u <- users, do: User.admin_api_update(u, update)
471 ModerationLog.insert_log(%{
475 permission: permission_group
480 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
484 def right_delete_multiple(conn, _) do
485 render_error(conn, :not_found, "No such permission_group")
489 %{assigns: %{user: admin}} = conn,
491 "permission_group" => permission_group,
492 "nickname" => nickname
495 when permission_group in ["moderator", "admin"] do
496 fields = %{:"is_#{permission_group}" => false}
500 |> User.get_cached_by_nickname()
501 |> User.admin_api_update(fields)
503 ModerationLog.insert_log(%{
507 permission: permission_group
513 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
514 render_error(conn, :forbidden, "You can't revoke your own admin status.")
517 def relay_list(conn, _params) do
518 with {:ok, list} <- Relay.list() do
519 json(conn, %{relays: list})
527 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
528 with {:ok, _message} <- Relay.follow(target) do
529 ModerationLog.insert_log(%{
530 action: "relay_follow",
544 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
545 with {:ok, _message} <- Relay.unfollow(target) do
546 ModerationLog.insert_log(%{
547 action: "relay_unfollow",
561 @doc "Sends registration invite via email"
562 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
564 Pleroma.Config.get([:instance, :invites_enabled]) &&
565 !Pleroma.Config.get([:instance, :registrations_open]),
566 {:ok, invite_token} <- UserInviteToken.create_invite(),
568 Pleroma.Emails.UserEmail.user_invitation_email(
574 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
575 json_response(conn, :no_content, "")
579 @doc "Create an account registration invite token"
580 def create_invite_token(conn, params) do
584 if params["max_use"],
585 do: Map.put(opts, :max_use, params["max_use"]),
589 if params["expires_at"],
590 do: Map.put(opts, :expires_at, params["expires_at"]),
593 {:ok, invite} = UserInviteToken.create_invite(opts)
595 json(conn, AccountView.render("invite.json", %{invite: invite}))
598 @doc "Get list of created invites"
599 def invites(conn, _params) do
600 invites = UserInviteToken.list_invites()
603 |> put_view(AccountView)
604 |> render("invites.json", %{invites: invites})
607 @doc "Revokes invite by token"
608 def revoke_invite(conn, %{"token" => token}) do
609 with {:ok, invite} <- UserInviteToken.find_by_token(token),
610 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
612 |> put_view(AccountView)
613 |> render("invite.json", %{invite: updated_invite})
615 nil -> {:error, :not_found}
619 @doc "Get a password reset token (base64 string) for given nickname"
620 def get_password_reset(conn, %{"nickname" => nickname}) do
621 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
622 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
627 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
631 @doc "Force password reset for a given user"
632 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
633 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
635 Enum.map(users, &User.force_password_reset_async/1)
637 ModerationLog.insert_log(%{
640 action: "force_password_reset"
643 json_response(conn, :no_content, "")
646 def list_reports(conn, params) do
647 {page, page_size} = page_params(params)
649 reports = Utils.get_reports(params, page, page_size)
652 |> put_view(ReportView)
653 |> render("index.json", %{reports: reports})
656 def list_grouped_reports(conn, _params) do
657 statuses = Utils.get_reported_activities()
660 |> put_view(ReportView)
661 |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
664 def report_show(conn, %{"id" => id}) do
665 with %Activity{} = report <- Activity.get_by_id(id) do
667 |> put_view(ReportView)
668 |> render("show.json", Report.extract_report_info(report))
670 _ -> {:error, :not_found}
674 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
677 |> Enum.map(fn report ->
678 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
679 ModerationLog.insert_log(%{
680 action: "report_update",
687 {:error, message} -> %{id: report["id"], error: message}
691 case Enum.any?(result, &Map.has_key?(&1, :error)) do
692 true -> json_response(conn, :bad_request, result)
693 false -> json_response(conn, :no_content, "")
697 def report_notes_create(%{assigns: %{user: user}} = conn, %{
701 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
702 ModerationLog.insert_log(%{
703 action: "report_note",
705 subject: Activity.get_by_id(report_id),
709 json_response(conn, :no_content, "")
711 _ -> json_response(conn, :bad_request, "")
715 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
717 "report_id" => report_id
719 with {:ok, note} <- ReportNote.destroy(note_id) do
720 ModerationLog.insert_log(%{
721 action: "report_note_delete",
723 subject: Activity.get_by_id(report_id),
727 json_response(conn, :no_content, "")
729 _ -> json_response(conn, :bad_request, "")
733 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
734 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
735 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
737 ModerationLog.insert_log(%{
738 action: "status_update",
741 sensitive: sensitive,
742 visibility: params["visibility"]
746 |> put_view(StatusView)
747 |> render("show.json", %{activity: activity})
751 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
752 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
753 ModerationLog.insert_log(%{
754 action: "status_delete",
763 def list_log(conn, params) do
764 {page, page_size} = page_params(params)
767 ModerationLog.get_all(%{
769 page_size: page_size,
770 start_date: params["start_date"],
771 end_date: params["end_date"],
772 user_id: params["user_id"],
773 search: params["search"]
777 |> put_view(ModerationLogView)
778 |> render("index.json", %{log: log})
781 def config_descriptions(conn, _params) do
783 |> Plug.Conn.put_resp_content_type("application/json")
784 |> Plug.Conn.send_resp(200, @descriptions_json)
787 def migrate_from_db(conn, _params) do
788 with :ok <- check_dynamic_configuration(conn) do
789 Mix.Tasks.Pleroma.Config.run([
792 to_string(Pleroma.Config.get(:env)),
800 def config_show(conn, _params) do
801 with :ok <- check_dynamic_configuration(conn) do
802 configs = Pleroma.Repo.all(Config)
805 errors(conn, {:error, "To use dynamic configuration migrate your settings to database."})
808 |> put_view(ConfigView)
809 |> render("index.json", %{configs: configs})
814 def config_update(conn, %{"configs" => configs}) do
815 with :ok <- check_dynamic_configuration(conn) do
818 %{"group" => group, "key" => key, "delete" => true} = params ->
819 with {:ok, config} <-
820 Config.delete(%{group: group, key: key, subkeys: params["subkeys"]}) do
824 %{"group" => group, "key" => key, "value" => value} ->
825 with {:ok, config} <-
826 Config.update_or_create(%{group: group, key: key, value: value}) do
830 |> Enum.reject(&is_nil(&1))
832 Pleroma.Config.TransferTask.load_and_update_env()
834 Mix.Tasks.Pleroma.Config.run([
837 to_string(Pleroma.Config.get(:env))
841 |> put_view(ConfigView)
842 |> render("index.json", %{configs: updated})
846 defp check_dynamic_configuration(conn) do
847 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
850 errors(conn, {:error, "To use this endpoint you need to enable dynamic configuration."})
854 def reload_emoji(conn, _params) do
855 Pleroma.Emoji.reload()
860 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
861 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
863 User.toggle_confirmation(users)
865 ModerationLog.insert_log(%{
868 action: "confirm_email"
874 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
875 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
877 User.try_send_confirmation_email(users)
879 ModerationLog.insert_log(%{
882 action: "resend_confirmation_email"
888 def errors(conn, {:error, :not_found}) do
890 |> put_status(:not_found)
891 |> json(dgettext("errors", "Not found"))
894 def errors(conn, {:error, reason}) do
896 |> put_status(:bad_request)
900 def errors(conn, {:param_cast, _}) do
902 |> put_status(:bad_request)
903 |> json(dgettext("errors", "Invalid parameters"))
906 def errors(conn, _) do
908 |> put_status(:internal_server_error)
909 |> json(dgettext("errors", "Something went wrong"))
912 defp page_params(params) do
913 {get_page(params["page"]), get_page_size(params["page_size"])}
916 defp get_page(page_string) when is_nil(page_string), do: 1
918 defp get_page(page_string) do
919 case Integer.parse(page_string) do
925 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
927 defp get_page_size(page_size_string) do
928 case Integer.parse(page_size_string) do
929 {page_size, _} -> page_size
930 :error -> @users_page_size