Merge branch 'develop' into feature/return-link-for-password-reset
[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 user = User.get_cached_by_nickname(nickname)
260
261 info =
262 %{}
263 |> Map.put("is_" <> permission_group, true)
264
265 info_cng = User.Info.admin_api_update(user.info, info)
266
267 cng =
268 user
269 |> Ecto.Changeset.change()
270 |> Ecto.Changeset.put_embed(:info, info_cng)
271
272 ModerationLog.insert_log(%{
273 action: "grant",
274 actor: admin,
275 subject: user,
276 permission: permission_group
277 })
278
279 {:ok, _user} = User.update_and_set_cache(cng)
280
281 json(conn, info)
282 end
283
284 def right_add(conn, _) do
285 render_error(conn, :not_found, "No such permission_group")
286 end
287
288 def right_get(conn, %{"nickname" => nickname}) do
289 user = User.get_cached_by_nickname(nickname)
290
291 conn
292 |> json(%{
293 is_moderator: user.info.is_moderator,
294 is_admin: user.info.is_admin
295 })
296 end
297
298 def right_delete(
299 %{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn,
300 %{
301 "permission_group" => permission_group,
302 "nickname" => nickname
303 }
304 )
305 when permission_group in ["moderator", "admin"] do
306 if admin_nickname == nickname do
307 render_error(conn, :forbidden, "You can't revoke your own admin status.")
308 else
309 user = User.get_cached_by_nickname(nickname)
310
311 info =
312 %{}
313 |> Map.put("is_" <> permission_group, false)
314
315 info_cng = User.Info.admin_api_update(user.info, info)
316
317 cng =
318 Ecto.Changeset.change(user)
319 |> Ecto.Changeset.put_embed(:info, info_cng)
320
321 {:ok, _user} = User.update_and_set_cache(cng)
322
323 ModerationLog.insert_log(%{
324 action: "revoke",
325 actor: admin,
326 subject: user,
327 permission: permission_group
328 })
329
330 json(conn, info)
331 end
332 end
333
334 def right_delete(conn, _) do
335 render_error(conn, :not_found, "No such permission_group")
336 end
337
338 def set_activation_status(%{assigns: %{user: admin}} = conn, %{
339 "nickname" => nickname,
340 "status" => status
341 }) do
342 with {:ok, status} <- Ecto.Type.cast(:boolean, status),
343 %User{} = user <- User.get_cached_by_nickname(nickname),
344 {:ok, _} <- User.deactivate(user, !status) do
345 action = if(user.info.deactivated, do: "activate", else: "deactivate")
346
347 ModerationLog.insert_log(%{
348 actor: admin,
349 subject: user,
350 action: action
351 })
352
353 json_response(conn, :no_content, "")
354 end
355 end
356
357 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
358 with {:ok, _message} <- Relay.follow(target) do
359 ModerationLog.insert_log(%{
360 action: "relay_follow",
361 actor: admin,
362 target: target
363 })
364
365 json(conn, target)
366 else
367 _ ->
368 conn
369 |> put_status(500)
370 |> json(target)
371 end
372 end
373
374 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
375 with {:ok, _message} <- Relay.unfollow(target) do
376 ModerationLog.insert_log(%{
377 action: "relay_unfollow",
378 actor: admin,
379 target: target
380 })
381
382 json(conn, target)
383 else
384 _ ->
385 conn
386 |> put_status(500)
387 |> json(target)
388 end
389 end
390
391 @doc "Sends registration invite via email"
392 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
393 with true <-
394 Pleroma.Config.get([:instance, :invites_enabled]) &&
395 !Pleroma.Config.get([:instance, :registrations_open]),
396 {:ok, invite_token} <- UserInviteToken.create_invite(),
397 email <-
398 Pleroma.Emails.UserEmail.user_invitation_email(
399 user,
400 invite_token,
401 email,
402 params["name"]
403 ),
404 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
405 json_response(conn, :no_content, "")
406 end
407 end
408
409 @doc "Create an account registration invite token"
410 def create_invite_token(conn, params) do
411 opts = %{}
412
413 opts =
414 if params["max_use"],
415 do: Map.put(opts, :max_use, params["max_use"]),
416 else: opts
417
418 opts =
419 if params["expires_at"],
420 do: Map.put(opts, :expires_at, params["expires_at"]),
421 else: opts
422
423 {:ok, invite} = UserInviteToken.create_invite(opts)
424
425 json(conn, AccountView.render("invite.json", %{invite: invite}))
426 end
427
428 @doc "Get list of created invites"
429 def invites(conn, _params) do
430 invites = UserInviteToken.list_invites()
431
432 conn
433 |> put_view(AccountView)
434 |> render("invites.json", %{invites: invites})
435 end
436
437 @doc "Revokes invite by token"
438 def revoke_invite(conn, %{"token" => token}) do
439 with {:ok, invite} <- UserInviteToken.find_by_token(token),
440 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
441 conn
442 |> put_view(AccountView)
443 |> render("invite.json", %{invite: updated_invite})
444 else
445 nil -> {:error, :not_found}
446 end
447 end
448
449 @doc "Get a password reset token (base64 string) for given nickname"
450 def get_password_reset(conn, %{"nickname" => nickname}) do
451 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
452 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
453
454 conn
455 |> json(%{
456 token: token.token,
457 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
458 })
459 end
460
461 @doc "Force password reset for a given user"
462 def force_password_reset(conn, %{"nickname" => nickname}) do
463 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
464
465 User.force_password_reset_async(user)
466
467 json_response(conn, :no_content, "")
468 end
469
470 def list_reports(conn, params) do
471 params =
472 params
473 |> Map.put("type", "Flag")
474 |> Map.put("skip_preload", true)
475 |> Map.put("total", true)
476
477 reports = ActivityPub.fetch_activities([], params)
478
479 conn
480 |> put_view(ReportView)
481 |> render("index.json", %{reports: reports})
482 end
483
484 def report_show(conn, %{"id" => id}) do
485 with %Activity{} = report <- Activity.get_by_id(id) do
486 conn
487 |> put_view(ReportView)
488 |> render("show.json", Report.extract_report_info(report))
489 else
490 _ -> {:error, :not_found}
491 end
492 end
493
494 def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
495 with {:ok, report} <- CommonAPI.update_report_state(id, state) do
496 ModerationLog.insert_log(%{
497 action: "report_update",
498 actor: admin,
499 subject: report
500 })
501
502 conn
503 |> put_view(ReportView)
504 |> render("show.json", Report.extract_report_info(report))
505 end
506 end
507
508 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
509 with false <- is_nil(params["status"]),
510 %Activity{} <- Activity.get_by_id(id) do
511 params =
512 params
513 |> Map.put("in_reply_to_status_id", id)
514 |> Map.put("visibility", "direct")
515
516 {:ok, activity} = CommonAPI.post(user, params)
517
518 ModerationLog.insert_log(%{
519 action: "report_response",
520 actor: user,
521 subject: activity,
522 text: params["status"]
523 })
524
525 conn
526 |> put_view(StatusView)
527 |> render("status.json", %{activity: activity})
528 else
529 true ->
530 {:param_cast, nil}
531
532 nil ->
533 {:error, :not_found}
534 end
535 end
536
537 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
538 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
539 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
540
541 ModerationLog.insert_log(%{
542 action: "status_update",
543 actor: admin,
544 subject: activity,
545 sensitive: sensitive,
546 visibility: params["visibility"]
547 })
548
549 conn
550 |> put_view(StatusView)
551 |> render("status.json", %{activity: activity})
552 end
553 end
554
555 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
556 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
557 ModerationLog.insert_log(%{
558 action: "status_delete",
559 actor: user,
560 subject_id: id
561 })
562
563 json(conn, %{})
564 end
565 end
566
567 def list_log(conn, params) do
568 {page, page_size} = page_params(params)
569
570 log = ModerationLog.get_all(page, page_size)
571
572 conn
573 |> put_view(ModerationLogView)
574 |> render("index.json", %{log: log})
575 end
576
577 def migrate_to_db(conn, _params) do
578 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
579 json(conn, %{})
580 end
581
582 def migrate_from_db(conn, _params) do
583 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
584 json(conn, %{})
585 end
586
587 def config_show(conn, _params) do
588 configs = Pleroma.Repo.all(Config)
589
590 conn
591 |> put_view(ConfigView)
592 |> render("index.json", %{configs: configs})
593 end
594
595 def config_update(conn, %{"configs" => configs}) do
596 updated =
597 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
598 updated =
599 Enum.map(configs, fn
600 %{"group" => group, "key" => key, "delete" => "true"} = params ->
601 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
602 config
603
604 %{"group" => group, "key" => key, "value" => value} ->
605 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
606 config
607 end)
608 |> Enum.reject(&is_nil(&1))
609
610 Pleroma.Config.TransferTask.load_and_update_env()
611 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
612 updated
613 else
614 []
615 end
616
617 conn
618 |> put_view(ConfigView)
619 |> render("index.json", %{configs: updated})
620 end
621
622 def reload_emoji(conn, _params) do
623 Pleroma.Emoji.reload()
624
625 conn |> json("ok")
626 end
627
628 def errors(conn, {:error, :not_found}) do
629 conn
630 |> put_status(:not_found)
631 |> json(dgettext("errors", "Not found"))
632 end
633
634 def errors(conn, {:error, reason}) do
635 conn
636 |> put_status(:bad_request)
637 |> json(reason)
638 end
639
640 def errors(conn, {:param_cast, _}) do
641 conn
642 |> put_status(:bad_request)
643 |> json(dgettext("errors", "Invalid parameters"))
644 end
645
646 def errors(conn, _) do
647 conn
648 |> put_status(:internal_server_error)
649 |> json(dgettext("errors", "Something went wrong"))
650 end
651
652 defp page_params(params) do
653 {get_page(params["page"]), get_page_size(params["page_size"])}
654 end
655
656 defp get_page(page_string) when is_nil(page_string), do: 1
657
658 defp get_page(page_string) do
659 case Integer.parse(page_string) do
660 {page, _} -> page
661 :error -> 1
662 end
663 end
664
665 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
666
667 defp get_page_size(page_size_string) do
668 case Integer.parse(page_size_string) do
669 {page_size, _} -> page_size
670 :error -> @users_page_size
671 end
672 end
673 end