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
17 alias Pleroma.UserInviteToken
18 alias Pleroma.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.ActivityPub.Relay
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.AdminAPI.AccountView
22 alias Pleroma.Web.AdminAPI.ConfigView
23 alias Pleroma.Web.AdminAPI.ModerationLogView
24 alias Pleroma.Web.AdminAPI.Report
25 alias Pleroma.Web.AdminAPI.ReportView
26 alias Pleroma.Web.AdminAPI.Search
27 alias Pleroma.Web.CommonAPI
28 alias Pleroma.Web.Endpoint
29 alias Pleroma.Web.MastodonAPI.StatusView
30 alias Pleroma.Web.Router
34 @descriptions_json Pleroma.Docs.JSON.compile()
39 %{scopes: ["read:accounts"], admin: true}
40 when action in [:list_users, :user_show, :right_get]
45 %{scopes: ["write:accounts"], admin: true}
50 :user_toggle_activation,
60 plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
64 %{scopes: ["write:invites"], admin: true}
65 when action in [:create_invite_token, :revoke_invite, :email_invite]
70 %{scopes: ["write:follows"], admin: true}
71 when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
76 %{scopes: ["read:reports"], admin: true}
77 when action in [:list_reports, :report_show]
82 %{scopes: ["write:reports"], admin: true}
83 when action in [:reports_update]
88 %{scopes: ["read:statuses"], admin: true}
89 when action == :list_user_statuses
94 %{scopes: ["write:statuses"], admin: true}
95 when action in [:status_update, :status_delete]
100 %{scopes: ["read"], admin: true}
101 when action in [:config_show, :list_log]
106 %{scopes: ["write"], admin: true}
107 when action == :config_update
110 action_fallback(:errors)
112 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
113 user = User.get_cached_by_nickname(nickname)
116 ModerationLog.insert_log(%{
126 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
127 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
130 ModerationLog.insert_log(%{
140 def user_follow(%{assigns: %{user: admin}} = conn, %{
141 "follower" => follower_nick,
142 "followed" => followed_nick
144 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
145 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
146 User.follow(follower, followed)
148 ModerationLog.insert_log(%{
160 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
161 "follower" => follower_nick,
162 "followed" => followed_nick
164 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
165 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
166 User.unfollow(follower, followed)
168 ModerationLog.insert_log(%{
180 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
182 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
188 password_confirmation: password,
192 User.register_changeset(%User{}, user_data, need_confirmation: false)
194 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
195 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
198 case Pleroma.Repo.transaction(changesets) do
203 |> Enum.map(fn user ->
204 {:ok, user} = User.post_register_action(user)
208 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
210 ModerationLog.insert_log(%{
212 subjects: Map.values(users),
219 {:error, id, changeset, _} ->
221 Enum.map(changesets.operations, fn
222 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
223 AccountView.render("create-error.json", %{changeset: changeset})
225 {_, {:changeset, current_changeset, _}} ->
226 AccountView.render("create-error.json", %{changeset: current_changeset})
230 |> put_status(:conflict)
235 def user_show(conn, %{"nickname" => nickname}) do
236 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
238 |> put_view(AccountView)
239 |> render("show.json", %{user: user})
241 _ -> {:error, :not_found}
245 def list_instance_statuses(conn, %{"instance" => instance} = params) do
246 {page, page_size} = page_params(params)
249 ActivityPub.fetch_instance_activities(%{
250 "instance" => instance,
251 "limit" => page_size,
252 "offset" => (page - 1) * page_size
256 |> put_view(Pleroma.Web.AdminAPI.StatusView)
257 |> render("index.json", %{activities: activities, as: :activity})
260 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
261 godmode = params["godmode"] == "true" || params["godmode"] == true
263 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
264 {_, page_size} = page_params(params)
267 ActivityPub.fetch_user_activities(user, nil, %{
268 "limit" => page_size,
273 |> put_view(StatusView)
274 |> render("index.json", %{activities: activities, as: :activity})
276 _ -> {:error, :not_found}
280 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
281 user = User.get_cached_by_nickname(nickname)
283 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
285 action = if user.deactivated, do: "activate", else: "deactivate"
287 ModerationLog.insert_log(%{
294 |> put_view(AccountView)
295 |> render("show.json", %{user: updated_user})
298 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
299 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
300 {:ok, updated_users} = User.deactivate(users, false)
302 ModerationLog.insert_log(%{
309 |> put_view(AccountView)
310 |> render("index.json", %{users: Keyword.values(updated_users)})
313 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
314 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
315 {:ok, updated_users} = User.deactivate(users, true)
317 ModerationLog.insert_log(%{
324 |> put_view(AccountView)
325 |> render("index.json", %{users: Keyword.values(updated_users)})
328 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
329 with {:ok, _} <- User.tag(nicknames, tags) do
330 ModerationLog.insert_log(%{
332 nicknames: nicknames,
337 json_response(conn, :no_content, "")
341 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
342 with {:ok, _} <- User.untag(nicknames, tags) do
343 ModerationLog.insert_log(%{
345 nicknames: nicknames,
350 json_response(conn, :no_content, "")
354 def list_users(conn, params) do
355 {page, page_size} = page_params(params)
356 filters = maybe_parse_filters(params["filters"])
359 query: params["query"],
361 page_size: page_size,
362 tags: params["tags"],
363 name: params["name"],
364 email: params["email"]
367 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
368 {:ok, users, count} <- filter_service_users(users, count),
372 AccountView.render("index.json",
380 defp filter_service_users(users, count) do
381 filtered_users = Enum.reject(users, &service_user?/1)
382 count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
384 {:ok, filtered_users, count}
387 defp service_user?(user) do
388 String.match?(user.ap_id, ~r/.*\/relay$/) or
389 String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
392 @filters ~w(local external active deactivated is_admin is_moderator)
394 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
395 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
397 defp maybe_parse_filters(filters) do
400 |> Enum.filter(&Enum.member?(@filters, &1))
401 |> Enum.map(&String.to_atom(&1))
402 |> Enum.into(%{}, &{&1, true})
405 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
406 "permission_group" => permission_group,
407 "nicknames" => nicknames
409 when permission_group in ["moderator", "admin"] do
410 update = %{:"is_#{permission_group}" => true}
412 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
414 for u <- users, do: User.admin_api_update(u, update)
416 ModerationLog.insert_log(%{
420 permission: permission_group
426 def right_add_multiple(conn, _) do
427 render_error(conn, :not_found, "No such permission_group")
430 def right_add(%{assigns: %{user: admin}} = conn, %{
431 "permission_group" => permission_group,
432 "nickname" => nickname
434 when permission_group in ["moderator", "admin"] do
435 fields = %{:"is_#{permission_group}" => true}
439 |> User.get_cached_by_nickname()
440 |> User.admin_api_update(fields)
442 ModerationLog.insert_log(%{
446 permission: permission_group
452 def right_add(conn, _) do
453 render_error(conn, :not_found, "No such permission_group")
456 def right_get(conn, %{"nickname" => nickname}) do
457 user = User.get_cached_by_nickname(nickname)
461 is_moderator: user.is_moderator,
462 is_admin: user.is_admin
466 def right_delete_multiple(
467 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
469 "permission_group" => permission_group,
470 "nicknames" => nicknames
473 when permission_group in ["moderator", "admin"] do
474 with false <- Enum.member?(nicknames, admin_nickname) do
475 update = %{:"is_#{permission_group}" => false}
477 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
479 for u <- users, do: User.admin_api_update(u, update)
481 ModerationLog.insert_log(%{
485 permission: permission_group
490 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
494 def right_delete_multiple(conn, _) do
495 render_error(conn, :not_found, "No such permission_group")
499 %{assigns: %{user: admin}} = conn,
501 "permission_group" => permission_group,
502 "nickname" => nickname
505 when permission_group in ["moderator", "admin"] do
506 fields = %{:"is_#{permission_group}" => false}
510 |> User.get_cached_by_nickname()
511 |> User.admin_api_update(fields)
513 ModerationLog.insert_log(%{
517 permission: permission_group
523 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
524 render_error(conn, :forbidden, "You can't revoke your own admin status.")
527 def relay_list(conn, _params) do
528 with {:ok, list} <- Relay.list() do
529 json(conn, %{relays: list})
537 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
538 with {:ok, _message} <- Relay.follow(target) do
539 ModerationLog.insert_log(%{
540 action: "relay_follow",
554 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
555 with {:ok, _message} <- Relay.unfollow(target) do
556 ModerationLog.insert_log(%{
557 action: "relay_unfollow",
571 @doc "Sends registration invite via email"
572 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
574 Config.get([:instance, :invites_enabled]) &&
575 !Config.get([:instance, :registrations_open]),
576 {:ok, invite_token} <- UserInviteToken.create_invite(),
578 Pleroma.Emails.UserEmail.user_invitation_email(
584 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
585 json_response(conn, :no_content, "")
589 @doc "Create an account registration invite token"
590 def create_invite_token(conn, params) do
594 if params["max_use"],
595 do: Map.put(opts, :max_use, params["max_use"]),
599 if params["expires_at"],
600 do: Map.put(opts, :expires_at, params["expires_at"]),
603 {:ok, invite} = UserInviteToken.create_invite(opts)
605 json(conn, AccountView.render("invite.json", %{invite: invite}))
608 @doc "Get list of created invites"
609 def invites(conn, _params) do
610 invites = UserInviteToken.list_invites()
613 |> put_view(AccountView)
614 |> render("invites.json", %{invites: invites})
617 @doc "Revokes invite by token"
618 def revoke_invite(conn, %{"token" => token}) do
619 with {:ok, invite} <- UserInviteToken.find_by_token(token),
620 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
622 |> put_view(AccountView)
623 |> render("invite.json", %{invite: updated_invite})
625 nil -> {:error, :not_found}
629 @doc "Get a password reset token (base64 string) for given nickname"
630 def get_password_reset(conn, %{"nickname" => nickname}) do
631 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
632 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
637 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
641 @doc "Force password reset for a given user"
642 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
643 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
645 Enum.each(users, &User.force_password_reset_async/1)
647 ModerationLog.insert_log(%{
650 action: "force_password_reset"
653 json_response(conn, :no_content, "")
656 def list_reports(conn, params) do
657 {page, page_size} = page_params(params)
659 reports = Utils.get_reports(params, page, page_size)
662 |> put_view(ReportView)
663 |> render("index.json", %{reports: reports})
666 def list_grouped_reports(conn, _params) do
667 statuses = Utils.get_reported_activities()
670 |> put_view(ReportView)
671 |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
674 def report_show(conn, %{"id" => id}) do
675 with %Activity{} = report <- Activity.get_by_id(id) do
677 |> put_view(ReportView)
678 |> render("show.json", Report.extract_report_info(report))
680 _ -> {:error, :not_found}
684 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
687 |> Enum.map(fn report ->
688 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
689 ModerationLog.insert_log(%{
690 action: "report_update",
697 {:error, message} -> %{id: report["id"], error: message}
701 case Enum.any?(result, &Map.has_key?(&1, :error)) do
702 true -> json_response(conn, :bad_request, result)
703 false -> json_response(conn, :no_content, "")
707 def report_notes_create(%{assigns: %{user: user}} = conn, %{
711 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
712 ModerationLog.insert_log(%{
713 action: "report_note",
715 subject: Activity.get_by_id(report_id),
719 json_response(conn, :no_content, "")
721 _ -> json_response(conn, :bad_request, "")
725 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
727 "report_id" => report_id
729 with {:ok, note} <- ReportNote.destroy(note_id) do
730 ModerationLog.insert_log(%{
731 action: "report_note_delete",
733 subject: Activity.get_by_id(report_id),
737 json_response(conn, :no_content, "")
739 _ -> json_response(conn, :bad_request, "")
743 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
744 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
745 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
747 ModerationLog.insert_log(%{
748 action: "status_update",
751 sensitive: sensitive,
752 visibility: params["visibility"]
756 |> put_view(StatusView)
757 |> render("show.json", %{activity: activity})
761 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
762 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
763 ModerationLog.insert_log(%{
764 action: "status_delete",
773 def list_log(conn, params) do
774 {page, page_size} = page_params(params)
777 ModerationLog.get_all(%{
779 page_size: page_size,
780 start_date: params["start_date"],
781 end_date: params["end_date"],
782 user_id: params["user_id"],
783 search: params["search"]
787 |> put_view(ModerationLogView)
788 |> render("index.json", %{log: log})
791 def config_descriptions(conn, _params) do
793 |> Plug.Conn.put_resp_content_type("application/json")
794 |> Plug.Conn.send_resp(200, @descriptions_json)
797 def config_show(conn, %{"only_db" => true}) do
798 with :ok <- configurable_from_database(conn) do
799 configs = Pleroma.Repo.all(ConfigDB)
802 |> put_view(ConfigView)
803 |> render("index.json", %{configs: configs})
807 def config_show(conn, _params) do
808 with :ok <- configurable_from_database(conn) do
809 configs = ConfigDB.get_all_as_keyword()
812 Config.Holder.config()
813 |> ConfigDB.merge(configs)
814 |> Enum.map(fn {group, values} ->
815 Enum.map(values, fn {key, value} ->
817 if configs[group][key] do
818 ConfigDB.get_db_keys(configs[group][key], key)
821 db_value = configs[group][key]
824 if !is_nil(db_value) and Keyword.keyword?(db_value) and
825 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
826 ConfigDB.merge_group(group, key, value, db_value)
832 group: ConfigDB.convert(group),
833 key: ConfigDB.convert(key),
834 value: ConfigDB.convert(merged_value)
837 if db, do: Map.put(setting, :db, db), else: setting
842 response = %{configs: merged}
845 if Restarter.Pleroma.need_reboot?() do
846 Map.put(response, :need_reboot, true)
855 def config_update(conn, %{"configs" => configs}) do
856 with :ok <- configurable_from_database(conn) do
859 %{"group" => group, "key" => key, "delete" => true} = params ->
860 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
862 %{"group" => group, "key" => key, "value" => value} ->
863 ConfigDB.update_or_create(%{group: group, key: key, value: value})
865 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
869 |> Enum.map(fn {:ok, config} ->
870 Map.put(config, :db, ConfigDB.get_db_keys(config))
872 |> Enum.split_with(fn config ->
873 Ecto.get_meta(config, :state) == :deleted
876 Config.TransferTask.load_and_update_env(deleted, false)
879 Restarter.Pleroma.need_reboot?() ||
880 Enum.any?(updated, fn config ->
881 group = ConfigDB.from_string(config.group)
882 key = ConfigDB.from_string(config.key)
883 value = ConfigDB.from_binary(config.value)
884 Config.TransferTask.pleroma_need_restart?(group, key, value)
887 response = %{configs: updated}
891 Restarter.Pleroma.need_reboot()
892 Map.put(response, :need_reboot, need_reboot?)
898 |> put_view(ConfigView)
899 |> render("index.json", response)
903 def restart(conn, _params) do
904 with :ok <- configurable_from_database(conn) do
905 Restarter.Pleroma.restart(Config.get(:env), 50)
911 defp configurable_from_database(conn) do
912 if Config.get(:configurable_from_database) do
917 {:error, "To use this endpoint you need to enable configuration from database."}
922 def reload_emoji(conn, _params) do
923 Pleroma.Emoji.reload()
928 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
929 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
931 User.toggle_confirmation(users)
933 ModerationLog.insert_log(%{
936 action: "confirm_email"
942 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
943 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
945 User.try_send_confirmation_email(users)
947 ModerationLog.insert_log(%{
950 action: "resend_confirmation_email"
956 def errors(conn, {:error, :not_found}) do
958 |> put_status(:not_found)
959 |> json(dgettext("errors", "Not found"))
962 def errors(conn, {:error, reason}) do
964 |> put_status(:bad_request)
968 def errors(conn, {:param_cast, _}) do
970 |> put_status(:bad_request)
971 |> json(dgettext("errors", "Invalid parameters"))
974 def errors(conn, _) do
976 |> put_status(:internal_server_error)
977 |> json(dgettext("errors", "Something went wrong"))
980 defp page_params(params) do
981 {get_page(params["page"]), get_page_size(params["page_size"])}
984 defp get_page(page_string) when is_nil(page_string), do: 1
986 defp get_page(page_string) do
987 case Integer.parse(page_string) do
993 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
995 defp get_page_size(page_size_string) do
996 case Integer.parse(page_size_string) do
997 {page_size, _} -> page_size
998 :error -> @users_page_size