Admin API: Allow changing the state of multiple reports at once
[akkoma] / lib / pleroma / web / admin_api / admin_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.AdminAPI.AdminAPIController do
6 use Pleroma.Web, :controller
7 alias Pleroma.Activity
8 alias Pleroma.ModerationLog
9 alias Pleroma.User
10 alias Pleroma.UserInviteToken
11 alias Pleroma.Web.ActivityPub.ActivityPub
12 alias Pleroma.Web.ActivityPub.Relay
13 alias Pleroma.Web.AdminAPI.AccountView
14 alias Pleroma.Web.AdminAPI.Config
15 alias Pleroma.Web.AdminAPI.ConfigView
16 alias Pleroma.Web.AdminAPI.ModerationLogView
17 alias Pleroma.Web.AdminAPI.Report
18 alias Pleroma.Web.AdminAPI.ReportView
19 alias Pleroma.Web.AdminAPI.Search
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.Endpoint
22 alias Pleroma.Web.MastodonAPI.StatusView
23 alias Pleroma.Web.Router
24
25 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
26
27 require Logger
28
29 @users_page_size 50
30
31 action_fallback(:errors)
32
33 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
34 user = User.get_cached_by_nickname(nickname)
35 User.delete(user)
36
37 ModerationLog.insert_log(%{
38 actor: admin,
39 subject: user,
40 action: "delete"
41 })
42
43 conn
44 |> json(nickname)
45 end
46
47 def user_follow(%{assigns: %{user: admin}} = conn, %{
48 "follower" => follower_nick,
49 "followed" => followed_nick
50 }) do
51 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
52 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
53 User.follow(follower, followed)
54
55 ModerationLog.insert_log(%{
56 actor: admin,
57 followed: followed,
58 follower: follower,
59 action: "follow"
60 })
61 end
62
63 conn
64 |> json("ok")
65 end
66
67 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
68 "follower" => follower_nick,
69 "followed" => followed_nick
70 }) do
71 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
72 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
73 User.unfollow(follower, followed)
74
75 ModerationLog.insert_log(%{
76 actor: admin,
77 followed: followed,
78 follower: follower,
79 action: "unfollow"
80 })
81 end
82
83 conn
84 |> json("ok")
85 end
86
87 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
88 changesets =
89 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
90 user_data = %{
91 nickname: nickname,
92 name: nickname,
93 email: email,
94 password: password,
95 password_confirmation: password,
96 bio: "."
97 }
98
99 User.register_changeset(%User{}, user_data, need_confirmation: false)
100 end)
101 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
102 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
103 end)
104
105 case Pleroma.Repo.transaction(changesets) do
106 {:ok, users} ->
107 res =
108 users
109 |> Map.values()
110 |> Enum.map(fn user ->
111 {:ok, user} = User.post_register_action(user)
112
113 user
114 end)
115 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
116
117 ModerationLog.insert_log(%{
118 actor: admin,
119 subjects: Map.values(users),
120 action: "create"
121 })
122
123 conn
124 |> json(res)
125
126 {:error, id, changeset, _} ->
127 res =
128 Enum.map(changesets.operations, fn
129 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
130 AccountView.render("create-error.json", %{changeset: changeset})
131
132 {_, {:changeset, current_changeset, _}} ->
133 AccountView.render("create-error.json", %{changeset: current_changeset})
134 end)
135
136 conn
137 |> put_status(:conflict)
138 |> json(res)
139 end
140 end
141
142 def user_show(conn, %{"nickname" => nickname}) do
143 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
144 conn
145 |> put_view(AccountView)
146 |> render("show.json", %{user: user})
147 else
148 _ -> {:error, :not_found}
149 end
150 end
151
152 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
153 godmode = params["godmode"] == "true" || params["godmode"] == true
154
155 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
156 {_, page_size} = page_params(params)
157
158 activities =
159 ActivityPub.fetch_user_activities(user, nil, %{
160 "limit" => page_size,
161 "godmode" => godmode
162 })
163
164 conn
165 |> put_view(StatusView)
166 |> render("index.json", %{activities: activities, as: :activity})
167 else
168 _ -> {:error, :not_found}
169 end
170 end
171
172 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
173 user = User.get_cached_by_nickname(nickname)
174
175 {:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
176
177 action = if user.info.deactivated, do: "activate", else: "deactivate"
178
179 ModerationLog.insert_log(%{
180 actor: admin,
181 subject: user,
182 action: action
183 })
184
185 conn
186 |> put_view(AccountView)
187 |> render("show.json", %{user: updated_user})
188 end
189
190 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
191 with {:ok, _} <- User.tag(nicknames, tags) do
192 ModerationLog.insert_log(%{
193 actor: admin,
194 nicknames: nicknames,
195 tags: tags,
196 action: "tag"
197 })
198
199 json_response(conn, :no_content, "")
200 end
201 end
202
203 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
204 with {:ok, _} <- User.untag(nicknames, tags) do
205 ModerationLog.insert_log(%{
206 actor: admin,
207 nicknames: nicknames,
208 tags: tags,
209 action: "untag"
210 })
211
212 json_response(conn, :no_content, "")
213 end
214 end
215
216 def list_users(conn, params) do
217 {page, page_size} = page_params(params)
218 filters = maybe_parse_filters(params["filters"])
219
220 search_params = %{
221 query: params["query"],
222 page: page,
223 page_size: page_size,
224 tags: params["tags"],
225 name: params["name"],
226 email: params["email"]
227 }
228
229 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
230 do:
231 conn
232 |> json(
233 AccountView.render("index.json",
234 users: users,
235 count: count,
236 page_size: page_size
237 )
238 )
239 end
240
241 @filters ~w(local external active deactivated is_admin is_moderator)
242
243 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
244 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
245
246 defp maybe_parse_filters(filters) do
247 filters
248 |> String.split(",")
249 |> Enum.filter(&Enum.member?(@filters, &1))
250 |> Enum.map(&String.to_atom(&1))
251 |> Enum.into(%{}, &{&1, true})
252 end
253
254 def right_add(%{assigns: %{user: admin}} = conn, %{
255 "permission_group" => permission_group,
256 "nickname" => nickname
257 })
258 when permission_group in ["moderator", "admin"] do
259 info = Map.put(%{}, "is_" <> permission_group, true)
260
261 {:ok, user} =
262 nickname
263 |> User.get_cached_by_nickname()
264 |> User.update_info(&User.Info.admin_api_update(&1, info))
265
266 ModerationLog.insert_log(%{
267 action: "grant",
268 actor: admin,
269 subject: user,
270 permission: permission_group
271 })
272
273 json(conn, info)
274 end
275
276 def right_add(conn, _) do
277 render_error(conn, :not_found, "No such permission_group")
278 end
279
280 def right_get(conn, %{"nickname" => nickname}) do
281 user = User.get_cached_by_nickname(nickname)
282
283 conn
284 |> json(%{
285 is_moderator: user.info.is_moderator,
286 is_admin: user.info.is_admin
287 })
288 end
289
290 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
291 render_error(conn, :forbidden, "You can't revoke your own admin status.")
292 end
293
294 def right_delete(
295 %{assigns: %{user: admin}} = conn,
296 %{
297 "permission_group" => permission_group,
298 "nickname" => nickname
299 }
300 )
301 when permission_group in ["moderator", "admin"] do
302 info = Map.put(%{}, "is_" <> permission_group, false)
303
304 {:ok, user} =
305 nickname
306 |> User.get_cached_by_nickname()
307 |> User.update_info(&User.Info.admin_api_update(&1, info))
308
309 ModerationLog.insert_log(%{
310 action: "revoke",
311 actor: admin,
312 subject: user,
313 permission: permission_group
314 })
315
316 json(conn, info)
317 end
318
319 def right_delete(conn, _) do
320 render_error(conn, :not_found, "No such permission_group")
321 end
322
323 def set_activation_status(%{assigns: %{user: admin}} = conn, %{
324 "nickname" => nickname,
325 "status" => status
326 }) do
327 with {:ok, status} <- Ecto.Type.cast(:boolean, status),
328 %User{} = user <- User.get_cached_by_nickname(nickname),
329 {:ok, _} <- User.deactivate(user, !status) do
330 action = if(user.info.deactivated, do: "activate", else: "deactivate")
331
332 ModerationLog.insert_log(%{
333 actor: admin,
334 subject: user,
335 action: action
336 })
337
338 json_response(conn, :no_content, "")
339 end
340 end
341
342 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
343 with {:ok, _message} <- Relay.follow(target) do
344 ModerationLog.insert_log(%{
345 action: "relay_follow",
346 actor: admin,
347 target: target
348 })
349
350 json(conn, target)
351 else
352 _ ->
353 conn
354 |> put_status(500)
355 |> json(target)
356 end
357 end
358
359 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
360 with {:ok, _message} <- Relay.unfollow(target) do
361 ModerationLog.insert_log(%{
362 action: "relay_unfollow",
363 actor: admin,
364 target: target
365 })
366
367 json(conn, target)
368 else
369 _ ->
370 conn
371 |> put_status(500)
372 |> json(target)
373 end
374 end
375
376 @doc "Sends registration invite via email"
377 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
378 with true <-
379 Pleroma.Config.get([:instance, :invites_enabled]) &&
380 !Pleroma.Config.get([:instance, :registrations_open]),
381 {:ok, invite_token} <- UserInviteToken.create_invite(),
382 email <-
383 Pleroma.Emails.UserEmail.user_invitation_email(
384 user,
385 invite_token,
386 email,
387 params["name"]
388 ),
389 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
390 json_response(conn, :no_content, "")
391 end
392 end
393
394 @doc "Create an account registration invite token"
395 def create_invite_token(conn, params) do
396 opts = %{}
397
398 opts =
399 if params["max_use"],
400 do: Map.put(opts, :max_use, params["max_use"]),
401 else: opts
402
403 opts =
404 if params["expires_at"],
405 do: Map.put(opts, :expires_at, params["expires_at"]),
406 else: opts
407
408 {:ok, invite} = UserInviteToken.create_invite(opts)
409
410 json(conn, AccountView.render("invite.json", %{invite: invite}))
411 end
412
413 @doc "Get list of created invites"
414 def invites(conn, _params) do
415 invites = UserInviteToken.list_invites()
416
417 conn
418 |> put_view(AccountView)
419 |> render("invites.json", %{invites: invites})
420 end
421
422 @doc "Revokes invite by token"
423 def revoke_invite(conn, %{"token" => token}) do
424 with {:ok, invite} <- UserInviteToken.find_by_token(token),
425 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
426 conn
427 |> put_view(AccountView)
428 |> render("invite.json", %{invite: updated_invite})
429 else
430 nil -> {:error, :not_found}
431 end
432 end
433
434 @doc "Get a password reset token (base64 string) for given nickname"
435 def get_password_reset(conn, %{"nickname" => nickname}) do
436 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
437 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
438
439 conn
440 |> json(%{
441 token: token.token,
442 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
443 })
444 end
445
446 @doc "Force password reset for a given user"
447 def force_password_reset(conn, %{"nickname" => nickname}) do
448 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
449
450 User.force_password_reset_async(user)
451
452 json_response(conn, :no_content, "")
453 end
454
455 def list_reports(conn, params) do
456 {page, page_size} = page_params(params)
457
458 params =
459 params
460 |> Map.put("type", "Flag")
461 |> Map.put("skip_preload", true)
462 |> Map.put("total", true)
463 |> Map.put("limit", page_size)
464 |> Map.put("offset", (page - 1) * page_size)
465
466 reports = ActivityPub.fetch_activities([], params, :offset)
467
468 conn
469 |> put_view(ReportView)
470 |> render("index.json", %{reports: reports})
471 end
472
473 def report_show(conn, %{"id" => id}) do
474 with %Activity{} = report <- Activity.get_by_id(id) do
475 conn
476 |> put_view(ReportView)
477 |> render("show.json", Report.extract_report_info(report))
478 else
479 _ -> {:error, :not_found}
480 end
481 end
482
483 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
484 result =
485 reports
486 |> Enum.map(fn report ->
487 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
488 ModerationLog.insert_log(%{
489 action: "report_update",
490 actor: admin,
491 subject: activity
492 })
493
494 activity
495 else
496 {:error, message} -> %{id: report["id"], error: message}
497 end
498 end)
499
500 case Enum.any?(result, &Map.has_key?(&1, :error)) do
501 true -> json_response(conn, :bad_request, result)
502 false -> json_response(conn, :no_content, "")
503 end
504 end
505
506 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
507 with false <- is_nil(params["status"]),
508 %Activity{} <- Activity.get_by_id(id) do
509 params =
510 params
511 |> Map.put("in_reply_to_status_id", id)
512 |> Map.put("visibility", "direct")
513
514 {:ok, activity} = CommonAPI.post(user, params)
515
516 ModerationLog.insert_log(%{
517 action: "report_response",
518 actor: user,
519 subject: activity,
520 text: params["status"]
521 })
522
523 conn
524 |> put_view(StatusView)
525 |> render("show.json", %{activity: activity})
526 else
527 true ->
528 {:param_cast, nil}
529
530 nil ->
531 {:error, :not_found}
532 end
533 end
534
535 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
536 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
537 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
538
539 ModerationLog.insert_log(%{
540 action: "status_update",
541 actor: admin,
542 subject: activity,
543 sensitive: sensitive,
544 visibility: params["visibility"]
545 })
546
547 conn
548 |> put_view(StatusView)
549 |> render("show.json", %{activity: activity})
550 end
551 end
552
553 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
554 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
555 ModerationLog.insert_log(%{
556 action: "status_delete",
557 actor: user,
558 subject_id: id
559 })
560
561 json(conn, %{})
562 end
563 end
564
565 def list_log(conn, params) do
566 {page, page_size} = page_params(params)
567
568 log =
569 ModerationLog.get_all(%{
570 page: page,
571 page_size: page_size,
572 start_date: params["start_date"],
573 end_date: params["end_date"],
574 user_id: params["user_id"],
575 search: params["search"]
576 })
577
578 conn
579 |> put_view(ModerationLogView)
580 |> render("index.json", %{log: log})
581 end
582
583 def migrate_to_db(conn, _params) do
584 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
585 json(conn, %{})
586 end
587
588 def migrate_from_db(conn, _params) do
589 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
590 json(conn, %{})
591 end
592
593 def config_show(conn, _params) do
594 configs = Pleroma.Repo.all(Config)
595
596 conn
597 |> put_view(ConfigView)
598 |> render("index.json", %{configs: configs})
599 end
600
601 def config_update(conn, %{"configs" => configs}) do
602 updated =
603 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
604 updated =
605 Enum.map(configs, fn
606 %{"group" => group, "key" => key, "delete" => "true"} = params ->
607 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
608 config
609
610 %{"group" => group, "key" => key, "value" => value} ->
611 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
612 config
613 end)
614 |> Enum.reject(&is_nil(&1))
615
616 Pleroma.Config.TransferTask.load_and_update_env()
617 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
618 updated
619 else
620 []
621 end
622
623 conn
624 |> put_view(ConfigView)
625 |> render("index.json", %{configs: updated})
626 end
627
628 def reload_emoji(conn, _params) do
629 Pleroma.Emoji.reload()
630
631 conn |> json("ok")
632 end
633
634 def errors(conn, {:error, :not_found}) do
635 conn
636 |> put_status(:not_found)
637 |> json(dgettext("errors", "Not found"))
638 end
639
640 def errors(conn, {:error, reason}) do
641 conn
642 |> put_status(:bad_request)
643 |> json(reason)
644 end
645
646 def errors(conn, {:param_cast, _}) do
647 conn
648 |> put_status(:bad_request)
649 |> json(dgettext("errors", "Invalid parameters"))
650 end
651
652 def errors(conn, _) do
653 conn
654 |> put_status(:internal_server_error)
655 |> json(dgettext("errors", "Something went wrong"))
656 end
657
658 defp page_params(params) do
659 {get_page(params["page"]), get_page_size(params["page_size"])}
660 end
661
662 defp get_page(page_string) when is_nil(page_string), do: 1
663
664 defp get_page(page_string) do
665 case Integer.parse(page_string) do
666 {page, _} -> page
667 :error -> 1
668 end
669 end
670
671 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
672
673 defp get_page_size(page_size_string) do
674 case Integer.parse(page_size_string) do
675 {page_size, _} -> page_size
676 :error -> @users_page_size
677 end
678 end
679 end