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