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 def list_reports(conn, params) do
662 {page, page_size} = page_params(params)
664 reports = Utils.get_reports(params, page, page_size)
667 |> put_view(ReportView)
668 |> render("index.json", %{reports: reports})
671 def list_grouped_reports(conn, _params) do
672 statuses = Utils.get_reported_activities()
675 |> put_view(ReportView)
676 |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
679 def report_show(conn, %{"id" => id}) do
680 with %Activity{} = report <- Activity.get_by_id(id) do
682 |> put_view(ReportView)
683 |> render("show.json", Report.extract_report_info(report))
685 _ -> {:error, :not_found}
689 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
692 |> Enum.map(fn report ->
693 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
694 ModerationLog.insert_log(%{
695 action: "report_update",
702 {:error, message} -> %{id: report["id"], error: message}
706 case Enum.any?(result, &Map.has_key?(&1, :error)) do
707 true -> json_response(conn, :bad_request, result)
708 false -> json_response(conn, :no_content, "")
712 def report_notes_create(%{assigns: %{user: user}} = conn, %{
716 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
717 ModerationLog.insert_log(%{
718 action: "report_note",
720 subject: Activity.get_by_id(report_id),
724 json_response(conn, :no_content, "")
726 _ -> json_response(conn, :bad_request, "")
730 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
732 "report_id" => report_id
734 with {:ok, note} <- ReportNote.destroy(note_id) do
735 ModerationLog.insert_log(%{
736 action: "report_note_delete",
738 subject: Activity.get_by_id(report_id),
742 json_response(conn, :no_content, "")
744 _ -> json_response(conn, :bad_request, "")
748 def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
749 godmode = params["godmode"] == "true" || params["godmode"] == true
750 local_only = params["local_only"] == "true" || params["local_only"] == true
751 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
752 {page, page_size} = page_params(params)
755 ActivityPub.fetch_statuses(nil, %{
756 "godmode" => godmode,
757 "local_only" => local_only,
758 "limit" => page_size,
759 "offset" => (page - 1) * page_size,
760 "exclude_reblogs" => !with_reblogs && "true"
764 |> put_view(Pleroma.Web.AdminAPI.StatusView)
765 |> render("index.json", %{activities: activities, as: :activity})
768 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
769 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
770 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
772 ModerationLog.insert_log(%{
773 action: "status_update",
776 sensitive: sensitive,
777 visibility: params["visibility"]
781 |> put_view(StatusView)
782 |> render("show.json", %{activity: activity})
786 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
787 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
788 ModerationLog.insert_log(%{
789 action: "status_delete",
798 def list_log(conn, params) do
799 {page, page_size} = page_params(params)
802 ModerationLog.get_all(%{
804 page_size: page_size,
805 start_date: params["start_date"],
806 end_date: params["end_date"],
807 user_id: params["user_id"],
808 search: params["search"]
812 |> put_view(ModerationLogView)
813 |> render("index.json", %{log: log})
816 def config_descriptions(conn, _params) do
818 |> Plug.Conn.put_resp_content_type("application/json")
819 |> Plug.Conn.send_resp(200, @descriptions_json)
822 def config_show(conn, %{"only_db" => true}) do
823 with :ok <- configurable_from_database(conn) do
824 configs = Pleroma.Repo.all(ConfigDB)
827 |> put_view(ConfigView)
828 |> render("index.json", %{configs: configs})
832 def config_show(conn, _params) do
833 with :ok <- configurable_from_database(conn) do
834 configs = ConfigDB.get_all_as_keyword()
837 Config.Holder.default_config()
838 |> ConfigDB.merge(configs)
839 |> Enum.map(fn {group, values} ->
840 Enum.map(values, fn {key, value} ->
842 if configs[group][key] do
843 ConfigDB.get_db_keys(configs[group][key], key)
846 db_value = configs[group][key]
849 if !is_nil(db_value) and Keyword.keyword?(db_value) and
850 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
851 ConfigDB.merge_group(group, key, value, db_value)
857 group: ConfigDB.convert(group),
858 key: ConfigDB.convert(key),
859 value: ConfigDB.convert(merged_value)
862 if db, do: Map.put(setting, :db, db), else: setting
867 response = %{configs: merged}
870 if Restarter.Pleroma.need_reboot?() do
871 Map.put(response, :need_reboot, true)
880 def config_update(conn, %{"configs" => configs}) do
881 with :ok <- configurable_from_database(conn) do
884 %{"group" => group, "key" => key, "delete" => true} = params ->
885 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
887 %{"group" => group, "key" => key, "value" => value} ->
888 ConfigDB.update_or_create(%{group: group, key: key, value: value})
890 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
894 |> Enum.map(fn {:ok, config} ->
895 Map.put(config, :db, ConfigDB.get_db_keys(config))
897 |> Enum.split_with(fn config ->
898 Ecto.get_meta(config, :state) == :deleted
901 Config.TransferTask.load_and_update_env(deleted, false)
904 Restarter.Pleroma.need_reboot?() ||
905 Enum.any?(updated, fn config ->
906 group = ConfigDB.from_string(config.group)
907 key = ConfigDB.from_string(config.key)
908 value = ConfigDB.from_binary(config.value)
909 Config.TransferTask.pleroma_need_restart?(group, key, value)
912 response = %{configs: updated}
916 Restarter.Pleroma.need_reboot()
917 Map.put(response, :need_reboot, need_reboot?)
923 |> put_view(ConfigView)
924 |> render("index.json", response)
928 def restart(conn, _params) do
929 with :ok <- configurable_from_database(conn) do
930 Restarter.Pleroma.restart(Config.get(:env), 50)
936 defp configurable_from_database(conn) do
937 if Config.get(:configurable_from_database) do
942 {:error, "To use this endpoint you need to enable configuration from database."}
947 def reload_emoji(conn, _params) do
948 Pleroma.Emoji.reload()
953 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
954 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
956 User.toggle_confirmation(users)
958 ModerationLog.insert_log(%{
961 action: "confirm_email"
967 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
968 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
970 User.try_send_confirmation_email(users)
972 ModerationLog.insert_log(%{
975 action: "resend_confirmation_email"
981 def stats(conn, _) do
982 count = Stats.get_status_visibility_count()
985 |> json(%{"status_visibility" => count})
988 def errors(conn, {:error, :not_found}) do
990 |> put_status(:not_found)
991 |> json(dgettext("errors", "Not found"))
994 def errors(conn, {:error, reason}) do
996 |> put_status(:bad_request)
1000 def errors(conn, {:param_cast, _}) do
1002 |> put_status(:bad_request)
1003 |> json(dgettext("errors", "Invalid parameters"))
1006 def errors(conn, _) do
1008 |> put_status(:internal_server_error)
1009 |> json(dgettext("errors", "Something went wrong"))
1012 defp page_params(params) do
1013 {get_page(params["page"]), get_page_size(params["page_size"])}
1016 defp get_page(page_string) when is_nil(page_string), do: 1
1018 defp get_page(page_string) do
1019 case Integer.parse(page_string) do
1025 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1027 defp get_page_size(page_size_string) do
1028 case Integer.parse(page_size_string) do
1029 {page_size, _} -> page_size
1030 :error -> @users_page_size