First
[anni] / lib / mix / tasks / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Mix.Tasks.Pleroma.User do
6   use Mix.Task
7   import Mix.Pleroma
8   alias Ecto.Changeset
9   alias Pleroma.User
10   alias Pleroma.UserInviteToken
11   alias Pleroma.Web.ActivityPub.Builder
12   alias Pleroma.Web.ActivityPub.Pipeline
13
14   @shortdoc "Manages Pleroma users"
15   @moduledoc File.read!("docs/administration/CLI_tasks/user.md")
16
17   def run(["new", nickname, email | rest]) do
18     {options, [], []} =
19       OptionParser.parse(
20         rest,
21         strict: [
22           name: :string,
23           bio: :string,
24           password: :string,
25           moderator: :boolean,
26           admin: :boolean,
27           assume_yes: :boolean
28         ],
29         aliases: [
30           y: :assume_yes
31         ]
32       )
33
34     name = Keyword.get(options, :name, nickname)
35     bio = Keyword.get(options, :bio, "")
36
37     {password, generated_password?} =
38       case Keyword.get(options, :password) do
39         nil ->
40           {:crypto.strong_rand_bytes(16) |> Base.encode64(), true}
41
42         password ->
43           {password, false}
44       end
45
46     moderator? = Keyword.get(options, :moderator, false)
47     admin? = Keyword.get(options, :admin, false)
48     assume_yes? = Keyword.get(options, :assume_yes, false)
49
50     shell_info("""
51     A user will be created with the following information:
52       - nickname: #{nickname}
53       - email: #{email}
54       - password: #{if(generated_password?, do: "[generated; a reset link will be created]", else: password)}
55       - name: #{name}
56       - bio: #{bio}
57       - moderator: #{if(moderator?, do: "true", else: "false")}
58       - admin: #{if(admin?, do: "true", else: "false")}
59     """)
60
61     proceed? = assume_yes? or shell_prompt("Continue?", "n") in ~w(Yn Y y)
62
63     if proceed? do
64       start_pleroma()
65
66       params = %{
67         nickname: nickname,
68         email: email,
69         password: password,
70         password_confirmation: password,
71         name: name,
72         bio: bio
73       }
74
75       changeset = User.register_changeset(%User{}, params, is_confirmed: true)
76       {:ok, _user} = User.register(changeset)
77
78       shell_info("User #{nickname} created")
79
80       if moderator? do
81         run(["set", nickname, "--moderator"])
82       end
83
84       if admin? do
85         run(["set", nickname, "--admin"])
86       end
87
88       if generated_password? do
89         run(["reset_password", nickname])
90       end
91     else
92       shell_info("User will not be created.")
93     end
94   end
95
96   def run(["rm", nickname]) do
97     start_pleroma()
98
99     with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
100          {:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
101          {:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
102       shell_info("User #{nickname} deleted.")
103     else
104       _ -> shell_error("No local user #{nickname}")
105     end
106   end
107
108   def run(["reset_password", nickname]) do
109     start_pleroma()
110
111     with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
112          {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
113       shell_info("Generated password reset token for #{user.nickname}")
114
115       url =
116         Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint, :reset, token.token)
117
118       IO.puts("URL: #{url}")
119     else
120       _ ->
121         shell_error("No local user #{nickname}")
122     end
123   end
124
125   def run(["reset_mfa", nickname]) do
126     start_pleroma()
127
128     with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
129          {:ok, _token} <- Pleroma.MFA.disable(user) do
130       shell_info("Multi-Factor Authentication disabled for #{user.nickname}")
131     else
132       _ ->
133         shell_error("No local user #{nickname}")
134     end
135   end
136
137   def run(["activate", nickname]) do
138     start_pleroma()
139
140     with %User{} = user <- User.get_cached_by_nickname(nickname),
141          false <- user.is_active do
142       User.set_activation(user, true)
143       :timer.sleep(500)
144
145       shell_info("Successfully activated #{nickname}")
146     else
147       true ->
148         shell_info("User #{nickname} already activated")
149
150       _ ->
151         shell_error("No user #{nickname}")
152     end
153   end
154
155   def run(["deactivate", nickname]) do
156     start_pleroma()
157
158     with %User{} = user <- User.get_cached_by_nickname(nickname),
159          true <- user.is_active do
160       User.set_activation(user, false)
161       :timer.sleep(500)
162
163       user = User.get_cached_by_id(user.id)
164
165       if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
166         shell_info("Successfully deactivated #{nickname} and unsubscribed all local followers")
167       end
168     else
169       false ->
170         shell_info("User #{nickname} already deactivated")
171
172       _ ->
173         shell_error("No user #{nickname}")
174     end
175   end
176
177   def run(["deactivate_all_from_instance", instance]) do
178     start_pleroma()
179
180     Pleroma.User.Query.build(%{nickname: "@#{instance}"})
181     |> Pleroma.Repo.chunk_stream(500, :batches)
182     |> Stream.each(fn users ->
183       users
184       |> Enum.each(fn user ->
185         run(["deactivate", user.nickname])
186       end)
187     end)
188     |> Stream.run()
189   end
190
191   def run(["set", nickname | rest]) do
192     start_pleroma()
193
194     {options, [], []} =
195       OptionParser.parse(
196         rest,
197         strict: [
198           admin: :boolean,
199           confirmed: :boolean,
200           locked: :boolean,
201           moderator: :boolean
202         ]
203       )
204
205     with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
206       user =
207         case Keyword.get(options, :admin) do
208           nil -> user
209           value -> set_admin(user, value)
210         end
211
212       user =
213         case Keyword.get(options, :confirmed) do
214           nil -> user
215           value -> set_confirmation(user, value)
216         end
217
218       user =
219         case Keyword.get(options, :locked) do
220           nil -> user
221           value -> set_locked(user, value)
222         end
223
224       _user =
225         case Keyword.get(options, :moderator) do
226           nil -> user
227           value -> set_moderator(user, value)
228         end
229     else
230       _ ->
231         shell_error("No local user #{nickname}")
232     end
233   end
234
235   def run(["tag", nickname | tags]) do
236     start_pleroma()
237
238     with %User{} = user <- User.get_cached_by_nickname(nickname) do
239       user = user |> User.tag(tags)
240
241       shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
242     else
243       _ ->
244         shell_error("Could not change user tags for #{nickname}")
245     end
246   end
247
248   def run(["untag", nickname | tags]) do
249     start_pleroma()
250
251     with %User{} = user <- User.get_cached_by_nickname(nickname) do
252       user = user |> User.untag(tags)
253
254       shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
255     else
256       _ ->
257         shell_error("Could not change user tags for #{nickname}")
258     end
259   end
260
261   def run(["invite" | rest]) do
262     {options, [], []} =
263       OptionParser.parse(rest,
264         strict: [
265           expires_at: :string,
266           max_use: :integer
267         ]
268       )
269
270     options =
271       options
272       |> Keyword.update(:expires_at, {:ok, nil}, fn
273         nil -> {:ok, nil}
274         val -> Date.from_iso8601(val)
275       end)
276       |> Enum.into(%{})
277
278     start_pleroma()
279
280     with {:ok, val} <- options[:expires_at],
281          options = Map.put(options, :expires_at, val),
282          {:ok, invite} <- UserInviteToken.create_invite(options) do
283       shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))
284
285       url =
286         Pleroma.Web.Router.Helpers.redirect_url(
287           Pleroma.Web.Endpoint,
288           :registration_page,
289           invite.token
290         )
291
292       IO.puts(url)
293     else
294       error ->
295         shell_error("Could not create invite token: #{inspect(error)}")
296     end
297   end
298
299   def run(["invites"]) do
300     start_pleroma()
301
302     shell_info("Invites list:")
303
304     UserInviteToken.list_invites()
305     |> Enum.each(fn invite ->
306       expire_info =
307         with expires_at when not is_nil(expires_at) <- invite.expires_at do
308           " | Expires at: #{Date.to_string(expires_at)}"
309         end
310
311       using_info =
312         with max_use when not is_nil(max_use) <- invite.max_use do
313           " | Max use: #{max_use}    Left use: #{max_use - invite.uses}"
314         end
315
316       shell_info(
317         "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{invite.used}#{expire_info}#{using_info}"
318       )
319     end)
320   end
321
322   def run(["revoke_invite", token]) do
323     start_pleroma()
324
325     with {:ok, invite} <- UserInviteToken.find_by_token(token),
326          {:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
327       shell_info("Invite for token #{token} was revoked.")
328     else
329       _ -> shell_error("No invite found with token #{token}")
330     end
331   end
332
333   def run(["delete_activities", nickname]) do
334     start_pleroma()
335
336     with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
337       User.delete_user_activities(user)
338       shell_info("User #{nickname} statuses deleted.")
339     else
340       _ ->
341         shell_error("No local user #{nickname}")
342     end
343   end
344
345   def run(["confirm", nickname]) do
346     start_pleroma()
347
348     with %User{} = user <- User.get_cached_by_nickname(nickname) do
349       {:ok, user} = User.confirm(user)
350
351       message = if !user.is_confirmed, do: "needs", else: "doesn't need"
352
353       shell_info("#{nickname} #{message} confirmation.")
354     else
355       _ ->
356         shell_error("No local user #{nickname}")
357     end
358   end
359
360   def run(["confirm_all"]) do
361     start_pleroma()
362
363     Pleroma.User.Query.build(%{
364       local: true,
365       is_active: true,
366       is_moderator: false,
367       is_admin: false,
368       invisible: false
369     })
370     |> Pleroma.Repo.chunk_stream(500, :batches)
371     |> Stream.each(fn users ->
372       users
373       |> Enum.each(fn user -> User.set_confirmation(user, true) end)
374     end)
375     |> Stream.run()
376   end
377
378   def run(["unconfirm_all"]) do
379     start_pleroma()
380
381     Pleroma.User.Query.build(%{
382       local: true,
383       is_active: true,
384       is_moderator: false,
385       is_admin: false,
386       invisible: false
387     })
388     |> Pleroma.Repo.chunk_stream(500, :batches)
389     |> Stream.each(fn users ->
390       users
391       |> Enum.each(fn user -> User.set_confirmation(user, false) end)
392     end)
393     |> Stream.run()
394   end
395
396   def run(["sign_out", nickname]) do
397     start_pleroma()
398
399     with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
400       User.global_sign_out(user)
401
402       shell_info("#{nickname} signed out from all apps.")
403     else
404       _ ->
405         shell_error("No local user #{nickname}")
406     end
407   end
408
409   def run(["list"]) do
410     start_pleroma()
411
412     Pleroma.User.Query.build(%{local: true})
413     |> Pleroma.Repo.chunk_stream(500, :batches)
414     |> Stream.each(fn users ->
415       users
416       |> Enum.each(fn user ->
417         shell_info(
418           "#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{user.is_locked}, is_active: #{user.is_active}"
419         )
420       end)
421     end)
422     |> Stream.run()
423   end
424
425   def run(["fix_follow_state", local_user, remote_user]) do
426     start_pleroma()
427
428     with {:local, %User{} = local} <- {:local, User.get_by_nickname(local_user)},
429          {:remote, %User{} = remote} <- {:remote, User.get_by_nickname(remote_user)},
430          {:follow_data, %{data: %{"state" => request_state}}} <-
431            {:follow_data, Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(local, remote)} do
432       calculated_state = User.following?(local, remote)
433
434       shell_info(
435         "Request state is #{request_state}, vs calculated state of following=#{calculated_state}"
436       )
437
438       if calculated_state == false && request_state == "accept" do
439         shell_info("Discrepancy found, fixing")
440         Pleroma.Web.CommonAPI.reject_follow_request(local, remote)
441         shell_info("Relationship fixed")
442       else
443         shell_info("No discrepancy found")
444       end
445     else
446       {:local, _} ->
447         shell_error("No local user #{local_user}")
448
449       {:remote, _} ->
450         shell_error("No remote user #{remote_user}")
451
452       {:follow_data, _} ->
453         shell_error("No follow data for #{local_user} and #{remote_user}")
454     end
455   end
456
457   defp set_moderator(user, value) do
458     {:ok, user} =
459       user
460       |> Changeset.change(%{is_moderator: value})
461       |> User.update_and_set_cache()
462
463     shell_info("Moderator status of #{user.nickname}: #{user.is_moderator}")
464     user
465   end
466
467   defp set_admin(user, value) do
468     {:ok, user} = User.admin_api_update(user, %{is_admin: value})
469
470     shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
471     user
472   end
473
474   defp set_locked(user, value) do
475     {:ok, user} =
476       user
477       |> Changeset.change(%{is_locked: value})
478       |> User.update_and_set_cache()
479
480     shell_info("Locked status of #{user.nickname}: #{user.is_locked}")
481     user
482   end
483
484   defp set_confirmation(user, value) do
485     {:ok, user} = User.set_confirmation(user, value)
486
487     shell_info("Confirmation status of #{user.nickname}: #{user.is_confirmed}")
488     user
489   end
490 end