Merge branch 'feature/bulk-confirmation' into 'develop'

Bulk account confirmation actions

Closes #2085

See merge request pleroma/pleroma!2975
This commit is contained in:
feld 2020-09-25 16:15:01 +00:00
commit 1672d8b37c
9 changed files with 276 additions and 23 deletions

View file

@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
### Added
- Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`)
- Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email send_confirmation_mails`)
### Changed ### Changed
- Search: Users are now findable by their urls. - Search: Users are now findable by their urls.

View file

@ -1,4 +1,4 @@
# Managing emails # EMail administration tasks
{! backend/administration/CLI_tasks/general_cli_task_info.include !} {! backend/administration/CLI_tasks/general_cli_task_info.include !}
@ -30,3 +30,17 @@ Example:
```sh ```sh
mix pleroma.email test --to root@example.org mix pleroma.email test --to root@example.org
``` ```
## Send confirmation emails to all unconfirmed user accounts
=== "OTP"
```sh
./bin/pleroma_ctl email send_confirmation_mails
```
=== "From Source"
```sh
mix pleroma.email send_confirmation_mails
```

View file

@ -224,9 +224,10 @@
``` ```
### Options ### Options
- `--admin`/`--no-admin` - whether the user should be an admin
- `--confirmed`/`--no-confirmed` - whether the user account is confirmed
- `--locked`/`--no-locked` - whether the user should be locked - `--locked`/`--no-locked` - whether the user should be locked
- `--moderator`/`--no-moderator` - whether the user should be a moderator - `--moderator`/`--no-moderator` - whether the user should be a moderator
- `--admin`/`--no-admin` - whether the user should be an admin
## Add tags to a user ## Add tags to a user
@ -271,3 +272,33 @@
```sh ```sh
mix pleroma.user toggle_confirmed <nickname> mix pleroma.user toggle_confirmed <nickname>
``` ```
## Set confirmation status for all regular active users
*Admins and moderators are excluded*
=== "OTP"
```sh
./bin/pleroma_ctl user confirm_all
```
=== "From Source"
```sh
mix pleroma.user confirm_all
```
## Revoke confirmation status for all regular active users
*Admins and moderators are excluded*
=== "OTP"
```sh
./bin/pleroma_ctl user unconfirm_all
```
=== "From Source"
```sh
mix pleroma.user unconfirm_all
```

View file

@ -2,11 +2,11 @@ defmodule Mix.Tasks.Pleroma.Email do
use Mix.Task use Mix.Task
import Mix.Pleroma import Mix.Pleroma
@shortdoc "Simple Email test" @shortdoc "Email administrative tasks"
@moduledoc File.read!("docs/administration/CLI_tasks/email.md") @moduledoc File.read!("docs/administration/CLI_tasks/email.md")
def run(["test" | args]) do def run(["test" | args]) do
Mix.Pleroma.start_pleroma() start_pleroma()
{options, [], []} = {options, [], []} =
OptionParser.parse( OptionParser.parse(
@ -21,4 +21,20 @@ def run(["test" | args]) do
shell_info("Test email has been sent to #{inspect(email.to)} from #{inspect(email.from)}") shell_info("Test email has been sent to #{inspect(email.to)} from #{inspect(email.from)}")
end end
def run(["resend_confirmation_emails"]) do
start_pleroma()
shell_info("Sending emails to all unconfirmed users")
Pleroma.User.Query.build(%{
local: true,
deactivated: false,
confirmation_pending: true,
invisible: false
})
|> Pleroma.Repo.chunk_stream(500)
|> Stream.each(&Pleroma.User.try_send_confirmation_email(&1))
|> Stream.run()
end
end end

View file

@ -196,17 +196,24 @@ def run(["set", nickname | rest]) do
OptionParser.parse( OptionParser.parse(
rest, rest,
strict: [ strict: [
moderator: :boolean,
admin: :boolean, admin: :boolean,
locked: :boolean confirmed: :boolean,
locked: :boolean,
moderator: :boolean
] ]
) )
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user = user =
case Keyword.get(options, :moderator) do case Keyword.get(options, :admin) do
nil -> user nil -> user
value -> set_moderator(user, value) value -> set_admin(user, value)
end
user =
case Keyword.get(options, :confirmed) do
nil -> user
value -> set_confirmed(user, value)
end end
user = user =
@ -216,9 +223,9 @@ def run(["set", nickname | rest]) do
end end
_user = _user =
case Keyword.get(options, :admin) do case Keyword.get(options, :moderator) do
nil -> user nil -> user
value -> set_admin(user, value) value -> set_moderator(user, value)
end end
else else
_ -> _ ->
@ -353,6 +360,42 @@ def run(["toggle_confirmed", nickname]) do
end end
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.need_confirmation(user, false) 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.need_confirmation(user, true) end)
end)
|> Stream.run()
end
def run(["sign_out", nickname]) do def run(["sign_out", nickname]) do
start_pleroma() start_pleroma()
@ -410,4 +453,11 @@ defp set_locked(user, value) do
shell_info("Locked status of #{user.nickname}: #{user.locked}") shell_info("Locked status of #{user.nickname}: #{user.locked}")
user user
end end
defp set_confirmed(user, value) do
{:ok, user} = User.need_confirmation(user, !value)
shell_info("Confirmation pending status of #{user.nickname}: #{user.confirmation_pending}")
user
end
end end

View file

@ -813,7 +813,8 @@ def send_welcome_email(%User{email: email} = user) when is_binary(email) do
def send_welcome_email(_), do: {:ok, :noop} def send_welcome_email(_), do: {:ok, :noop}
@spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop} @spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
def try_send_confirmation_email(%User{confirmation_pending: true} = user) do def try_send_confirmation_email(%User{confirmation_pending: true, email: email} = user)
when is_binary(email) do
if Config.get([:instance, :account_activation_required]) do if Config.get([:instance, :account_activation_required]) do
send_confirmation_email(user) send_confirmation_email(user)
{:ok, :enqueued} {:ok, :enqueued}
@ -2071,6 +2072,13 @@ def toggle_confirmation(users) do
Enum.map(users, &toggle_confirmation/1) Enum.map(users, &toggle_confirmation/1)
end end
@spec need_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
def need_confirmation(%User{} = user, bool) do
user
|> confirmation_changeset(need_confirmation: bool)
|> update_and_set_cache()
end
def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
mascot mascot
end end

View file

@ -110,12 +110,12 @@ defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0
where(query, [u], fragment("? && ?", u.tags, ^tags)) where(query, [u], fragment("? && ?", u.tags, ^tags))
end end
defp compose_query({:is_admin, _}, query) do defp compose_query({:is_admin, bool}, query) do
where(query, [u], u.is_admin) where(query, [u], u.is_admin == ^bool)
end end
defp compose_query({:is_moderator, _}, query) do defp compose_query({:is_moderator, bool}, query) do
where(query, [u], u.is_moderator) where(query, [u], u.is_moderator == ^bool)
end end
defp compose_query({:super_users, _}, query) do defp compose_query({:super_users, _}, query) do
@ -148,6 +148,10 @@ defp compose_query({:deactivated, true}, query) do
where(query, [u], u.deactivated == ^true) where(query, [u], u.deactivated == ^true)
end end
defp compose_query({:confirmation_pending, bool}, query) do
where(query, [u], u.confirmation_pending == ^bool)
end
defp compose_query({:need_approval, _}, query) do defp compose_query({:need_approval, _}, query) do
where(query, [u], u.approval_pending) where(query, [u], u.approval_pending)
end end

View file

@ -6,6 +6,8 @@ defmodule Mix.Tasks.Pleroma.EmailTest do
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Tests.ObanHelpers alias Pleroma.Tests.ObanHelpers
import Pleroma.Factory
setup_all do setup_all do
Mix.shell(Mix.Shell.Process) Mix.shell(Mix.Shell.Process)
@ -17,6 +19,7 @@ defmodule Mix.Tasks.Pleroma.EmailTest do
end end
setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true)
setup do: clear_config([:instance, :account_activation_required], true)
describe "pleroma.email test" do describe "pleroma.email test" do
test "Sends test email with no given address" do test "Sends test email with no given address" do
@ -50,5 +53,71 @@ test "Sends test email with given address" do
html_body: ~r/a test email was requested./i html_body: ~r/a test email was requested./i
) )
end end
test "Sends confirmation emails" do
local_user1 =
insert(:user, %{
confirmation_pending: true,
confirmation_token: "mytoken",
deactivated: false,
email: "local1@pleroma.com",
local: true
})
local_user2 =
insert(:user, %{
confirmation_pending: true,
confirmation_token: "mytoken",
deactivated: false,
email: "local2@pleroma.com",
local: true
})
:ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"])
ObanHelpers.perform_all()
assert_email_sent(to: {local_user1.name, local_user1.email})
assert_email_sent(to: {local_user2.name, local_user2.email})
end
test "Does not send confirmation email to inappropriate users" do
# confirmed user
insert(:user, %{
confirmation_pending: false,
confirmation_token: "mytoken",
deactivated: false,
email: "confirmed@pleroma.com",
local: true
})
# remote user
insert(:user, %{
deactivated: false,
email: "remote@not-pleroma.com",
local: false
})
# deactivated user =
insert(:user, %{
deactivated: true,
email: "deactivated@pleroma.com",
local: false
})
# invisible user
insert(:user, %{
deactivated: false,
email: "invisible@pleroma.com",
local: true,
invisible: true
})
:ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"])
ObanHelpers.perform_all()
refute_email_sent()
end
end end
end end

View file

@ -225,47 +225,64 @@ test "no user to deactivate" do
test "All statuses set" do test "All statuses set" do
user = insert(:user) user = insert(:user)
Mix.Tasks.Pleroma.User.run(["set", user.nickname, "--moderator", "--admin", "--locked"]) Mix.Tasks.Pleroma.User.run([
"set",
user.nickname,
"--admin",
"--confirmed",
"--locked",
"--moderator"
])
assert_received {:mix_shell, :info, [message]} assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Moderator status .* true/ assert message =~ ~r/Admin status .* true/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Confirmation pending .* false/
assert_received {:mix_shell, :info, [message]} assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Locked status .* true/ assert message =~ ~r/Locked status .* true/
assert_received {:mix_shell, :info, [message]} assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Admin status .* true/ assert message =~ ~r/Moderator status .* true/
user = User.get_cached_by_nickname(user.nickname) user = User.get_cached_by_nickname(user.nickname)
assert user.is_moderator assert user.is_moderator
assert user.locked assert user.locked
assert user.is_admin assert user.is_admin
refute user.confirmation_pending
end end
test "All statuses unset" do test "All statuses unset" do
user = insert(:user, locked: true, is_moderator: true, is_admin: true) user =
insert(:user, locked: true, is_moderator: true, is_admin: true, confirmation_pending: true)
Mix.Tasks.Pleroma.User.run([ Mix.Tasks.Pleroma.User.run([
"set", "set",
user.nickname, user.nickname,
"--no-moderator",
"--no-admin", "--no-admin",
"--no-locked" "--no-confirmed",
"--no-locked",
"--no-moderator"
]) ])
assert_received {:mix_shell, :info, [message]} assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Moderator status .* false/ assert message =~ ~r/Admin status .* false/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Confirmation pending .* true/
assert_received {:mix_shell, :info, [message]} assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Locked status .* false/ assert message =~ ~r/Locked status .* false/
assert_received {:mix_shell, :info, [message]} assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Admin status .* false/ assert message =~ ~r/Moderator status .* false/
user = User.get_cached_by_nickname(user.nickname) user = User.get_cached_by_nickname(user.nickname)
refute user.is_moderator refute user.is_moderator
refute user.locked refute user.locked
refute user.is_admin refute user.is_admin
assert user.confirmation_pending
end end
test "no user to set status" do test "no user to set status" do
@ -554,4 +571,44 @@ test "it prints an error message when user is not exist" do
assert message =~ "Could not change user tags" assert message =~ "Could not change user tags"
end end
end end
describe "bulk confirm and unconfirm" do
test "confirm all" do
user1 = insert(:user, confirmation_pending: true)
user2 = insert(:user, confirmation_pending: true)
assert user1.confirmation_pending
assert user2.confirmation_pending
Mix.Tasks.Pleroma.User.run(["confirm_all"])
user1 = User.get_cached_by_nickname(user1.nickname)
user2 = User.get_cached_by_nickname(user2.nickname)
refute user1.confirmation_pending
refute user2.confirmation_pending
end
test "unconfirm all" do
user1 = insert(:user, confirmation_pending: false)
user2 = insert(:user, confirmation_pending: false)
admin = insert(:user, is_admin: true, confirmation_pending: false)
mod = insert(:user, is_moderator: true, confirmation_pending: false)
refute user1.confirmation_pending
refute user2.confirmation_pending
Mix.Tasks.Pleroma.User.run(["unconfirm_all"])
user1 = User.get_cached_by_nickname(user1.nickname)
user2 = User.get_cached_by_nickname(user2.nickname)
admin = User.get_cached_by_nickname(admin.nickname)
mod = User.get_cached_by_nickname(mod.nickname)
assert user1.confirmation_pending
assert user2.confirmation_pending
refute admin.confirmation_pending
refute mod.confirmation_pending
end
end
end end