Merge branch 'develop' into feature/moderation-log-filters
[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 report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
484 with {:ok, report} <- CommonAPI.update_report_state(id, state) do
485 ModerationLog.insert_log(%{
486 action: "report_update",
487 actor: admin,
488 subject: report
489 })
490
491 conn
492 |> put_view(ReportView)
493 |> render("show.json", Report.extract_report_info(report))
494 end
495 end
496
497 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
498 with false <- is_nil(params["status"]),
499 %Activity{} <- Activity.get_by_id(id) do
500 params =
501 params
502 |> Map.put("in_reply_to_status_id", id)
503 |> Map.put("visibility", "direct")
504
505 {:ok, activity} = CommonAPI.post(user, params)
506
507 ModerationLog.insert_log(%{
508 action: "report_response",
509 actor: user,
510 subject: activity,
511 text: params["status"]
512 })
513
514 conn
515 |> put_view(StatusView)
516 |> render("status.json", %{activity: activity})
517 else
518 true ->
519 {:param_cast, nil}
520
521 nil ->
522 {:error, :not_found}
523 end
524 end
525
526 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
527 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
528 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
529
530 ModerationLog.insert_log(%{
531 action: "status_update",
532 actor: admin,
533 subject: activity,
534 sensitive: sensitive,
535 visibility: params["visibility"]
536 })
537
538 conn
539 |> put_view(StatusView)
540 |> render("status.json", %{activity: activity})
541 end
542 end
543
544 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
545 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
546 ModerationLog.insert_log(%{
547 action: "status_delete",
548 actor: user,
549 subject_id: id
550 })
551
552 json(conn, %{})
553 end
554 end
555
556 def list_log(conn, params) do
557 {page, page_size} = page_params(params)
558
559 log =
560 ModerationLog.get_all(%{
561 page: page,
562 page_size: page_size,
563 start_date: params["start_date"],
564 end_date: params["end_date"],
565 user_id: params["user_id"],
566 search: params["search"]
567 })
568
569 conn
570 |> put_view(ModerationLogView)
571 |> render("index.json", %{log: log})
572 end
573
574 def migrate_to_db(conn, _params) do
575 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
576 json(conn, %{})
577 end
578
579 def migrate_from_db(conn, _params) do
580 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
581 json(conn, %{})
582 end
583
584 def config_show(conn, _params) do
585 configs = Pleroma.Repo.all(Config)
586
587 conn
588 |> put_view(ConfigView)
589 |> render("index.json", %{configs: configs})
590 end
591
592 def config_update(conn, %{"configs" => configs}) do
593 updated =
594 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
595 updated =
596 Enum.map(configs, fn
597 %{"group" => group, "key" => key, "delete" => "true"} = params ->
598 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
599 config
600
601 %{"group" => group, "key" => key, "value" => value} ->
602 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
603 config
604 end)
605 |> Enum.reject(&is_nil(&1))
606
607 Pleroma.Config.TransferTask.load_and_update_env()
608 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
609 updated
610 else
611 []
612 end
613
614 conn
615 |> put_view(ConfigView)
616 |> render("index.json", %{configs: updated})
617 end
618
619 def reload_emoji(conn, _params) do
620 Pleroma.Emoji.reload()
621
622 conn |> json("ok")
623 end
624
625 def errors(conn, {:error, :not_found}) do
626 conn
627 |> put_status(:not_found)
628 |> json(dgettext("errors", "Not found"))
629 end
630
631 def errors(conn, {:error, reason}) do
632 conn
633 |> put_status(:bad_request)
634 |> json(reason)
635 end
636
637 def errors(conn, {:param_cast, _}) do
638 conn
639 |> put_status(:bad_request)
640 |> json(dgettext("errors", "Invalid parameters"))
641 end
642
643 def errors(conn, _) do
644 conn
645 |> put_status(:internal_server_error)
646 |> json(dgettext("errors", "Something went wrong"))
647 end
648
649 defp page_params(params) do
650 {get_page(params["page"]), get_page_size(params["page_size"])}
651 end
652
653 defp get_page(page_string) when is_nil(page_string), do: 1
654
655 defp get_page(page_string) do
656 case Integer.parse(page_string) do
657 {page, _} -> page
658 :error -> 1
659 end
660 end
661
662 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
663
664 defp get_page_size(page_size_string) do
665 case Integer.parse(page_size_string) do
666 {page_size, _} -> page_size
667 :error -> @users_page_size
668 end
669 end
670 end