Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: Delete inactive users automatically after 6 months #739

Merged
merged 4 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified locales/fr_FR/LC_MESSAGES/main.mo
Binary file not shown.
62 changes: 52 additions & 10 deletions locales/fr_FR/LC_MESSAGES/main.po
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
msgid ""
msgstr ""
"Project-Id-Version: Flus\n"
"POT-Creation-Date: 2024-11-13 11:36+0100\n"
"PO-Revision-Date: 2024-11-13 11:36+0100\n"
"POT-Creation-Date: 2024-11-28 15:50+0100\n"
"PO-Revision-Date: 2024-11-28 15:56+0100\n"
"Last-Translator: Marien Fressinaud <dev@marienfressinaud.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
Expand Down Expand Up @@ -117,7 +117,7 @@ msgstr "L’URL est invalide."
msgid "The Mastodon host returned an error, please try later."
msgstr "Le serveur Mastodon a renvoyé une erreur, veuillez essayer plus tard."

#: controllers/Passwords.php:67 controllers/Sessions.php:96 models/User.php:49
#: controllers/Passwords.php:67 controllers/Sessions.php:96 models/User.php:55
msgid "The address email is invalid."
msgstr "L’adresse courriel est invalide."

Expand Down Expand Up @@ -285,6 +285,11 @@ msgstr "[%s] Confirmer votre compte"
msgid "[%s] Reset your password"
msgstr "[%s] Réinitialisation de votre mot de passe"

#: mailers/Users.php:105
#, php-format
msgid "[%s] Your account will be deleted soon due to inactivity"
msgstr "[%s] Votre compte sera bientôt supprimé pour cause d'inactivité"

#: models/Collection.php:38 models/Group.php:29
msgid "The name is required."
msgstr "Le nom est obligatoire."
Expand Down Expand Up @@ -349,31 +354,31 @@ msgstr "Le nom est obligatoire."
msgid "The label must be less than {max} characters."
msgstr "Le nom doit faire moins de {max} caractères."

#: models/User.php:46
#: models/User.php:52
msgid "The address email is required."
msgstr "L’adresse courriel est obligatoire."

#: models/User.php:55
#: models/User.php:61
msgid "The username is required."
msgstr "Le nom d’utilisateur·ice est obligatoire."

#: models/User.php:59
#: models/User.php:65
msgid "The username must be less than {max} characters."
msgstr "Le nom d’utilisateur·ice ne doit pas faire plus de {max} caractères."

#: models/User.php:63
#: models/User.php:69
msgid "The username cannot contain the character ‘@’."
msgstr "Le nom d’utilisateur·ice ne doit pas contenir le caractère ‘@’."

#: models/User.php:69
#: models/User.php:75
msgid "The password is required."
msgstr "Le mot de passe est obligatoire."

#: models/User.php:75
#: models/User.php:81
msgid "The locale is required."
msgstr "La langue est obligatoire."

#: models/User.php:78
#: models/User.php:84
msgid "The locale is invalid."
msgstr "La langue est invalide."

Expand Down Expand Up @@ -1737,6 +1742,43 @@ msgstr ""
msgid "Have a nice day!"
msgstr "Bonne journée !"

#: views/mailers/users/inactivity_email.phtml:2
#, php-format
msgid "Hello %s,"
msgstr "Bonjour %s,"

#: views/mailers/users/inactivity_email.phtml:6
#, php-format
msgid ""
"You receive this email because you haven’t been active on %s for several "
"months. To avoid storing outdated data, your account will be deleted after "
"one month. If you don’t want to keep it, you don’t have to do anything. "
"However, if you wish to keep your account, you should login to %s by "
"clicking on the following link:"
msgstr ""
"Vous recevez ce courriel parce que vous n’avez pas été actif sur %s depuis "
"plusieurs mois. Pour éviter de conserver des données obsolètes, votre compte "
"sera supprimé dans un mois. Si vous ne souhaitez pas le conserver, vous "
"n’avez rien faire. Toutefois, si vous souhaitez conserver votre compte, vous "
"devez vous connecter à %s en cliquant sur le lien suivant :"

#: views/mailers/users/inactivity_email.phtml:13
msgid ""
"Note that you will not receive any further notification that your account "
"will be deleted."
msgstr ""
"Notez que vous ne recevrez pas d’autre notification concernant la "
"suppression de votre compte."

#: views/mailers/users/inactivity_email.phtml:17
msgid "Best regards,"
msgstr "Bien à vous,"

#: views/mailers/users/inactivity_email.phtml:21
#, php-format
msgid "The %s robot"
msgstr "Le robot %s"

#: views/mailers/users/reset_password_email.phtml:2
#, php-format
msgid "Hi %s,"
Expand Down
6 changes: 6 additions & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ public function run(\Minz\Request $request): mixed
return \Minz\Response::redirect('account');
}

// Track the last activity of the user
$changed = $current_user->refreshLastActivity();
if ($changed) {
$current_user->save();
}

$beta_enabled = models\FeatureFlag::isEnabled('beta', $current_user->id);

if ($current_user->autoload_modal === 'showcase navigation') {
Expand Down
1 change: 1 addition & 0 deletions src/cli/Jobs.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function install(Request $request): Response

\App\jobs\scheduled\FeedsSync::install();
\App\jobs\scheduled\LinksSync::install();
\App\jobs\scheduled\InactivityNotifier::install();
\App\jobs\scheduled\Cleaner::install();
if ($subscriptions_enabled) {
\App\jobs\scheduled\SubscriptionsSync::install();
Expand Down
6 changes: 4 additions & 2 deletions src/jobs/scheduled/Cleaner.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use App\models;
use App\services;
use App\utils;

/**
* Job to clean the system.
Expand Down Expand Up @@ -45,7 +44,10 @@ public function perform(): void
models\FetchLog::deleteOlderThan(\Minz\Time::ago(3, 'days'));
models\Token::deleteExpired();
models\Session::deleteExpired();
models\User::deleteNotValidatedOlderThan(\Minz\Time::ago(6, 'months'));
models\User::deleteInactiveAndNotified(
inactive_since: \Minz\Time::ago(6, 'months'),
notified_since: \Minz\Time::ago(1, 'month'),
);
models\Collection::deleteUnfollowedOlderThan($support_user->id, \Minz\Time::ago(7, 'days'));
models\Link::deleteNotStoredOlderThan($support_user->id, \Minz\Time::ago(7, 'days'));
/** @var int */
Expand Down
57 changes: 57 additions & 0 deletions src/jobs/scheduled/InactivityNotifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace App\jobs\scheduled;

use App\mailers;
use App\models;

/**
* Job to notify the users of their inactivity.
*
* @author Marien Fressinaud <dev@marienfressinaud.fr>
* @license http://www.gnu.org/licenses/agpl-3.0.en.html AGPL
*/
class InactivityNotifier extends \Minz\Job
{
/**
* Install the job in database.
*/
public static function install(): void
{
$job = new self();

if (!\Minz\Job::existsBy(['name' => $job->name])) {
$perform_at = \Minz\Time::relative('tomorrow 7:00');
$job->performLater($perform_at);
}
}

public function __construct()
{
parent::__construct();
$this->frequency = '+1 day';
}

public function perform(): void
{
$inactive_since = \Minz\Time::ago(5, 'months');
$inactive_users = models\User::listInactiveAndNotNotified($inactive_since);

$mailer = new mailers\Users();

foreach ($inactive_users as $user) {
if ($user->validated_at) {
$success = $mailer->sendInactivityEmail($user->id);
} else {
$success = true;
}

if ($success) {
$user->deletion_notified_at = \Minz\Time::now();
$user->save();
}

sleep(1);
}
}
}
38 changes: 38 additions & 0 deletions src/mailers/Users.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,42 @@ public function sendResetPasswordEmail(string $user_id): bool
);
return $this->send($user->email, $subject);
}

/**
* Send an email to the given user to warn about its inactivity and the
* upcoming deletion of its account.
*/
public function sendInactivityEmail(string $user_id): bool
{
$user = models\User::find($user_id);
if (!$user) {
\Minz\Log::warning("Can’t send inactivity email to user {$user_id} (not found)");
return false;
}

if (!$user->isInactive(months: 5)) {
\Minz\Log::warning("Can’t send inactivity email to user {$user_id} (not inactive)");
return false;
}

if ($user->deletion_notified_at) {
\Minz\Log::warning("Can’t send inactivity email to user {$user_id} (already notified)");
return false;
}

utils\Locale::setCurrentLocale($user->locale);

/** @var string */
$brand = \Minz\Configuration::$application['brand'];
$subject = sprintf(_('[%s] Your account will be deleted soon due to inactivity'), $brand);
$this->setBody(
'mailers/users/inactivity_email.phtml',
'mailers/users/inactivity_email.txt',
[
'brand' => $brand,
'username' => $user->username,
]
);
return $this->send($user->email, $subject);
}
}
32 changes: 32 additions & 0 deletions src/migrations/Migration202411280001AddLastActivityAtToUsers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App\migrations;

class Migration202411280001AddLastActivityAtToUsers
{
public function migrate(): bool
{
$database = \Minz\Database::get();

$database->exec(<<<'SQL'
ALTER TABLE users
ADD COLUMN last_activity_at TIMESTAMPTZ NOT NULL DEFAULT date_trunc('second', NOW()),
ADD COLUMN deletion_notified_at TIMESTAMPTZ;
SQL);

return true;
}

public function rollback(): bool
{
$database = \Minz\Database::get();

$database->exec(<<<'SQL'
ALTER TABLE users
DROP COLUMN last_activity_at,
DROP COLUMN deletion_notified_at;
SQL);

return true;
}
}
35 changes: 35 additions & 0 deletions src/models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class User
#[Database\Column]
public \DateTimeImmutable $subscription_expired_at;

#[Database\Column]
public \DateTimeImmutable $last_activity_at;

#[Database\Column]
public ?\DateTimeImmutable $deletion_notified_at;

#[Database\Column]
#[Validable\Presence(
message: new Translatable('The address email is required.'),
Expand Down Expand Up @@ -98,6 +104,7 @@ public function __construct(string $username, string $email, string $password)
{
$this->id = \Minz\Random::timebased();
$this->subscription_expired_at = \Minz\Time::fromNow(1, 'month');
$this->last_activity_at = \Minz\Time::now();
$this->username = trim($username);
$this->email = \Minz\Email::sanitize($email);
$this->password_hash = self::passwordHash($password);
Expand Down Expand Up @@ -503,6 +510,34 @@ public function isBlocked(): bool
return $must_validate || $must_renew;
}

/**
* Change the last activity attribute. Return true if the date changed,
* false otherwise. Only the day is remembered to not track the user too
* much and to avoid to save the user at each request.
*/
public function refreshLastActivity(): bool
{
$changed = false;
$today = \Minz\Time::relative('today');

if ($this->last_activity_at != $today) {
$this->last_activity_at = $today;
$changed = true;
}

if ($this->deletion_notified_at !== null) {
$this->deletion_notified_at = null;
$changed = true;
}

return $changed;
}

public function isInactive(int $months = 6): bool
{
return $this->last_activity_at < \Minz\Time::ago($months, 'months');
}

/**
* Return a tag URI that can be used as Atom id
*
Expand Down
Loading
Loading