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.ConfigDB
12 alias Pleroma.ModerationLog
13 alias Pleroma.Plugs.OAuthScopesPlug
14 alias Pleroma.ReportNote
16 alias Pleroma.UserInviteToken
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Relay
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.AdminAPI.AccountView
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]
44 %{scopes: ["write:accounts"], admin: true}
49 :user_toggle_activation,
59 plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
63 %{scopes: ["write:invites"], admin: true}
64 when action in [:create_invite_token, :revoke_invite, :email_invite]
69 %{scopes: ["write:follows"], admin: true}
70 when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
75 %{scopes: ["read:reports"], admin: true}
76 when action in [:list_reports, :report_show]
81 %{scopes: ["write:reports"], admin: true}
82 when action in [:reports_update]
87 %{scopes: ["read:statuses"], admin: true}
88 when action == :list_user_statuses
93 %{scopes: ["write:statuses"], admin: true}
94 when action in [:status_update, :status_delete]
99 %{scopes: ["read"], admin: true}
100 when action in [:config_show, :list_log]
105 %{scopes: ["write"], admin: true}
106 when action == :config_update
109 action_fallback(:errors)
111 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
112 user = User.get_cached_by_nickname(nickname)
115 ModerationLog.insert_log(%{
125 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
126 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
129 ModerationLog.insert_log(%{
139 def user_follow(%{assigns: %{user: admin}} = conn, %{
140 "follower" => follower_nick,
141 "followed" => followed_nick
143 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
144 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
145 User.follow(follower, followed)
147 ModerationLog.insert_log(%{
159 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
160 "follower" => follower_nick,
161 "followed" => followed_nick
163 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
164 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
165 User.unfollow(follower, followed)
167 ModerationLog.insert_log(%{
179 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
181 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
187 password_confirmation: password,
191 User.register_changeset(%User{}, user_data, need_confirmation: false)
193 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
194 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
197 case Pleroma.Repo.transaction(changesets) do
202 |> Enum.map(fn user ->
203 {:ok, user} = User.post_register_action(user)
207 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
209 ModerationLog.insert_log(%{
211 subjects: Map.values(users),
218 {:error, id, changeset, _} ->
220 Enum.map(changesets.operations, fn
221 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
222 AccountView.render("create-error.json", %{changeset: changeset})
224 {_, {:changeset, current_changeset, _}} ->
225 AccountView.render("create-error.json", %{changeset: current_changeset})
229 |> put_status(:conflict)
234 def user_show(conn, %{"nickname" => nickname}) do
235 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
237 |> put_view(AccountView)
238 |> render("show.json", %{user: user})
240 _ -> {:error, :not_found}
244 def list_instance_statuses(conn, %{"instance" => instance} = params) do
245 {page, page_size} = page_params(params)
248 ActivityPub.fetch_instance_activities(%{
249 "instance" => instance,
250 "limit" => page_size,
251 "offset" => (page - 1) * page_size
255 |> put_view(Pleroma.Web.AdminAPI.StatusView)
256 |> render("index.json", %{activities: activities, as: :activity})
259 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
260 godmode = params["godmode"] == "true" || params["godmode"] == true
262 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
263 {_, page_size} = page_params(params)
266 ActivityPub.fetch_user_activities(user, nil, %{
267 "limit" => page_size,
272 |> put_view(StatusView)
273 |> render("index.json", %{activities: activities, as: :activity})
275 _ -> {:error, :not_found}
279 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
280 user = User.get_cached_by_nickname(nickname)
282 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
284 action = if user.deactivated, do: "activate", else: "deactivate"
286 ModerationLog.insert_log(%{
293 |> put_view(AccountView)
294 |> render("show.json", %{user: updated_user})
297 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
298 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
299 {:ok, updated_users} = User.deactivate(users, false)
301 ModerationLog.insert_log(%{
308 |> put_view(AccountView)
309 |> render("index.json", %{users: Keyword.values(updated_users)})
312 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
313 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
314 {:ok, updated_users} = User.deactivate(users, true)
316 ModerationLog.insert_log(%{
323 |> put_view(AccountView)
324 |> render("index.json", %{users: Keyword.values(updated_users)})
327 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
328 with {:ok, _} <- User.tag(nicknames, tags) do
329 ModerationLog.insert_log(%{
331 nicknames: nicknames,
336 json_response(conn, :no_content, "")
340 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
341 with {:ok, _} <- User.untag(nicknames, tags) do
342 ModerationLog.insert_log(%{
344 nicknames: nicknames,
349 json_response(conn, :no_content, "")
353 def list_users(conn, params) do
354 {page, page_size} = page_params(params)
355 filters = maybe_parse_filters(params["filters"])
358 query: params["query"],
360 page_size: page_size,
361 tags: params["tags"],
362 name: params["name"],
363 email: params["email"]
366 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
367 {:ok, users, count} <- filter_service_users(users, count),
371 AccountView.render("index.json",
379 defp filter_service_users(users, count) do
380 filtered_users = Enum.reject(users, &service_user?/1)
381 count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
383 {:ok, filtered_users, count}
386 defp service_user?(user) do
387 String.match?(user.ap_id, ~r/.*\/relay$/) or
388 String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
391 @filters ~w(local external active deactivated is_admin is_moderator)
393 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
394 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
396 defp maybe_parse_filters(filters) do
399 |> Enum.filter(&Enum.member?(@filters, &1))
400 |> Enum.map(&String.to_atom(&1))
401 |> Enum.into(%{}, &{&1, true})
404 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
405 "permission_group" => permission_group,
406 "nicknames" => nicknames
408 when permission_group in ["moderator", "admin"] do
409 update = %{:"is_#{permission_group}" => true}
411 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
413 for u <- users, do: User.admin_api_update(u, update)
415 ModerationLog.insert_log(%{
419 permission: permission_group
425 def right_add_multiple(conn, _) do
426 render_error(conn, :not_found, "No such permission_group")
429 def right_add(%{assigns: %{user: admin}} = conn, %{
430 "permission_group" => permission_group,
431 "nickname" => nickname
433 when permission_group in ["moderator", "admin"] do
434 fields = %{:"is_#{permission_group}" => true}
438 |> User.get_cached_by_nickname()
439 |> User.admin_api_update(fields)
441 ModerationLog.insert_log(%{
445 permission: permission_group
451 def right_add(conn, _) do
452 render_error(conn, :not_found, "No such permission_group")
455 def right_get(conn, %{"nickname" => nickname}) do
456 user = User.get_cached_by_nickname(nickname)
460 is_moderator: user.is_moderator,
461 is_admin: user.is_admin
465 def right_delete_multiple(
466 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
468 "permission_group" => permission_group,
469 "nicknames" => nicknames
472 when permission_group in ["moderator", "admin"] do
473 with false <- Enum.member?(nicknames, admin_nickname) do
474 update = %{:"is_#{permission_group}" => false}
476 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
478 for u <- users, do: User.admin_api_update(u, update)
480 ModerationLog.insert_log(%{
484 permission: permission_group
489 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
493 def right_delete_multiple(conn, _) do
494 render_error(conn, :not_found, "No such permission_group")
498 %{assigns: %{user: admin}} = conn,
500 "permission_group" => permission_group,
501 "nickname" => nickname
504 when permission_group in ["moderator", "admin"] do
505 fields = %{:"is_#{permission_group}" => false}
509 |> User.get_cached_by_nickname()
510 |> User.admin_api_update(fields)
512 ModerationLog.insert_log(%{
516 permission: permission_group
522 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
523 render_error(conn, :forbidden, "You can't revoke your own admin status.")
526 def relay_list(conn, _params) do
527 with {:ok, list} <- Relay.list() do
528 json(conn, %{relays: list})
536 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
537 with {:ok, _message} <- Relay.follow(target) do
538 ModerationLog.insert_log(%{
539 action: "relay_follow",
553 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
554 with {:ok, _message} <- Relay.unfollow(target) do
555 ModerationLog.insert_log(%{
556 action: "relay_unfollow",
570 @doc "Sends registration invite via email"
571 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
573 Pleroma.Config.get([:instance, :invites_enabled]) &&
574 !Pleroma.Config.get([:instance, :registrations_open]),
575 {:ok, invite_token} <- UserInviteToken.create_invite(),
577 Pleroma.Emails.UserEmail.user_invitation_email(
583 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
584 json_response(conn, :no_content, "")
588 @doc "Create an account registration invite token"
589 def create_invite_token(conn, params) do
593 if params["max_use"],
594 do: Map.put(opts, :max_use, params["max_use"]),
598 if params["expires_at"],
599 do: Map.put(opts, :expires_at, params["expires_at"]),
602 {:ok, invite} = UserInviteToken.create_invite(opts)
604 json(conn, AccountView.render("invite.json", %{invite: invite}))
607 @doc "Get list of created invites"
608 def invites(conn, _params) do
609 invites = UserInviteToken.list_invites()
612 |> put_view(AccountView)
613 |> render("invites.json", %{invites: invites})
616 @doc "Revokes invite by token"
617 def revoke_invite(conn, %{"token" => token}) do
618 with {:ok, invite} <- UserInviteToken.find_by_token(token),
619 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
621 |> put_view(AccountView)
622 |> render("invite.json", %{invite: updated_invite})
624 nil -> {:error, :not_found}
628 @doc "Get a password reset token (base64 string) for given nickname"
629 def get_password_reset(conn, %{"nickname" => nickname}) do
630 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
631 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
636 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
640 @doc "Force password reset for a given user"
641 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
642 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
644 Enum.each(users, &User.force_password_reset_async/1)
646 ModerationLog.insert_log(%{
649 action: "force_password_reset"
652 json_response(conn, :no_content, "")
655 def list_reports(conn, params) do
656 {page, page_size} = page_params(params)
658 reports = Utils.get_reports(params, page, page_size)
661 |> put_view(ReportView)
662 |> render("index.json", %{reports: reports})
665 def list_grouped_reports(conn, _params) do
666 statuses = Utils.get_reported_activities()
669 |> put_view(ReportView)
670 |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
673 def report_show(conn, %{"id" => id}) do
674 with %Activity{} = report <- Activity.get_by_id(id) do
676 |> put_view(ReportView)
677 |> render("show.json", Report.extract_report_info(report))
679 _ -> {:error, :not_found}
683 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
686 |> Enum.map(fn report ->
687 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
688 ModerationLog.insert_log(%{
689 action: "report_update",
696 {:error, message} -> %{id: report["id"], error: message}
700 case Enum.any?(result, &Map.has_key?(&1, :error)) do
701 true -> json_response(conn, :bad_request, result)
702 false -> json_response(conn, :no_content, "")
706 def report_notes_create(%{assigns: %{user: user}} = conn, %{
710 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
711 ModerationLog.insert_log(%{
712 action: "report_note",
714 subject: Activity.get_by_id(report_id),
718 json_response(conn, :no_content, "")
720 _ -> json_response(conn, :bad_request, "")
724 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
726 "report_id" => report_id
728 with {:ok, note} <- ReportNote.destroy(note_id) do
729 ModerationLog.insert_log(%{
730 action: "report_note_delete",
732 subject: Activity.get_by_id(report_id),
736 json_response(conn, :no_content, "")
738 _ -> json_response(conn, :bad_request, "")
742 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
743 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
744 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
746 ModerationLog.insert_log(%{
747 action: "status_update",
750 sensitive: sensitive,
751 visibility: params["visibility"]
755 |> put_view(StatusView)
756 |> render("show.json", %{activity: activity})
760 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
761 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
762 ModerationLog.insert_log(%{
763 action: "status_delete",
772 def list_log(conn, params) do
773 {page, page_size} = page_params(params)
776 ModerationLog.get_all(%{
778 page_size: page_size,
779 start_date: params["start_date"],
780 end_date: params["end_date"],
781 user_id: params["user_id"],
782 search: params["search"]
786 |> put_view(ModerationLogView)
787 |> render("index.json", %{log: log})
790 def config_descriptions(conn, _params) do
792 |> Plug.Conn.put_resp_content_type("application/json")
793 |> Plug.Conn.send_resp(200, @descriptions_json)
796 def config_show(conn, %{"only_db" => true}) do
797 with :ok <- configurable_from_database(conn) do
798 configs = Pleroma.Repo.all(ConfigDB)
803 {:error, "To use configuration from database migrate your settings to database."}
807 |> put_view(ConfigView)
808 |> render("index.json", %{configs: configs})
813 def config_show(conn, _params) do
814 with :ok <- configurable_from_database(conn) do
815 configs = ConfigDB.get_all_as_keyword()
820 {:error, "To use configuration from database migrate your settings to database."}
824 Pleroma.Config.Holder.config()
825 |> ConfigDB.merge(configs)
826 |> Enum.map(fn {group, values} ->
827 Enum.map(values, fn {key, value} ->
829 if configs[group][key] do
830 ConfigDB.get_db_keys(configs[group][key], key)
833 db_value = configs[group][key]
836 if !is_nil(db_value) and Keyword.keyword?(db_value) and
837 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
838 ConfigDB.merge_group(group, key, value, db_value)
844 group: ConfigDB.convert(group),
845 key: ConfigDB.convert(key),
846 value: ConfigDB.convert(merged_value)
849 if db, do: Map.put(setting, :db, db), else: setting
854 json(conn, %{configs: merged})
859 def config_update(conn, %{"configs" => configs}) do
860 with :ok <- configurable_from_database(conn) do
863 %{"group" => group, "key" => key, "delete" => true} = params ->
864 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
866 %{"group" => group, "key" => key, "value" => value} ->
867 ConfigDB.update_or_create(%{group: group, key: key, value: value})
869 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
873 |> Enum.map(fn {:ok, config} ->
874 Map.put(config, :db, ConfigDB.get_db_keys(config))
876 |> Enum.split_with(fn config ->
877 Ecto.get_meta(config, :state) == :deleted
880 Pleroma.Config.TransferTask.load_and_update_env(deleted, false)
883 Enum.any?(updated, fn config ->
884 group = ConfigDB.from_string(config.group)
885 key = ConfigDB.from_string(config.key)
886 value = ConfigDB.from_binary(config.value)
887 Pleroma.Config.TransferTask.pleroma_need_restart?(group, key, value)
890 response = %{configs: updated}
893 if need_reboot?, do: Map.put(response, :need_reboot, need_reboot?), else: response
896 |> put_view(ConfigView)
897 |> render("index.json", response)
901 def restart(conn, _params) do
902 with :ok <- configurable_from_database(conn) do
903 if Pleroma.Config.get(:env) == :test do
904 Logger.warn("pleroma restarted")
906 send(Restarter.Pleroma, {:restart, 50})
913 defp configurable_from_database(conn) do
914 if Pleroma.Config.get(:configurable_from_database) do
919 {:error, "To use this endpoint you need to enable configuration from database."}
924 def reload_emoji(conn, _params) do
925 Pleroma.Emoji.reload()
930 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
931 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
933 User.toggle_confirmation(users)
935 ModerationLog.insert_log(%{
938 action: "confirm_email"
944 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
945 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
947 User.try_send_confirmation_email(users)
949 ModerationLog.insert_log(%{
952 action: "resend_confirmation_email"
958 def errors(conn, {:error, :not_found}) do
960 |> put_status(:not_found)
961 |> json(dgettext("errors", "Not found"))
964 def errors(conn, {:error, reason}) do
966 |> put_status(:bad_request)
970 def errors(conn, {:param_cast, _}) do
972 |> put_status(:bad_request)
973 |> json(dgettext("errors", "Invalid parameters"))
976 def errors(conn, _) do
978 |> put_status(:internal_server_error)
979 |> json(dgettext("errors", "Something went wrong"))
982 defp page_params(params) do
983 {get_page(params["page"]), get_page_size(params["page_size"])}
986 defp get_page(page_string) when is_nil(page_string), do: 1
988 defp get_page(page_string) do
989 case Integer.parse(page_string) do
995 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
997 defp get_page_size(page_size_string) do
998 case Integer.parse(page_size_string) do
999 {page_size, _} -> page_size
1000 :error -> @users_page_size