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
11 alias Pleroma.UserInviteToken
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Relay
14 alias Pleroma.Web.ActivityPub.Utils
15 alias Pleroma.Web.AdminAPI.AccountView
16 alias Pleroma.Web.AdminAPI.Config
17 alias Pleroma.Web.AdminAPI.ConfigView
18 alias Pleroma.Web.AdminAPI.ModerationLogView
19 alias Pleroma.Web.AdminAPI.Report
20 alias Pleroma.Web.AdminAPI.ReportView
21 alias Pleroma.Web.AdminAPI.Search
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.Endpoint
24 alias Pleroma.Web.MastodonAPI.StatusView
25 alias Pleroma.Web.Router
27 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
33 %{scopes: ["read:accounts"]}
34 when action in [:list_users, :user_show, :right_get, :invites]
39 %{scopes: ["write:accounts"]}
49 :user_toggle_activation,
61 %{scopes: ["read:reports"]} when action in [:list_reports, :report_show]
66 %{scopes: ["write:reports"]}
67 when action in [:report_update_state, :report_respond]
72 %{scopes: ["read:statuses"]} when action == :list_user_statuses
77 %{scopes: ["write:statuses"]}
78 when action in [:status_update, :status_delete]
84 when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
90 when action in [:relay_follow, :relay_unfollow, :config_update]
95 action_fallback(:errors)
97 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
98 user = User.get_cached_by_nickname(nickname)
101 ModerationLog.insert_log(%{
111 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
112 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
115 ModerationLog.insert_log(%{
125 def user_follow(%{assigns: %{user: admin}} = conn, %{
126 "follower" => follower_nick,
127 "followed" => followed_nick
129 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
130 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
131 User.follow(follower, followed)
133 ModerationLog.insert_log(%{
145 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
146 "follower" => follower_nick,
147 "followed" => followed_nick
149 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
150 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
151 User.unfollow(follower, followed)
153 ModerationLog.insert_log(%{
165 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
167 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
173 password_confirmation: password,
177 User.register_changeset(%User{}, user_data, need_confirmation: false)
179 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
180 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
183 case Pleroma.Repo.transaction(changesets) do
188 |> Enum.map(fn user ->
189 {:ok, user} = User.post_register_action(user)
193 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
195 ModerationLog.insert_log(%{
197 subjects: Map.values(users),
204 {:error, id, changeset, _} ->
206 Enum.map(changesets.operations, fn
207 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
208 AccountView.render("create-error.json", %{changeset: changeset})
210 {_, {:changeset, current_changeset, _}} ->
211 AccountView.render("create-error.json", %{changeset: current_changeset})
215 |> put_status(:conflict)
220 def user_show(conn, %{"nickname" => nickname}) do
221 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
223 |> put_view(AccountView)
224 |> render("show.json", %{user: user})
226 _ -> {:error, :not_found}
230 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
231 godmode = params["godmode"] == "true" || params["godmode"] == true
233 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
234 {_, page_size} = page_params(params)
237 ActivityPub.fetch_user_activities(user, nil, %{
238 "limit" => page_size,
243 |> put_view(StatusView)
244 |> render("index.json", %{activities: activities, as: :activity})
246 _ -> {:error, :not_found}
250 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
251 user = User.get_cached_by_nickname(nickname)
253 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
255 action = if user.deactivated, do: "activate", else: "deactivate"
257 ModerationLog.insert_log(%{
264 |> put_view(AccountView)
265 |> render("show.json", %{user: updated_user})
268 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
269 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
270 {:ok, updated_users} = User.deactivate(users, false)
272 ModerationLog.insert_log(%{
279 |> put_view(AccountView)
280 |> render("index.json", %{users: Keyword.values(updated_users)})
283 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
284 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
285 {:ok, updated_users} = User.deactivate(users, true)
287 ModerationLog.insert_log(%{
294 |> put_view(AccountView)
295 |> render("index.json", %{users: Keyword.values(updated_users)})
298 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
299 with {:ok, _} <- User.tag(nicknames, tags) do
300 ModerationLog.insert_log(%{
302 nicknames: nicknames,
307 json_response(conn, :no_content, "")
311 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
312 with {:ok, _} <- User.untag(nicknames, tags) do
313 ModerationLog.insert_log(%{
315 nicknames: nicknames,
320 json_response(conn, :no_content, "")
324 def list_users(conn, params) do
325 {page, page_size} = page_params(params)
326 filters = maybe_parse_filters(params["filters"])
329 query: params["query"],
331 page_size: page_size,
332 tags: params["tags"],
333 name: params["name"],
334 email: params["email"]
337 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
338 {:ok, users, count} <- filter_relay_user(users, count),
342 AccountView.render("index.json",
350 defp filter_relay_user(users, count) do
351 filtered_users = Enum.reject(users, &relay_user?/1)
352 count = if Enum.any?(users, &relay_user?/1), do: length(filtered_users), else: count
354 {:ok, filtered_users, count}
357 defp relay_user?(user) do
358 user.ap_id == Relay.relay_ap_id()
361 @filters ~w(local external active deactivated is_admin is_moderator)
363 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
364 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
366 defp maybe_parse_filters(filters) do
369 |> Enum.filter(&Enum.member?(@filters, &1))
370 |> Enum.map(&String.to_atom(&1))
371 |> Enum.into(%{}, &{&1, true})
374 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
375 "permission_group" => permission_group,
376 "nicknames" => nicknames
378 when permission_group in ["moderator", "admin"] do
379 update = %{:"is_#{permission_group}" => true}
381 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
383 for u <- users, do: User.admin_api_update(u, update)
385 ModerationLog.insert_log(%{
389 permission: permission_group
395 def right_add_multiple(conn, _) do
396 render_error(conn, :not_found, "No such permission_group")
399 def right_add(%{assigns: %{user: admin}} = conn, %{
400 "permission_group" => permission_group,
401 "nickname" => nickname
403 when permission_group in ["moderator", "admin"] do
404 fields = %{:"is_#{permission_group}" => true}
408 |> User.get_cached_by_nickname()
409 |> User.admin_api_update(fields)
411 ModerationLog.insert_log(%{
415 permission: permission_group
421 def right_add(conn, _) do
422 render_error(conn, :not_found, "No such permission_group")
425 def right_get(conn, %{"nickname" => nickname}) do
426 user = User.get_cached_by_nickname(nickname)
430 is_moderator: user.is_moderator,
431 is_admin: user.is_admin
435 def right_delete_multiple(
436 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
438 "permission_group" => permission_group,
439 "nicknames" => nicknames
442 when permission_group in ["moderator", "admin"] do
443 with false <- Enum.member?(nicknames, admin_nickname) do
444 update = %{:"is_#{permission_group}" => false}
446 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
448 for u <- users, do: User.admin_api_update(u, update)
450 ModerationLog.insert_log(%{
454 permission: permission_group
459 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
463 def right_delete_multiple(conn, _) do
464 render_error(conn, :not_found, "No such permission_group")
468 %{assigns: %{user: admin}} = conn,
470 "permission_group" => permission_group,
471 "nickname" => nickname
474 when permission_group in ["moderator", "admin"] do
475 fields = %{:"is_#{permission_group}" => false}
479 |> User.get_cached_by_nickname()
480 |> User.admin_api_update(fields)
482 ModerationLog.insert_log(%{
486 permission: permission_group
492 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
493 render_error(conn, :forbidden, "You can't revoke your own admin status.")
496 def relay_list(conn, _params) do
497 with {:ok, list} <- Relay.list() do
498 json(conn, %{relays: list})
506 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
507 with {:ok, _message} <- Relay.follow(target) do
508 ModerationLog.insert_log(%{
509 action: "relay_follow",
523 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
524 with {:ok, _message} <- Relay.unfollow(target) do
525 ModerationLog.insert_log(%{
526 action: "relay_unfollow",
540 @doc "Sends registration invite via email"
541 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
543 Pleroma.Config.get([:instance, :invites_enabled]) &&
544 !Pleroma.Config.get([:instance, :registrations_open]),
545 {:ok, invite_token} <- UserInviteToken.create_invite(),
547 Pleroma.Emails.UserEmail.user_invitation_email(
553 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
554 json_response(conn, :no_content, "")
558 @doc "Create an account registration invite token"
559 def create_invite_token(conn, params) do
563 if params["max_use"],
564 do: Map.put(opts, :max_use, params["max_use"]),
568 if params["expires_at"],
569 do: Map.put(opts, :expires_at, params["expires_at"]),
572 {:ok, invite} = UserInviteToken.create_invite(opts)
574 json(conn, AccountView.render("invite.json", %{invite: invite}))
577 @doc "Get list of created invites"
578 def invites(conn, _params) do
579 invites = UserInviteToken.list_invites()
582 |> put_view(AccountView)
583 |> render("invites.json", %{invites: invites})
586 @doc "Revokes invite by token"
587 def revoke_invite(conn, %{"token" => token}) do
588 with {:ok, invite} <- UserInviteToken.find_by_token(token),
589 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
591 |> put_view(AccountView)
592 |> render("invite.json", %{invite: updated_invite})
594 nil -> {:error, :not_found}
598 @doc "Get a password reset token (base64 string) for given nickname"
599 def get_password_reset(conn, %{"nickname" => nickname}) do
600 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
601 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
606 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
610 @doc "Force password reset for a given user"
611 def force_password_reset(conn, %{"nickname" => nickname}) do
612 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
614 User.force_password_reset_async(user)
616 json_response(conn, :no_content, "")
619 def list_reports(conn, params) do
620 {page, page_size} = page_params(params)
623 |> put_view(ReportView)
624 |> render("index.json", %{reports: Utils.get_reports(params, page, page_size)})
627 def list_grouped_reports(conn, _params) do
629 |> put_view(ReportView)
630 |> render("index_grouped.json", Utils.get_reports_grouped_by_status())
633 def report_show(conn, %{"id" => id}) do
634 with %Activity{} = report <- Activity.get_by_id(id) do
636 |> put_view(ReportView)
637 |> render("show.json", Report.extract_report_info(report))
639 _ -> {:error, :not_found}
643 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
646 |> Enum.map(fn report ->
647 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
648 ModerationLog.insert_log(%{
649 action: "report_update",
656 {:error, message} -> %{id: report["id"], error: message}
660 case Enum.any?(result, &Map.has_key?(&1, :error)) do
661 true -> json_response(conn, :bad_request, result)
662 false -> json_response(conn, :no_content, "")
666 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
667 with false <- is_nil(params["status"]),
668 %Activity{} <- Activity.get_by_id(id) do
671 |> Map.put("in_reply_to_status_id", id)
672 |> Map.put("visibility", "direct")
674 {:ok, activity} = CommonAPI.post(user, params)
676 ModerationLog.insert_log(%{
677 action: "report_response",
680 text: params["status"]
684 |> put_view(StatusView)
685 |> render("show.json", %{activity: activity})
695 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
696 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
697 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
699 ModerationLog.insert_log(%{
700 action: "status_update",
703 sensitive: sensitive,
704 visibility: params["visibility"]
708 |> put_view(StatusView)
709 |> render("show.json", %{activity: activity})
713 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
714 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
715 ModerationLog.insert_log(%{
716 action: "status_delete",
725 def list_log(conn, params) do
726 {page, page_size} = page_params(params)
729 ModerationLog.get_all(%{
731 page_size: page_size,
732 start_date: params["start_date"],
733 end_date: params["end_date"],
734 user_id: params["user_id"],
735 search: params["search"]
739 |> put_view(ModerationLogView)
740 |> render("index.json", %{log: log})
743 def migrate_to_db(conn, _params) do
744 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
748 def migrate_from_db(conn, _params) do
749 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
753 def config_show(conn, _params) do
754 configs = Pleroma.Repo.all(Config)
757 |> put_view(ConfigView)
758 |> render("index.json", %{configs: configs})
761 def config_update(conn, %{"configs" => configs}) do
763 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
766 %{"group" => group, "key" => key, "delete" => "true"} = params ->
767 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
770 %{"group" => group, "key" => key, "value" => value} ->
771 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
774 |> Enum.reject(&is_nil(&1))
776 Pleroma.Config.TransferTask.load_and_update_env()
777 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
784 |> put_view(ConfigView)
785 |> render("index.json", %{configs: updated})
788 def reload_emoji(conn, _params) do
789 Pleroma.Emoji.reload()
794 def errors(conn, {:error, :not_found}) do
796 |> put_status(:not_found)
797 |> json(dgettext("errors", "Not found"))
800 def errors(conn, {:error, reason}) do
802 |> put_status(:bad_request)
806 def errors(conn, {:param_cast, _}) do
808 |> put_status(:bad_request)
809 |> json(dgettext("errors", "Invalid parameters"))
812 def errors(conn, _) do
814 |> put_status(:internal_server_error)
815 |> json(dgettext("errors", "Something went wrong"))
818 defp page_params(params) do
819 {get_page(params["page"]), get_page_size(params["page_size"])}
822 defp get_page(page_string) when is_nil(page_string), do: 1
824 defp get_page(page_string) do
825 case Integer.parse(page_string) do
831 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
833 defp get_page_size(page_size_string) do
834 case Integer.parse(page_size_string) do
835 {page_size, _} -> page_size
836 :error -> @users_page_size