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, :migrate_from_db, :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 migrate_from_db(conn, _params) do
797 with :ok <- configurable_from_database(conn) do
798 Mix.Tasks.Pleroma.Config.run([
801 to_string(Pleroma.Config.get(:env)),
809 def config_show(conn, %{"only_db" => true}) do
810 with :ok <- configurable_from_database(conn) do
811 configs = Pleroma.Repo.all(ConfigDB)
816 {:error, "To use configuration from database migrate your settings to database."}
820 |> put_view(ConfigView)
821 |> render("index.json", %{configs: configs})
826 def config_show(conn, _params) do
827 with :ok <- configurable_from_database(conn) do
828 configs = ConfigDB.get_all_as_keyword()
833 {:error, "To use configuration from database migrate your settings to database."}
837 Pleroma.Config.Holder.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 json(conn, %{configs: merged})
872 def config_update(conn, %{"configs" => configs}) do
873 with :ok <- configurable_from_database(conn) do
876 %{"group" => group, "key" => key, "delete" => true} = params ->
877 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
879 %{"group" => group, "key" => key, "value" => value} ->
880 ConfigDB.update_or_create(%{group: group, key: key, value: value})
882 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
886 |> Enum.map(fn {:ok, config} ->
887 Map.put(config, :db, ConfigDB.get_db_keys(config))
889 |> Enum.split_with(fn config ->
890 Ecto.get_meta(config, :state) == :deleted
893 Pleroma.Config.TransferTask.load_and_update_env(deleted, false)
896 Enum.any?(updated, fn config ->
897 group = ConfigDB.from_string(config.group)
898 key = ConfigDB.from_string(config.key)
899 value = ConfigDB.from_binary(config.value)
900 Pleroma.Config.TransferTask.pleroma_need_restart?(group, key, value)
903 response = %{configs: updated}
906 if need_reboot?, do: Map.put(response, :need_reboot, need_reboot?), else: response
909 |> put_view(ConfigView)
910 |> render("index.json", response)
914 def restart(conn, _params) do
915 with :ok <- configurable_from_database(conn) do
916 if Pleroma.Config.get(:env) == :test do
917 Logger.warn("pleroma restarted")
919 send(Restarter.Pleroma, {:restart, 50})
926 defp configurable_from_database(conn) do
927 if Pleroma.Config.get(:configurable_from_database) do
932 {:error, "To use this endpoint you need to enable configuration from database."}
937 def reload_emoji(conn, _params) do
938 Pleroma.Emoji.reload()
943 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
944 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
946 User.toggle_confirmation(users)
948 ModerationLog.insert_log(%{
951 action: "confirm_email"
957 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
958 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
960 User.try_send_confirmation_email(users)
962 ModerationLog.insert_log(%{
965 action: "resend_confirmation_email"
971 def errors(conn, {:error, :not_found}) do
973 |> put_status(:not_found)
974 |> json(dgettext("errors", "Not found"))
977 def errors(conn, {:error, reason}) do
979 |> put_status(:bad_request)
983 def errors(conn, {:param_cast, _}) do
985 |> put_status(:bad_request)
986 |> json(dgettext("errors", "Invalid parameters"))
989 def errors(conn, _) do
991 |> put_status(:internal_server_error)
992 |> json(dgettext("errors", "Something went wrong"))
995 defp page_params(params) do
996 {get_page(params["page"]), get_page_size(params["page_size"])}
999 defp get_page(page_string) when is_nil(page_string), do: 1
1001 defp get_page(page_string) do
1002 case Integer.parse(page_string) do
1008 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1010 defp get_page_size(page_size_string) do
1011 case Integer.parse(page_size_string) do
1012 {page_size, _} -> page_size
1013 :error -> @users_page_size