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 alias Pleroma.ModerationLog
9 alias Pleroma.Plugs.OAuthScopesPlug
10 alias Pleroma.ReportNote
12 alias Pleroma.UserInviteToken
13 alias Pleroma.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.Relay
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.AdminAPI.AccountView
17 alias Pleroma.Web.AdminAPI.Config
18 alias Pleroma.Web.AdminAPI.ConfigView
19 alias Pleroma.Web.AdminAPI.ModerationLogView
20 alias Pleroma.Web.AdminAPI.Report
21 alias Pleroma.Web.AdminAPI.ReportView
22 alias Pleroma.Web.AdminAPI.Search
23 alias Pleroma.Web.CommonAPI
24 alias Pleroma.Web.Endpoint
25 alias Pleroma.Web.MastodonAPI.StatusView
26 alias Pleroma.Web.Router
28 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
34 %{scopes: ["read:accounts"], admin: true}
35 when action in [:list_users, :user_show, :right_get, :invites]
40 %{scopes: ["write:accounts"], admin: true}
50 :user_toggle_activation,
62 %{scopes: ["read:reports"], admin: true}
63 when action in [:list_reports, :report_show]
68 %{scopes: ["write:reports"], admin: true}
69 when action in [:report_update_state, :report_respond]
74 %{scopes: ["read:statuses"], admin: true}
75 when action == :list_user_statuses
80 %{scopes: ["write:statuses"], admin: true}
81 when action in [:status_update, :status_delete]
86 %{scopes: ["read"], admin: true}
87 when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
92 %{scopes: ["write"], admin: true}
93 when action in [:relay_follow, :relay_unfollow, :config_update]
98 action_fallback(:errors)
100 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
101 user = User.get_cached_by_nickname(nickname)
104 ModerationLog.insert_log(%{
114 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
115 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
118 ModerationLog.insert_log(%{
128 def user_follow(%{assigns: %{user: admin}} = conn, %{
129 "follower" => follower_nick,
130 "followed" => followed_nick
132 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
133 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
134 User.follow(follower, followed)
136 ModerationLog.insert_log(%{
148 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
149 "follower" => follower_nick,
150 "followed" => followed_nick
152 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
153 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
154 User.unfollow(follower, followed)
156 ModerationLog.insert_log(%{
168 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
170 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
176 password_confirmation: password,
180 User.register_changeset(%User{}, user_data, need_confirmation: false)
182 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
183 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
186 case Pleroma.Repo.transaction(changesets) do
191 |> Enum.map(fn user ->
192 {:ok, user} = User.post_register_action(user)
196 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
198 ModerationLog.insert_log(%{
200 subjects: Map.values(users),
207 {:error, id, changeset, _} ->
209 Enum.map(changesets.operations, fn
210 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
211 AccountView.render("create-error.json", %{changeset: changeset})
213 {_, {:changeset, current_changeset, _}} ->
214 AccountView.render("create-error.json", %{changeset: current_changeset})
218 |> put_status(:conflict)
223 def user_show(conn, %{"nickname" => nickname}) do
224 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
226 |> put_view(AccountView)
227 |> render("show.json", %{user: user})
229 _ -> {:error, :not_found}
233 def list_instance_statuses(conn, %{"instance" => instance} = params) do
234 {page, page_size} = page_params(params)
237 ActivityPub.fetch_instance_activities(%{
238 "instance" => instance,
239 "limit" => page_size,
240 "offset" => (page - 1) * page_size
244 |> put_view(StatusView)
245 |> render("index.json", %{activities: activities, as: :activity})
248 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
249 godmode = params["godmode"] == "true" || params["godmode"] == true
251 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
252 {_, page_size} = page_params(params)
255 ActivityPub.fetch_user_activities(user, nil, %{
256 "limit" => page_size,
261 |> put_view(StatusView)
262 |> render("index.json", %{activities: activities, as: :activity})
264 _ -> {:error, :not_found}
268 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
269 user = User.get_cached_by_nickname(nickname)
271 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
273 action = if user.deactivated, do: "activate", else: "deactivate"
275 ModerationLog.insert_log(%{
282 |> put_view(AccountView)
283 |> render("show.json", %{user: updated_user})
286 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
287 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
288 {:ok, updated_users} = User.deactivate(users, false)
290 ModerationLog.insert_log(%{
297 |> put_view(AccountView)
298 |> render("index.json", %{users: Keyword.values(updated_users)})
301 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
302 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
303 {:ok, updated_users} = User.deactivate(users, true)
305 ModerationLog.insert_log(%{
312 |> put_view(AccountView)
313 |> render("index.json", %{users: Keyword.values(updated_users)})
316 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
317 with {:ok, _} <- User.tag(nicknames, tags) do
318 ModerationLog.insert_log(%{
320 nicknames: nicknames,
325 json_response(conn, :no_content, "")
329 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
330 with {:ok, _} <- User.untag(nicknames, tags) do
331 ModerationLog.insert_log(%{
333 nicknames: nicknames,
338 json_response(conn, :no_content, "")
342 def list_users(conn, params) do
343 {page, page_size} = page_params(params)
344 filters = maybe_parse_filters(params["filters"])
347 query: params["query"],
349 page_size: page_size,
350 tags: params["tags"],
351 name: params["name"],
352 email: params["email"]
355 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
356 {:ok, users, count} <- filter_service_users(users, count),
360 AccountView.render("index.json",
368 defp filter_service_users(users, count) do
369 filtered_users = Enum.reject(users, &service_user?/1)
370 count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
372 {:ok, filtered_users, count}
375 defp service_user?(user) do
376 String.match?(user.ap_id, ~r/.*\/relay$/) or
377 String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
380 @filters ~w(local external active deactivated is_admin is_moderator)
382 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
383 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
385 defp maybe_parse_filters(filters) do
388 |> Enum.filter(&Enum.member?(@filters, &1))
389 |> Enum.map(&String.to_atom(&1))
390 |> Enum.into(%{}, &{&1, true})
393 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
394 "permission_group" => permission_group,
395 "nicknames" => nicknames
397 when permission_group in ["moderator", "admin"] do
398 update = %{:"is_#{permission_group}" => true}
400 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
402 for u <- users, do: User.admin_api_update(u, update)
404 ModerationLog.insert_log(%{
408 permission: permission_group
414 def right_add_multiple(conn, _) do
415 render_error(conn, :not_found, "No such permission_group")
418 def right_add(%{assigns: %{user: admin}} = conn, %{
419 "permission_group" => permission_group,
420 "nickname" => nickname
422 when permission_group in ["moderator", "admin"] do
423 fields = %{:"is_#{permission_group}" => true}
427 |> User.get_cached_by_nickname()
428 |> User.admin_api_update(fields)
430 ModerationLog.insert_log(%{
434 permission: permission_group
440 def right_add(conn, _) do
441 render_error(conn, :not_found, "No such permission_group")
444 def right_get(conn, %{"nickname" => nickname}) do
445 user = User.get_cached_by_nickname(nickname)
449 is_moderator: user.is_moderator,
450 is_admin: user.is_admin
454 def right_delete_multiple(
455 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
457 "permission_group" => permission_group,
458 "nicknames" => nicknames
461 when permission_group in ["moderator", "admin"] do
462 with false <- Enum.member?(nicknames, admin_nickname) do
463 update = %{:"is_#{permission_group}" => false}
465 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
467 for u <- users, do: User.admin_api_update(u, update)
469 ModerationLog.insert_log(%{
473 permission: permission_group
478 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
482 def right_delete_multiple(conn, _) do
483 render_error(conn, :not_found, "No such permission_group")
487 %{assigns: %{user: admin}} = conn,
489 "permission_group" => permission_group,
490 "nickname" => nickname
493 when permission_group in ["moderator", "admin"] do
494 fields = %{:"is_#{permission_group}" => false}
498 |> User.get_cached_by_nickname()
499 |> User.admin_api_update(fields)
501 ModerationLog.insert_log(%{
505 permission: permission_group
511 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
512 render_error(conn, :forbidden, "You can't revoke your own admin status.")
515 def relay_list(conn, _params) do
516 with {:ok, list} <- Relay.list() do
517 json(conn, %{relays: list})
525 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
526 with {:ok, _message} <- Relay.follow(target) do
527 ModerationLog.insert_log(%{
528 action: "relay_follow",
542 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
543 with {:ok, _message} <- Relay.unfollow(target) do
544 ModerationLog.insert_log(%{
545 action: "relay_unfollow",
559 @doc "Sends registration invite via email"
560 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
562 Pleroma.Config.get([:instance, :invites_enabled]) &&
563 !Pleroma.Config.get([:instance, :registrations_open]),
564 {:ok, invite_token} <- UserInviteToken.create_invite(),
566 Pleroma.Emails.UserEmail.user_invitation_email(
572 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
573 json_response(conn, :no_content, "")
577 @doc "Create an account registration invite token"
578 def create_invite_token(conn, params) do
582 if params["max_use"],
583 do: Map.put(opts, :max_use, params["max_use"]),
587 if params["expires_at"],
588 do: Map.put(opts, :expires_at, params["expires_at"]),
591 {:ok, invite} = UserInviteToken.create_invite(opts)
593 json(conn, AccountView.render("invite.json", %{invite: invite}))
596 @doc "Get list of created invites"
597 def invites(conn, _params) do
598 invites = UserInviteToken.list_invites()
601 |> put_view(AccountView)
602 |> render("invites.json", %{invites: invites})
605 @doc "Revokes invite by token"
606 def revoke_invite(conn, %{"token" => token}) do
607 with {:ok, invite} <- UserInviteToken.find_by_token(token),
608 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
610 |> put_view(AccountView)
611 |> render("invite.json", %{invite: updated_invite})
613 nil -> {:error, :not_found}
617 @doc "Get a password reset token (base64 string) for given nickname"
618 def get_password_reset(conn, %{"nickname" => nickname}) do
619 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
620 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
625 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
629 @doc "Force password reset for a given user"
630 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
631 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
633 Enum.map(users, &User.force_password_reset_async/1)
635 ModerationLog.insert_log(%{
638 action: "force_password_reset"
641 json_response(conn, :no_content, "")
644 def list_reports(conn, params) do
645 {page, page_size} = page_params(params)
647 reports = Utils.get_reports(params, page, page_size)
650 |> put_view(ReportView)
651 |> render("index.json", %{reports: reports})
654 def list_grouped_reports(conn, _params) do
655 statuses = Utils.get_reported_activities()
658 |> put_view(ReportView)
659 |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
662 def report_show(conn, %{"id" => id}) do
663 with %Activity{} = report <- Activity.get_by_id(id) do
665 |> put_view(ReportView)
666 |> render("show.json", Report.extract_report_info(report))
668 _ -> {:error, :not_found}
672 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
675 |> Enum.map(fn report ->
676 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
677 ModerationLog.insert_log(%{
678 action: "report_update",
685 {:error, message} -> %{id: report["id"], error: message}
689 case Enum.any?(result, &Map.has_key?(&1, :error)) do
690 true -> json_response(conn, :bad_request, result)
691 false -> json_response(conn, :no_content, "")
695 def report_notes_create(%{assigns: %{user: user}} = conn, %{
699 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
700 ModerationLog.insert_log(%{
701 action: "report_note",
703 subject: Activity.get_by_id(report_id),
707 json_response(conn, :no_content, "")
709 _ -> json_response(conn, :bad_request, "")
713 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
715 "report_id" => report_id
717 with {:ok, note} <- ReportNote.destroy(note_id) do
718 ModerationLog.insert_log(%{
719 action: "report_note_delete",
721 subject: Activity.get_by_id(report_id),
725 json_response(conn, :no_content, "")
727 _ -> json_response(conn, :bad_request, "")
731 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
732 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
733 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
735 ModerationLog.insert_log(%{
736 action: "status_update",
739 sensitive: sensitive,
740 visibility: params["visibility"]
744 |> put_view(StatusView)
745 |> render("show.json", %{activity: activity})
749 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
750 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
751 ModerationLog.insert_log(%{
752 action: "status_delete",
761 def list_log(conn, params) do
762 {page, page_size} = page_params(params)
765 ModerationLog.get_all(%{
767 page_size: page_size,
768 start_date: params["start_date"],
769 end_date: params["end_date"],
770 user_id: params["user_id"],
771 search: params["search"]
775 |> put_view(ModerationLogView)
776 |> render("index.json", %{log: log})
779 def migrate_to_db(conn, _params) do
780 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
784 def migrate_from_db(conn, _params) do
785 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
789 def config_show(conn, _params) do
790 configs = Pleroma.Repo.all(Config)
793 |> put_view(ConfigView)
794 |> render("index.json", %{configs: configs})
797 def config_update(conn, %{"configs" => configs}) do
799 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
802 %{"group" => group, "key" => key, "delete" => "true"} = params ->
803 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
806 %{"group" => group, "key" => key, "value" => value} ->
807 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
810 |> Enum.reject(&is_nil(&1))
812 Pleroma.Config.TransferTask.load_and_update_env()
813 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
820 |> put_view(ConfigView)
821 |> render("index.json", %{configs: updated})
824 def reload_emoji(conn, _params) do
825 Pleroma.Emoji.reload()
830 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
831 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
833 User.toggle_confirmation(users)
835 ModerationLog.insert_log(%{
838 action: "confirm_email"
844 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
845 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
847 User.try_send_confirmation_email(users)
849 ModerationLog.insert_log(%{
852 action: "resend_confirmation_email"
858 def errors(conn, {:error, :not_found}) do
860 |> put_status(:not_found)
861 |> json(dgettext("errors", "Not found"))
864 def errors(conn, {:error, reason}) do
866 |> put_status(:bad_request)
870 def errors(conn, {:param_cast, _}) do
872 |> put_status(:bad_request)
873 |> json(dgettext("errors", "Invalid parameters"))
876 def errors(conn, _) do
878 |> put_status(:internal_server_error)
879 |> json(dgettext("errors", "Something went wrong"))
882 defp page_params(params) do
883 {get_page(params["page"]), get_page_size(params["page_size"])}
886 defp get_page(page_string) when is_nil(page_string), do: 1
888 defp get_page(page_string) do
889 case Integer.parse(page_string) do
895 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
897 defp get_page_size(page_size_string) do
898 case Integer.parse(page_size_string) do
899 {page_size, _} -> page_size
900 :error -> @users_page_size