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.AdminAPI.AccountView
15 alias Pleroma.Web.AdminAPI.Config
16 alias Pleroma.Web.AdminAPI.ConfigView
17 alias Pleroma.Web.AdminAPI.ModerationLogView
18 alias Pleroma.Web.AdminAPI.Report
19 alias Pleroma.Web.AdminAPI.ReportView
20 alias Pleroma.Web.AdminAPI.Search
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.Endpoint
23 alias Pleroma.Web.MastodonAPI.StatusView
24 alias Pleroma.Web.Router
26 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
32 %{scopes: ["read:accounts"]}
33 when action in [:list_users, :user_show, :right_get, :invites]
38 %{scopes: ["write:accounts"]}
48 :user_toggle_activation,
53 :set_activation_status
59 %{scopes: ["read:reports"]} when action in [:list_reports, :report_show]
64 %{scopes: ["write:reports"]}
65 when action in [:report_update_state, :report_respond]
70 %{scopes: ["read:statuses"]} when action == :list_user_statuses
75 %{scopes: ["write:statuses"]}
76 when action in [:status_update, :status_delete]
82 when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
88 when action in [:relay_follow, :relay_unfollow, :config_update]
93 action_fallback(:errors)
95 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
96 user = User.get_cached_by_nickname(nickname)
99 ModerationLog.insert_log(%{
109 def user_follow(%{assigns: %{user: admin}} = conn, %{
110 "follower" => follower_nick,
111 "followed" => followed_nick
113 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
114 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
115 User.follow(follower, followed)
117 ModerationLog.insert_log(%{
129 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
130 "follower" => follower_nick,
131 "followed" => followed_nick
133 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
134 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
135 User.unfollow(follower, followed)
137 ModerationLog.insert_log(%{
149 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
151 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
157 password_confirmation: password,
161 User.register_changeset(%User{}, user_data, need_confirmation: false)
163 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
164 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
167 case Pleroma.Repo.transaction(changesets) do
172 |> Enum.map(fn user ->
173 {:ok, user} = User.post_register_action(user)
177 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
179 ModerationLog.insert_log(%{
181 subjects: Map.values(users),
188 {:error, id, changeset, _} ->
190 Enum.map(changesets.operations, fn
191 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
192 AccountView.render("create-error.json", %{changeset: changeset})
194 {_, {:changeset, current_changeset, _}} ->
195 AccountView.render("create-error.json", %{changeset: current_changeset})
199 |> put_status(:conflict)
204 def user_show(conn, %{"nickname" => nickname}) do
205 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
207 |> put_view(AccountView)
208 |> render("show.json", %{user: user})
210 _ -> {:error, :not_found}
214 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
215 godmode = params["godmode"] == "true" || params["godmode"] == true
217 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
218 {_, page_size} = page_params(params)
221 ActivityPub.fetch_user_activities(user, nil, %{
222 "limit" => page_size,
227 |> put_view(StatusView)
228 |> render("index.json", %{activities: activities, as: :activity})
230 _ -> {:error, :not_found}
234 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
235 user = User.get_cached_by_nickname(nickname)
237 {:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
239 action = if user.info.deactivated, do: "activate", else: "deactivate"
241 ModerationLog.insert_log(%{
248 |> put_view(AccountView)
249 |> render("show.json", %{user: updated_user})
252 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
253 with {:ok, _} <- User.tag(nicknames, tags) do
254 ModerationLog.insert_log(%{
256 nicknames: nicknames,
261 json_response(conn, :no_content, "")
265 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
266 with {:ok, _} <- User.untag(nicknames, tags) do
267 ModerationLog.insert_log(%{
269 nicknames: nicknames,
274 json_response(conn, :no_content, "")
278 def list_users(conn, params) do
279 {page, page_size} = page_params(params)
280 filters = maybe_parse_filters(params["filters"])
283 query: params["query"],
285 page_size: page_size,
286 tags: params["tags"],
287 name: params["name"],
288 email: params["email"]
291 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
295 AccountView.render("index.json",
303 @filters ~w(local external active deactivated is_admin is_moderator)
305 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
306 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
308 defp maybe_parse_filters(filters) do
311 |> Enum.filter(&Enum.member?(@filters, &1))
312 |> Enum.map(&String.to_atom(&1))
313 |> Enum.into(%{}, &{&1, true})
316 def right_add(%{assigns: %{user: admin}} = conn, %{
317 "permission_group" => permission_group,
318 "nickname" => nickname
320 when permission_group in ["moderator", "admin"] do
321 info = Map.put(%{}, "is_" <> permission_group, true)
325 |> User.get_cached_by_nickname()
326 |> User.update_info(&User.Info.admin_api_update(&1, info))
328 ModerationLog.insert_log(%{
332 permission: permission_group
338 def right_add(conn, _) do
339 render_error(conn, :not_found, "No such permission_group")
342 def right_get(conn, %{"nickname" => nickname}) do
343 user = User.get_cached_by_nickname(nickname)
347 is_moderator: user.info.is_moderator,
348 is_admin: user.info.is_admin
352 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
353 render_error(conn, :forbidden, "You can't revoke your own admin status.")
357 %{assigns: %{user: admin}} = conn,
359 "permission_group" => permission_group,
360 "nickname" => nickname
363 when permission_group in ["moderator", "admin"] do
364 info = Map.put(%{}, "is_" <> permission_group, false)
368 |> User.get_cached_by_nickname()
369 |> User.update_info(&User.Info.admin_api_update(&1, info))
371 ModerationLog.insert_log(%{
375 permission: permission_group
381 def right_delete(conn, _) do
382 render_error(conn, :not_found, "No such permission_group")
385 def set_activation_status(%{assigns: %{user: admin}} = conn, %{
386 "nickname" => nickname,
389 with {:ok, status} <- Ecto.Type.cast(:boolean, status),
390 %User{} = user <- User.get_cached_by_nickname(nickname),
391 {:ok, _} <- User.deactivate(user, !status) do
392 action = if(user.info.deactivated, do: "activate", else: "deactivate")
394 ModerationLog.insert_log(%{
400 json_response(conn, :no_content, "")
404 def relay_list(conn, _params) do
405 with {:ok, list} <- Relay.list() do
406 json(conn, %{relays: list})
414 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
415 with {:ok, _message} <- Relay.follow(target) do
416 ModerationLog.insert_log(%{
417 action: "relay_follow",
431 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
432 with {:ok, _message} <- Relay.unfollow(target) do
433 ModerationLog.insert_log(%{
434 action: "relay_unfollow",
448 @doc "Sends registration invite via email"
449 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
451 Pleroma.Config.get([:instance, :invites_enabled]) &&
452 !Pleroma.Config.get([:instance, :registrations_open]),
453 {:ok, invite_token} <- UserInviteToken.create_invite(),
455 Pleroma.Emails.UserEmail.user_invitation_email(
461 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
462 json_response(conn, :no_content, "")
466 @doc "Create an account registration invite token"
467 def create_invite_token(conn, params) do
471 if params["max_use"],
472 do: Map.put(opts, :max_use, params["max_use"]),
476 if params["expires_at"],
477 do: Map.put(opts, :expires_at, params["expires_at"]),
480 {:ok, invite} = UserInviteToken.create_invite(opts)
482 json(conn, AccountView.render("invite.json", %{invite: invite}))
485 @doc "Get list of created invites"
486 def invites(conn, _params) do
487 invites = UserInviteToken.list_invites()
490 |> put_view(AccountView)
491 |> render("invites.json", %{invites: invites})
494 @doc "Revokes invite by token"
495 def revoke_invite(conn, %{"token" => token}) do
496 with {:ok, invite} <- UserInviteToken.find_by_token(token),
497 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
499 |> put_view(AccountView)
500 |> render("invite.json", %{invite: updated_invite})
502 nil -> {:error, :not_found}
506 @doc "Get a password reset token (base64 string) for given nickname"
507 def get_password_reset(conn, %{"nickname" => nickname}) do
508 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
509 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
514 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
518 @doc "Force password reset for a given user"
519 def force_password_reset(conn, %{"nickname" => nickname}) do
520 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
522 User.force_password_reset_async(user)
524 json_response(conn, :no_content, "")
527 def list_reports(conn, params) do
528 {page, page_size} = page_params(params)
532 |> Map.put("type", "Flag")
533 |> Map.put("skip_preload", true)
534 |> Map.put("total", true)
535 |> Map.put("limit", page_size)
536 |> Map.put("offset", (page - 1) * page_size)
538 reports = ActivityPub.fetch_activities([], params, :offset)
541 |> put_view(ReportView)
542 |> render("index.json", %{reports: reports})
545 def report_show(conn, %{"id" => id}) do
546 with %Activity{} = report <- Activity.get_by_id(id) do
548 |> put_view(ReportView)
549 |> render("show.json", Report.extract_report_info(report))
551 _ -> {:error, :not_found}
555 def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
556 with {:ok, report} <- CommonAPI.update_report_state(id, state) do
557 ModerationLog.insert_log(%{
558 action: "report_update",
564 |> put_view(ReportView)
565 |> render("show.json", Report.extract_report_info(report))
569 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
570 with false <- is_nil(params["status"]),
571 %Activity{} <- Activity.get_by_id(id) do
574 |> Map.put("in_reply_to_status_id", id)
575 |> Map.put("visibility", "direct")
577 {:ok, activity} = CommonAPI.post(user, params)
579 ModerationLog.insert_log(%{
580 action: "report_response",
583 text: params["status"]
587 |> put_view(StatusView)
588 |> render("show.json", %{activity: activity})
598 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
599 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
600 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
602 ModerationLog.insert_log(%{
603 action: "status_update",
606 sensitive: sensitive,
607 visibility: params["visibility"]
611 |> put_view(StatusView)
612 |> render("show.json", %{activity: activity})
616 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
617 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
618 ModerationLog.insert_log(%{
619 action: "status_delete",
628 def list_log(conn, params) do
629 {page, page_size} = page_params(params)
632 ModerationLog.get_all(%{
634 page_size: page_size,
635 start_date: params["start_date"],
636 end_date: params["end_date"],
637 user_id: params["user_id"],
638 search: params["search"]
642 |> put_view(ModerationLogView)
643 |> render("index.json", %{log: log})
646 def migrate_to_db(conn, _params) do
647 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
651 def migrate_from_db(conn, _params) do
652 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
656 def config_show(conn, _params) do
657 configs = Pleroma.Repo.all(Config)
660 |> put_view(ConfigView)
661 |> render("index.json", %{configs: configs})
664 def config_update(conn, %{"configs" => configs}) do
666 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
669 %{"group" => group, "key" => key, "delete" => "true"} = params ->
670 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
673 %{"group" => group, "key" => key, "value" => value} ->
674 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
677 |> Enum.reject(&is_nil(&1))
679 Pleroma.Config.TransferTask.load_and_update_env()
680 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
687 |> put_view(ConfigView)
688 |> render("index.json", %{configs: updated})
691 def reload_emoji(conn, _params) do
692 Pleroma.Emoji.reload()
697 def errors(conn, {:error, :not_found}) do
699 |> put_status(:not_found)
700 |> json(dgettext("errors", "Not found"))
703 def errors(conn, {:error, reason}) do
705 |> put_status(:bad_request)
709 def errors(conn, {:param_cast, _}) do
711 |> put_status(:bad_request)
712 |> json(dgettext("errors", "Invalid parameters"))
715 def errors(conn, _) do
717 |> put_status(:internal_server_error)
718 |> json(dgettext("errors", "Something went wrong"))
721 defp page_params(params) do
722 {get_page(params["page"]), get_page_size(params["page_size"])}
725 defp get_page(page_string) when is_nil(page_string), do: 1
727 defp get_page(page_string) do
728 case Integer.parse(page_string) do
734 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
736 defp get_page_size(page_size_string) do
737 case Integer.parse(page_size_string) do
738 {page_size, _} -> page_size
739 :error -> @users_page_size