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_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
405 with {:ok, _message} <- Relay.follow(target) do
406 ModerationLog.insert_log(%{
407 action: "relay_follow",
421 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
422 with {:ok, _message} <- Relay.unfollow(target) do
423 ModerationLog.insert_log(%{
424 action: "relay_unfollow",
438 @doc "Sends registration invite via email"
439 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
441 Pleroma.Config.get([:instance, :invites_enabled]) &&
442 !Pleroma.Config.get([:instance, :registrations_open]),
443 {:ok, invite_token} <- UserInviteToken.create_invite(),
445 Pleroma.Emails.UserEmail.user_invitation_email(
451 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
452 json_response(conn, :no_content, "")
456 @doc "Create an account registration invite token"
457 def create_invite_token(conn, params) do
461 if params["max_use"],
462 do: Map.put(opts, :max_use, params["max_use"]),
466 if params["expires_at"],
467 do: Map.put(opts, :expires_at, params["expires_at"]),
470 {:ok, invite} = UserInviteToken.create_invite(opts)
472 json(conn, AccountView.render("invite.json", %{invite: invite}))
475 @doc "Get list of created invites"
476 def invites(conn, _params) do
477 invites = UserInviteToken.list_invites()
480 |> put_view(AccountView)
481 |> render("invites.json", %{invites: invites})
484 @doc "Revokes invite by token"
485 def revoke_invite(conn, %{"token" => token}) do
486 with {:ok, invite} <- UserInviteToken.find_by_token(token),
487 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
489 |> put_view(AccountView)
490 |> render("invite.json", %{invite: updated_invite})
492 nil -> {:error, :not_found}
496 @doc "Get a password reset token (base64 string) for given nickname"
497 def get_password_reset(conn, %{"nickname" => nickname}) do
498 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
499 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
504 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
508 @doc "Force password reset for a given user"
509 def force_password_reset(conn, %{"nickname" => nickname}) do
510 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
512 User.force_password_reset_async(user)
514 json_response(conn, :no_content, "")
517 def list_reports(conn, params) do
518 {page, page_size} = page_params(params)
522 |> Map.put("type", "Flag")
523 |> Map.put("skip_preload", true)
524 |> Map.put("total", true)
525 |> Map.put("limit", page_size)
526 |> Map.put("offset", (page - 1) * page_size)
528 reports = ActivityPub.fetch_activities([], params, :offset)
531 |> put_view(ReportView)
532 |> render("index.json", %{reports: reports})
535 def report_show(conn, %{"id" => id}) do
536 with %Activity{} = report <- Activity.get_by_id(id) do
538 |> put_view(ReportView)
539 |> render("show.json", Report.extract_report_info(report))
541 _ -> {:error, :not_found}
545 def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
546 with {:ok, report} <- CommonAPI.update_report_state(id, state) do
547 ModerationLog.insert_log(%{
548 action: "report_update",
554 |> put_view(ReportView)
555 |> render("show.json", Report.extract_report_info(report))
559 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
560 with false <- is_nil(params["status"]),
561 %Activity{} <- Activity.get_by_id(id) do
564 |> Map.put("in_reply_to_status_id", id)
565 |> Map.put("visibility", "direct")
567 {:ok, activity} = CommonAPI.post(user, params)
569 ModerationLog.insert_log(%{
570 action: "report_response",
573 text: params["status"]
577 |> put_view(StatusView)
578 |> render("show.json", %{activity: activity})
588 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
589 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
590 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
592 ModerationLog.insert_log(%{
593 action: "status_update",
596 sensitive: sensitive,
597 visibility: params["visibility"]
601 |> put_view(StatusView)
602 |> render("show.json", %{activity: activity})
606 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
607 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
608 ModerationLog.insert_log(%{
609 action: "status_delete",
618 def list_log(conn, params) do
619 {page, page_size} = page_params(params)
622 ModerationLog.get_all(%{
624 page_size: page_size,
625 start_date: params["start_date"],
626 end_date: params["end_date"],
627 user_id: params["user_id"],
628 search: params["search"]
632 |> put_view(ModerationLogView)
633 |> render("index.json", %{log: log})
636 def migrate_to_db(conn, _params) do
637 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
641 def migrate_from_db(conn, _params) do
642 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
646 def config_show(conn, _params) do
647 configs = Pleroma.Repo.all(Config)
650 |> put_view(ConfigView)
651 |> render("index.json", %{configs: configs})
654 def config_update(conn, %{"configs" => configs}) do
656 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
659 %{"group" => group, "key" => key, "delete" => "true"} = params ->
660 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
663 %{"group" => group, "key" => key, "value" => value} ->
664 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
667 |> Enum.reject(&is_nil(&1))
669 Pleroma.Config.TransferTask.load_and_update_env()
670 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
677 |> put_view(ConfigView)
678 |> render("index.json", %{configs: updated})
681 def reload_emoji(conn, _params) do
682 Pleroma.Emoji.reload()
687 def errors(conn, {:error, :not_found}) do
689 |> put_status(:not_found)
690 |> json(dgettext("errors", "Not found"))
693 def errors(conn, {:error, reason}) do
695 |> put_status(:bad_request)
699 def errors(conn, {:param_cast, _}) do
701 |> put_status(:bad_request)
702 |> json(dgettext("errors", "Invalid parameters"))
705 def errors(conn, _) do
707 |> put_status(:internal_server_error)
708 |> json(dgettext("errors", "Something went wrong"))
711 defp page_params(params) do
712 {get_page(params["page"]), get_page_size(params["page_size"])}
715 defp get_page(page_string) when is_nil(page_string), do: 1
717 defp get_page(page_string) do
718 case Integer.parse(page_string) do
724 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
726 defp get_page_size(page_size_string) do
727 case Integer.parse(page_size_string) do
728 {page_size, _} -> page_size
729 :error -> @users_page_size