While "is_active" is a property of users, it is not a recognised keyword
for the User.Query helper; instead "deactivated" with negated value must
be used.
This arose because originally the user property was also called
"deactivated" until it was renamed adn negated five years ago
in 860b5c7804. This included renaiming
the parameter in most but not all User.Query usages.
Notably the parameter in User.get_followers_query was renamed
but not in User.get_friends_query (friends == following).
The accepted query parameter in User.Query however was not changed.
This lead to the former mistakenly including deleted users causing
too large values to be reported in the ActivityPub follower, but not
following collection as reported in #1078.
In Masto API responses filtering by `User.is_visible` weeded out
the extra accounts before they got displayed to API users.
On the surface it might seem logical to align the name of the User.Query
parameter with the actual property name. However, User.Query already
accepts an "active" parameter which is an alias for limiting to accounts
which are neither deleted nor deactivated by moderators (both indicated
by is_active) and also are not newly created account requests still
pending an admin approval (is_approved) or necessary email confirmation
(is_confirmed); in short as the alias suggests whether the account is
active. Two highly similar parameter names like this would be much too
confusing.
The renamed "is_active" on the other hand does not actually suffice to
say whether an account is actually active, only whether it has (not yet)
ceased to be active (by its own volition or moderator action).
Meaning its "new" name is actively misleading. Arguably the rename
made things worse for no reason whatsoever and should not ever have
happened.
For now, we’ll just revert the incorrect query helper parameter renames.
Fixes: https://akkoma.dev/AkkomaGang/akkoma/ issues/ 1078
(cherry picked from commit a899663ddc0e3a2084a239eabd79608a94ac5493)
490 lines
13 KiB
Elixir
490 lines
13 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Mix.Tasks.Pleroma.User do
|
|
use Mix.Task
|
|
import Mix.Pleroma
|
|
alias Ecto.Changeset
|
|
alias Pleroma.User
|
|
alias Pleroma.UserInviteToken
|
|
alias Pleroma.Web.ActivityPub.Builder
|
|
alias Pleroma.Web.ActivityPub.Pipeline
|
|
|
|
@shortdoc "Manages Pleroma users"
|
|
@moduledoc File.read!("docs/administration/CLI_tasks/user.md")
|
|
|
|
def run(["new", nickname, email | rest]) do
|
|
{options, [], []} =
|
|
OptionParser.parse(
|
|
rest,
|
|
strict: [
|
|
name: :string,
|
|
bio: :string,
|
|
password: :string,
|
|
moderator: :boolean,
|
|
admin: :boolean,
|
|
assume_yes: :boolean
|
|
],
|
|
aliases: [
|
|
y: :assume_yes
|
|
]
|
|
)
|
|
|
|
name = Keyword.get(options, :name, nickname)
|
|
bio = Keyword.get(options, :bio, "")
|
|
|
|
{password, generated_password?} =
|
|
case Keyword.get(options, :password) do
|
|
nil ->
|
|
{:crypto.strong_rand_bytes(16) |> Base.encode64(), true}
|
|
|
|
password ->
|
|
{password, false}
|
|
end
|
|
|
|
moderator? = Keyword.get(options, :moderator, false)
|
|
admin? = Keyword.get(options, :admin, false)
|
|
assume_yes? = Keyword.get(options, :assume_yes, false)
|
|
|
|
shell_info("""
|
|
A user will be created with the following information:
|
|
- nickname: #{nickname}
|
|
- email: #{email}
|
|
- password: #{if(generated_password?, do: "[generated; a reset link will be created]", else: password)}
|
|
- name: #{name}
|
|
- bio: #{bio}
|
|
- moderator: #{if(moderator?, do: "true", else: "false")}
|
|
- admin: #{if(admin?, do: "true", else: "false")}
|
|
""")
|
|
|
|
proceed? = assume_yes? or shell_prompt("Continue?", "n") in ~w(Yn Y y)
|
|
|
|
if proceed? do
|
|
start_pleroma()
|
|
|
|
params = %{
|
|
nickname: nickname,
|
|
email: email,
|
|
password: password,
|
|
password_confirmation: password,
|
|
name: name,
|
|
bio: bio
|
|
}
|
|
|
|
changeset = User.register_changeset(%User{}, params, is_confirmed: true)
|
|
{:ok, _user} = User.register(changeset)
|
|
|
|
shell_info("User #{nickname} created")
|
|
|
|
if moderator? do
|
|
run(["set", nickname, "--moderator"])
|
|
end
|
|
|
|
if admin? do
|
|
run(["set", nickname, "--admin"])
|
|
end
|
|
|
|
if generated_password? do
|
|
run(["reset_password", nickname])
|
|
end
|
|
else
|
|
shell_info("User will not be created.")
|
|
end
|
|
end
|
|
|
|
def run(["rm", nickname]) do
|
|
start_pleroma()
|
|
|
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
|
|
{:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
|
|
{:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
|
|
shell_info("User #{nickname} deleted.")
|
|
else
|
|
_ -> shell_error("No local user #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["reset_password", nickname]) do
|
|
start_pleroma()
|
|
|
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
|
|
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
|
|
shell_info("Generated password reset token for #{user.nickname}")
|
|
|
|
url =
|
|
Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint, :reset, token.token)
|
|
|
|
IO.puts("URL: #{url}")
|
|
else
|
|
_ ->
|
|
shell_error("No local user #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["reset_mfa", nickname]) do
|
|
start_pleroma()
|
|
|
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
|
|
{:ok, _token} <- Pleroma.MFA.disable(user) do
|
|
shell_info("Multi-Factor Authentication disabled for #{user.nickname}")
|
|
else
|
|
_ ->
|
|
shell_error("No local user #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["activate", nickname]) do
|
|
start_pleroma()
|
|
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
|
false <- user.is_active do
|
|
User.set_activation(user, true)
|
|
:timer.sleep(500)
|
|
|
|
shell_info("Successfully activated #{nickname}")
|
|
else
|
|
true ->
|
|
shell_info("User #{nickname} already activated")
|
|
|
|
_ ->
|
|
shell_error("No user #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["deactivate", nickname]) do
|
|
start_pleroma()
|
|
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
|
true <- user.is_active do
|
|
User.set_activation(user, false)
|
|
:timer.sleep(500)
|
|
|
|
user = User.get_cached_by_id(user.id)
|
|
|
|
if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
|
|
shell_info("Successfully deactivated #{nickname} and unsubscribed all local followers")
|
|
end
|
|
else
|
|
false ->
|
|
shell_info("User #{nickname} already deactivated")
|
|
|
|
_ ->
|
|
shell_error("No user #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["deactivate_all_from_instance", instance]) do
|
|
start_pleroma()
|
|
|
|
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
|
|
|> Pleroma.Repo.chunk_stream(500, :batches)
|
|
|> Stream.each(fn users ->
|
|
users
|
|
|> Enum.each(fn user ->
|
|
run(["deactivate", user.nickname])
|
|
end)
|
|
end)
|
|
|> Stream.run()
|
|
end
|
|
|
|
def run(["set", nickname | rest]) do
|
|
start_pleroma()
|
|
|
|
{options, [], []} =
|
|
OptionParser.parse(
|
|
rest,
|
|
strict: [
|
|
admin: :boolean,
|
|
confirmed: :boolean,
|
|
locked: :boolean,
|
|
moderator: :boolean
|
|
]
|
|
)
|
|
|
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
|
user =
|
|
case Keyword.get(options, :admin) do
|
|
nil -> user
|
|
value -> set_admin(user, value)
|
|
end
|
|
|
|
user =
|
|
case Keyword.get(options, :confirmed) do
|
|
nil -> user
|
|
value -> set_confirmation(user, value)
|
|
end
|
|
|
|
user =
|
|
case Keyword.get(options, :locked) do
|
|
nil -> user
|
|
value -> set_locked(user, value)
|
|
end
|
|
|
|
_user =
|
|
case Keyword.get(options, :moderator) do
|
|
nil -> user
|
|
value -> set_moderator(user, value)
|
|
end
|
|
else
|
|
_ ->
|
|
shell_error("No local user #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["tag", nickname | tags]) do
|
|
start_pleroma()
|
|
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
|
user = user |> User.tag(tags)
|
|
|
|
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
|
|
else
|
|
_ ->
|
|
shell_error("Could not change user tags for #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["untag", nickname | tags]) do
|
|
start_pleroma()
|
|
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
|
user = user |> User.untag(tags)
|
|
|
|
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
|
|
else
|
|
_ ->
|
|
shell_error("Could not change user tags for #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["invite" | rest]) do
|
|
{options, [], []} =
|
|
OptionParser.parse(rest,
|
|
strict: [
|
|
expires_at: :string,
|
|
max_use: :integer
|
|
]
|
|
)
|
|
|
|
options =
|
|
options
|
|
|> Keyword.update(:expires_at, {:ok, nil}, fn
|
|
nil -> {:ok, nil}
|
|
val -> Date.from_iso8601(val)
|
|
end)
|
|
|> Enum.into(%{})
|
|
|
|
start_pleroma()
|
|
|
|
with {:ok, val} <- options[:expires_at],
|
|
options = Map.put(options, :expires_at, val),
|
|
{:ok, invite} <- UserInviteToken.create_invite(options) do
|
|
shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))
|
|
|
|
url =
|
|
Pleroma.Web.Router.Helpers.redirect_url(
|
|
Pleroma.Web.Endpoint,
|
|
:registration_page,
|
|
invite.token
|
|
)
|
|
|
|
IO.puts(url)
|
|
else
|
|
error ->
|
|
shell_error("Could not create invite token: #{inspect(error)}")
|
|
end
|
|
end
|
|
|
|
def run(["invites"]) do
|
|
start_pleroma()
|
|
|
|
shell_info("Invites list:")
|
|
|
|
UserInviteToken.list_invites()
|
|
|> Enum.each(fn invite ->
|
|
expire_info =
|
|
with expires_at when not is_nil(expires_at) <- invite.expires_at do
|
|
" | Expires at: #{Date.to_string(expires_at)}"
|
|
end
|
|
|
|
using_info =
|
|
with max_use when not is_nil(max_use) <- invite.max_use do
|
|
" | Max use: #{max_use} Left use: #{max_use - invite.uses}"
|
|
end
|
|
|
|
shell_info(
|
|
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{invite.used}#{expire_info}#{using_info}"
|
|
)
|
|
end)
|
|
end
|
|
|
|
def run(["revoke_invite", token]) do
|
|
start_pleroma()
|
|
|
|
with {:ok, invite} <- UserInviteToken.find_by_token(token),
|
|
{:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
|
|
shell_info("Invite for token #{token} was revoked.")
|
|
else
|
|
_ -> shell_error("No invite found with token #{token}")
|
|
end
|
|
end
|
|
|
|
def run(["delete_activities", nickname]) do
|
|
start_pleroma()
|
|
|
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
|
User.delete_user_activities(user)
|
|
shell_info("User #{nickname} statuses deleted.")
|
|
else
|
|
_ ->
|
|
shell_error("No local user #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["confirm", nickname]) do
|
|
start_pleroma()
|
|
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
|
{:ok, user} = User.confirm(user)
|
|
|
|
message = if !user.is_confirmed, do: "needs", else: "doesn't need"
|
|
|
|
shell_info("#{nickname} #{message} confirmation.")
|
|
else
|
|
_ ->
|
|
shell_error("No local user #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["confirm_all"]) do
|
|
start_pleroma()
|
|
|
|
Pleroma.User.Query.build(%{
|
|
local: true,
|
|
deactivated: false,
|
|
is_moderator: false,
|
|
is_admin: false,
|
|
invisible: false
|
|
})
|
|
|> Pleroma.Repo.chunk_stream(500, :batches)
|
|
|> Stream.each(fn users ->
|
|
users
|
|
|> Enum.each(fn user -> User.set_confirmation(user, true) end)
|
|
end)
|
|
|> Stream.run()
|
|
end
|
|
|
|
def run(["unconfirm_all"]) do
|
|
start_pleroma()
|
|
|
|
Pleroma.User.Query.build(%{
|
|
local: true,
|
|
deactivated: false,
|
|
is_moderator: false,
|
|
is_admin: false,
|
|
invisible: false
|
|
})
|
|
|> Pleroma.Repo.chunk_stream(500, :batches)
|
|
|> Stream.each(fn users ->
|
|
users
|
|
|> Enum.each(fn user -> User.set_confirmation(user, false) end)
|
|
end)
|
|
|> Stream.run()
|
|
end
|
|
|
|
def run(["sign_out", nickname]) do
|
|
start_pleroma()
|
|
|
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
|
User.global_sign_out(user)
|
|
|
|
shell_info("#{nickname} signed out from all apps.")
|
|
else
|
|
_ ->
|
|
shell_error("No local user #{nickname}")
|
|
end
|
|
end
|
|
|
|
def run(["list"]) do
|
|
start_pleroma()
|
|
|
|
Pleroma.User.Query.build(%{local: true})
|
|
|> Pleroma.Repo.chunk_stream(500, :batches)
|
|
|> Stream.each(fn users ->
|
|
users
|
|
|> Enum.each(fn user ->
|
|
shell_info(
|
|
"#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{user.is_locked}, is_active: #{user.is_active}"
|
|
)
|
|
end)
|
|
end)
|
|
|> Stream.run()
|
|
end
|
|
|
|
def run(["fix_follow_state", local_user, remote_user]) do
|
|
start_pleroma()
|
|
|
|
with {:local, %User{} = local} <- {:local, User.get_by_nickname(local_user)},
|
|
{:remote, %User{} = remote} <- {:remote, User.get_by_nickname(remote_user)},
|
|
{:follow_data, %{data: %{"state" => request_state}}} <-
|
|
{:follow_data, Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(local, remote)} do
|
|
calculated_state = User.following?(local, remote)
|
|
|
|
shell_info(
|
|
"Request state is #{request_state}, vs calculated state of following=#{calculated_state}"
|
|
)
|
|
|
|
if calculated_state == false && request_state == "accept" do
|
|
shell_info("Discrepancy found, fixing")
|
|
Pleroma.Web.CommonAPI.reject_follow_request(local, remote)
|
|
shell_info("Relationship fixed")
|
|
else
|
|
shell_info("No discrepancy found")
|
|
end
|
|
else
|
|
{:local, _} ->
|
|
shell_error("No local user #{local_user}")
|
|
|
|
{:remote, _} ->
|
|
shell_error("No remote user #{remote_user}")
|
|
|
|
{:follow_data, _} ->
|
|
shell_error("No follow data for #{local_user} and #{remote_user}")
|
|
end
|
|
end
|
|
|
|
defp set_moderator(user, value) do
|
|
{:ok, user} =
|
|
user
|
|
|> Changeset.change(%{is_moderator: value})
|
|
|> User.update_and_set_cache()
|
|
|
|
shell_info("Moderator status of #{user.nickname}: #{user.is_moderator}")
|
|
user
|
|
end
|
|
|
|
defp set_admin(user, value) do
|
|
{:ok, user} = User.admin_api_update(user, %{is_admin: value})
|
|
|
|
shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
|
|
user
|
|
end
|
|
|
|
defp set_locked(user, value) do
|
|
{:ok, user} =
|
|
user
|
|
|> Changeset.change(%{is_locked: value})
|
|
|> User.update_and_set_cache()
|
|
|
|
shell_info("Locked status of #{user.nickname}: #{user.is_locked}")
|
|
user
|
|
end
|
|
|
|
defp set_confirmation(user, value) do
|
|
{:ok, user} = User.set_confirmation(user, value)
|
|
|
|
shell_info("Confirmation status of #{user.nickname}: #{user.is_confirmed}")
|
|
user
|
|
end
|
|
end
|