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
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 {page, page_size} = page_params(params)
250 ActivityPub.fetch_statuses(nil, %{
251 "instance" => instance,
252 "limit" => page_size,
253 "offset" => (page - 1) * page_size
257 |> put_view(Pleroma.Web.AdminAPI.StatusView)
258 |> render("index.json", %{activities: activities, as: :activity})
261 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
262 godmode = params["godmode"] == "true" || params["godmode"] == true
264 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
265 {_, page_size} = page_params(params)
268 ActivityPub.fetch_user_activities(user, nil, %{
269 "limit" => page_size,
274 |> put_view(StatusView)
275 |> render("index.json", %{activities: activities, as: :activity})
277 _ -> {:error, :not_found}
281 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
282 user = User.get_cached_by_nickname(nickname)
284 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
286 action = if user.deactivated, do: "activate", else: "deactivate"
288 ModerationLog.insert_log(%{
295 |> put_view(AccountView)
296 |> render("show.json", %{user: updated_user})
299 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
300 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
301 {:ok, updated_users} = User.deactivate(users, false)
303 ModerationLog.insert_log(%{
310 |> put_view(AccountView)
311 |> render("index.json", %{users: Keyword.values(updated_users)})
314 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
315 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
316 {:ok, updated_users} = User.deactivate(users, true)
318 ModerationLog.insert_log(%{
325 |> put_view(AccountView)
326 |> render("index.json", %{users: Keyword.values(updated_users)})
329 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
330 with {:ok, _} <- User.tag(nicknames, tags) do
331 ModerationLog.insert_log(%{
333 nicknames: nicknames,
338 json_response(conn, :no_content, "")
342 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
343 with {:ok, _} <- User.untag(nicknames, tags) do
344 ModerationLog.insert_log(%{
346 nicknames: nicknames,
351 json_response(conn, :no_content, "")
355 def list_users(conn, params) do
356 {page, page_size} = page_params(params)
357 filters = maybe_parse_filters(params["filters"])
360 query: params["query"],
362 page_size: page_size,
363 tags: params["tags"],
364 name: params["name"],
365 email: params["email"]
368 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
369 {:ok, users, count} <- filter_service_users(users, count),
373 AccountView.render("index.json",
381 defp filter_service_users(users, count) do
382 filtered_users = Enum.reject(users, &service_user?/1)
383 count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
385 {:ok, filtered_users, count}
388 defp service_user?(user) do
389 String.match?(user.ap_id, ~r/.*\/relay$/) or
390 String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
393 @filters ~w(local external active deactivated is_admin is_moderator)
395 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
396 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
398 defp maybe_parse_filters(filters) do
401 |> Enum.filter(&Enum.member?(@filters, &1))
402 |> Enum.map(&String.to_atom(&1))
403 |> Enum.into(%{}, &{&1, true})
406 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
407 "permission_group" => permission_group,
408 "nicknames" => nicknames
410 when permission_group in ["moderator", "admin"] do
411 update = %{:"is_#{permission_group}" => true}
413 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
415 for u <- users, do: User.admin_api_update(u, update)
417 ModerationLog.insert_log(%{
421 permission: permission_group
427 def right_add_multiple(conn, _) do
428 render_error(conn, :not_found, "No such permission_group")
431 def right_add(%{assigns: %{user: admin}} = conn, %{
432 "permission_group" => permission_group,
433 "nickname" => nickname
435 when permission_group in ["moderator", "admin"] do
436 fields = %{:"is_#{permission_group}" => true}
440 |> User.get_cached_by_nickname()
441 |> User.admin_api_update(fields)
443 ModerationLog.insert_log(%{
447 permission: permission_group
453 def right_add(conn, _) do
454 render_error(conn, :not_found, "No such permission_group")
457 def right_get(conn, %{"nickname" => nickname}) do
458 user = User.get_cached_by_nickname(nickname)
462 is_moderator: user.is_moderator,
463 is_admin: user.is_admin
467 def right_delete_multiple(
468 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
470 "permission_group" => permission_group,
471 "nicknames" => nicknames
474 when permission_group in ["moderator", "admin"] do
475 with false <- Enum.member?(nicknames, admin_nickname) do
476 update = %{:"is_#{permission_group}" => false}
478 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
480 for u <- users, do: User.admin_api_update(u, update)
482 ModerationLog.insert_log(%{
486 permission: permission_group
491 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
495 def right_delete_multiple(conn, _) do
496 render_error(conn, :not_found, "No such permission_group")
500 %{assigns: %{user: admin}} = conn,
502 "permission_group" => permission_group,
503 "nickname" => nickname
506 when permission_group in ["moderator", "admin"] do
507 fields = %{:"is_#{permission_group}" => false}
511 |> User.get_cached_by_nickname()
512 |> User.admin_api_update(fields)
514 ModerationLog.insert_log(%{
518 permission: permission_group
524 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
525 render_error(conn, :forbidden, "You can't revoke your own admin status.")
528 def relay_list(conn, _params) do
529 with {:ok, list} <- Relay.list() do
530 json(conn, %{relays: list})
538 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
539 with {:ok, _message} <- Relay.follow(target) do
540 ModerationLog.insert_log(%{
541 action: "relay_follow",
555 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
556 with {:ok, _message} <- Relay.unfollow(target) do
557 ModerationLog.insert_log(%{
558 action: "relay_unfollow",
572 @doc "Sends registration invite via email"
573 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
575 Config.get([:instance, :invites_enabled]) &&
576 !Config.get([:instance, :registrations_open]),
577 {:ok, invite_token} <- UserInviteToken.create_invite(),
579 Pleroma.Emails.UserEmail.user_invitation_email(
585 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
586 json_response(conn, :no_content, "")
590 @doc "Create an account registration invite token"
591 def create_invite_token(conn, params) do
595 if params["max_use"],
596 do: Map.put(opts, :max_use, params["max_use"]),
600 if params["expires_at"],
601 do: Map.put(opts, :expires_at, params["expires_at"]),
604 {:ok, invite} = UserInviteToken.create_invite(opts)
606 json(conn, AccountView.render("invite.json", %{invite: invite}))
609 @doc "Get list of created invites"
610 def invites(conn, _params) do
611 invites = UserInviteToken.list_invites()
614 |> put_view(AccountView)
615 |> render("invites.json", %{invites: invites})
618 @doc "Revokes invite by token"
619 def revoke_invite(conn, %{"token" => token}) do
620 with {:ok, invite} <- UserInviteToken.find_by_token(token),
621 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
623 |> put_view(AccountView)
624 |> render("invite.json", %{invite: updated_invite})
626 nil -> {:error, :not_found}
630 @doc "Get a password reset token (base64 string) for given nickname"
631 def get_password_reset(conn, %{"nickname" => nickname}) do
632 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
633 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
638 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
642 @doc "Force password reset for a given user"
643 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
644 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
646 Enum.each(users, &User.force_password_reset_async/1)
648 ModerationLog.insert_log(%{
651 action: "force_password_reset"
654 json_response(conn, :no_content, "")
657 def list_reports(conn, params) do
658 {page, page_size} = page_params(params)
660 reports = Utils.get_reports(params, page, page_size)
663 |> put_view(ReportView)
664 |> render("index.json", %{reports: reports})
667 def list_grouped_reports(conn, _params) do
668 statuses = Utils.get_reported_activities()
671 |> put_view(ReportView)
672 |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
675 def report_show(conn, %{"id" => id}) do
676 with %Activity{} = report <- Activity.get_by_id(id) do
678 |> put_view(ReportView)
679 |> render("show.json", Report.extract_report_info(report))
681 _ -> {:error, :not_found}
685 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
688 |> Enum.map(fn report ->
689 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
690 ModerationLog.insert_log(%{
691 action: "report_update",
698 {:error, message} -> %{id: report["id"], error: message}
702 case Enum.any?(result, &Map.has_key?(&1, :error)) do
703 true -> json_response(conn, :bad_request, result)
704 false -> json_response(conn, :no_content, "")
708 def report_notes_create(%{assigns: %{user: user}} = conn, %{
712 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
713 ModerationLog.insert_log(%{
714 action: "report_note",
716 subject: Activity.get_by_id(report_id),
720 json_response(conn, :no_content, "")
722 _ -> json_response(conn, :bad_request, "")
726 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
728 "report_id" => report_id
730 with {:ok, note} <- ReportNote.destroy(note_id) do
731 ModerationLog.insert_log(%{
732 action: "report_note_delete",
734 subject: Activity.get_by_id(report_id),
738 json_response(conn, :no_content, "")
740 _ -> json_response(conn, :bad_request, "")
744 def list_statuses(%{assigns: %{user: admin}} = conn, params) do
745 godmode = params["godmode"] == "true" || params["godmode"] == true
746 local_only = params["local_only"] == "true" || params["local_only"] == true
747 {page, page_size} = page_params(params)
750 ActivityPub.fetch_statuses(admin, %{
751 "godmode" => godmode,
752 "local_only" => local_only,
753 "limit" => page_size,
754 "offset" => (page - 1) * page_size
758 |> put_view(Pleroma.Web.AdminAPI.StatusView)
759 |> render("index.json", %{activities: activities, as: :activity})
762 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
763 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
764 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
766 ModerationLog.insert_log(%{
767 action: "status_update",
770 sensitive: sensitive,
771 visibility: params["visibility"]
775 |> put_view(StatusView)
776 |> render("show.json", %{activity: activity})
780 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
781 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
782 ModerationLog.insert_log(%{
783 action: "status_delete",
792 def list_log(conn, params) do
793 {page, page_size} = page_params(params)
796 ModerationLog.get_all(%{
798 page_size: page_size,
799 start_date: params["start_date"],
800 end_date: params["end_date"],
801 user_id: params["user_id"],
802 search: params["search"]
806 |> put_view(ModerationLogView)
807 |> render("index.json", %{log: log})
810 def config_descriptions(conn, _params) do
812 |> Plug.Conn.put_resp_content_type("application/json")
813 |> Plug.Conn.send_resp(200, @descriptions_json)
816 def config_show(conn, %{"only_db" => true}) do
817 with :ok <- configurable_from_database(conn) do
818 configs = Pleroma.Repo.all(ConfigDB)
821 |> put_view(ConfigView)
822 |> 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()
831 Config.Holder.config()
832 |> ConfigDB.merge(configs)
833 |> Enum.map(fn {group, values} ->
834 Enum.map(values, fn {key, value} ->
836 if configs[group][key] do
837 ConfigDB.get_db_keys(configs[group][key], key)
840 db_value = configs[group][key]
843 if !is_nil(db_value) and Keyword.keyword?(db_value) and
844 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
845 ConfigDB.merge_group(group, key, value, db_value)
851 group: ConfigDB.convert(group),
852 key: ConfigDB.convert(key),
853 value: ConfigDB.convert(merged_value)
856 if db, do: Map.put(setting, :db, db), else: setting
861 response = %{configs: merged}
864 if Restarter.Pleroma.need_reboot?() do
865 Map.put(response, :need_reboot, true)
874 def config_update(conn, %{"configs" => configs}) do
875 with :ok <- configurable_from_database(conn) do
878 %{"group" => group, "key" => key, "delete" => true} = params ->
879 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
881 %{"group" => group, "key" => key, "value" => value} ->
882 ConfigDB.update_or_create(%{group: group, key: key, value: value})
884 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
888 |> Enum.map(fn {:ok, config} ->
889 Map.put(config, :db, ConfigDB.get_db_keys(config))
891 |> Enum.split_with(fn config ->
892 Ecto.get_meta(config, :state) == :deleted
895 Config.TransferTask.load_and_update_env(deleted, false)
898 Restarter.Pleroma.need_reboot?() ||
899 Enum.any?(updated, fn config ->
900 group = ConfigDB.from_string(config.group)
901 key = ConfigDB.from_string(config.key)
902 value = ConfigDB.from_binary(config.value)
903 Config.TransferTask.pleroma_need_restart?(group, key, value)
906 response = %{configs: updated}
910 Restarter.Pleroma.need_reboot()
911 Map.put(response, :need_reboot, need_reboot?)
917 |> put_view(ConfigView)
918 |> render("index.json", response)
922 def restart(conn, _params) do
923 with :ok <- configurable_from_database(conn) do
924 Restarter.Pleroma.restart(Config.get(:env), 50)
930 defp configurable_from_database(conn) do
931 if Config.get(:configurable_from_database) do
936 {:error, "To use this endpoint you need to enable configuration from database."}
941 def reload_emoji(conn, _params) do
942 Pleroma.Emoji.reload()
947 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
948 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
950 User.toggle_confirmation(users)
952 ModerationLog.insert_log(%{
955 action: "confirm_email"
961 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
962 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
964 User.try_send_confirmation_email(users)
966 ModerationLog.insert_log(%{
969 action: "resend_confirmation_email"
975 def stats(conn, _) do
976 count = Stats.get_status_visibility_count()
979 |> json(%{"status_visibility" => count})
982 def errors(conn, {:error, :not_found}) do
984 |> put_status(:not_found)
985 |> json(dgettext("errors", "Not found"))
988 def errors(conn, {:error, reason}) do
990 |> put_status(:bad_request)
994 def errors(conn, {:param_cast, _}) do
996 |> put_status(:bad_request)
997 |> json(dgettext("errors", "Invalid parameters"))
1000 def errors(conn, _) do
1002 |> put_status(:internal_server_error)
1003 |> json(dgettext("errors", "Something went wrong"))
1006 defp page_params(params) do
1007 {get_page(params["page"]), get_page_size(params["page_size"])}
1010 defp get_page(page_string) when is_nil(page_string), do: 1
1012 defp get_page(page_string) do
1013 case Integer.parse(page_string) do
1019 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1021 defp get_page_size(page_size_string) do
1022 case Integer.parse(page_size_string) do
1023 {page_size, _} -> page_size
1024 :error -> @users_page_size