From 14524db269aafbd6979789220a81593d5f4e56af Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 28 Nov 2024 11:17:07 +0100 Subject: [PATCH 1/4] Track users last activity day --- src/Application.php | 6 ++++ ...on202411280001AddLastActivityAtToUsers.php | 32 +++++++++++++++++++ src/models/User.php | 30 +++++++++++++++++ src/schema.sql | 3 ++ tests/ApplicationTest.php | 20 ++++++++++++ 5 files changed, 91 insertions(+) create mode 100644 src/migrations/Migration202411280001AddLastActivityAtToUsers.php diff --git a/src/Application.php b/src/Application.php index f36342e2..23d22c76 100644 --- a/src/Application.php +++ b/src/Application.php @@ -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') { diff --git a/src/migrations/Migration202411280001AddLastActivityAtToUsers.php b/src/migrations/Migration202411280001AddLastActivityAtToUsers.php new file mode 100644 index 00000000..7acf0ad5 --- /dev/null +++ b/src/migrations/Migration202411280001AddLastActivityAtToUsers.php @@ -0,0 +1,32 @@ +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; + } +} diff --git a/src/models/User.php b/src/models/User.php index 6886fcb5..48a4cf8a 100644 --- a/src/models/User.php +++ b/src/models/User.php @@ -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.'), @@ -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); @@ -503,6 +510,29 @@ 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; + } + /** * Return a tag URI that can be used as Atom id * diff --git a/src/schema.sql b/src/schema.sql index 4d642748..a1efb5ea 100644 --- a/src/schema.sql +++ b/src/schema.sql @@ -40,6 +40,9 @@ CREATE TABLE users ( validation_token TEXT REFERENCES tokens ON DELETE SET NULL ON UPDATE CASCADE, reset_token TEXT REFERENCES tokens ON DELETE SET NULL ON UPDATE CASCADE, + last_activity_at TIMESTAMPTZ NOT NULL DEFAULT date_trunc('second', NOW()), + deletion_notified_at TIMESTAMPTZ, + subscription_account_id TEXT, subscription_expired_at TIMESTAMPTZ NOT NULL diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 4d97998d..c11302b0 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -10,6 +10,7 @@ class ApplicationTest extends \PHPUnit\Framework\TestCase { use \Minz\Tests\InitializerHelper; use \Minz\Tests\ResponseAsserts; + use \Minz\Tests\TimeHelper; use \tests\FakerHelper; use \tests\LoginHelper; @@ -81,6 +82,25 @@ public function testRunSetsCurrentUserFromCookie(): void $this->assertSame($user->id, $current_user->id); } + public function testRunRefreshesLastActivity(): void + { + $last_activity = new \DateTimeImmutable('2024-11-01'); + $current_datetime = new \DateTimeImmutable('2024-11-30 12:42:42'); + $current_date = new \DateTimeImmutable('2024-11-30 00:00:00'); + $this->freeze($current_datetime); + $user = $this->login([ + 'last_activity_at' => $last_activity, + ]); + $request = new \Minz\Request('GET', '/news'); + + $application = new Application(); + $response = $application->run($request); + + $this->assertResponseCode($response, 200); + $user = $user->reload(); + $this->assertEquals($current_date, $user->last_activity_at); + } + public function testRunSetsAutoloadModal(): void { $user = $this->login([ From cc2fde47a9356934d82969998d3b335abd80acff Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 28 Nov 2024 11:55:14 +0100 Subject: [PATCH 2/4] Notify users about their inactivity and account deletion --- src/cli/Jobs.php | 1 + src/jobs/scheduled/InactivityNotifier.php | 57 ++++++ src/mailers/Users.php | 38 ++++ src/models/User.php | 5 + src/models/dao/User.php | 24 +++ .../mailers/users/inactivity_email.phtml | 22 +++ src/views/mailers/users/inactivity_email.txt | 15 ++ .../jobs/scheduled/InactivityNotifierTest.php | 164 ++++++++++++++++++ 8 files changed, 326 insertions(+) create mode 100644 src/jobs/scheduled/InactivityNotifier.php create mode 100644 src/views/mailers/users/inactivity_email.phtml create mode 100644 src/views/mailers/users/inactivity_email.txt create mode 100644 tests/jobs/scheduled/InactivityNotifierTest.php diff --git a/src/cli/Jobs.php b/src/cli/Jobs.php index 12b0dcb5..2096b230 100644 --- a/src/cli/Jobs.php +++ b/src/cli/Jobs.php @@ -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(); diff --git a/src/jobs/scheduled/InactivityNotifier.php b/src/jobs/scheduled/InactivityNotifier.php new file mode 100644 index 00000000..12330083 --- /dev/null +++ b/src/jobs/scheduled/InactivityNotifier.php @@ -0,0 +1,57 @@ + + * @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); + } + } +} diff --git a/src/mailers/Users.php b/src/mailers/Users.php index 3fa6520c..dd6e2ee8 100644 --- a/src/mailers/Users.php +++ b/src/mailers/Users.php @@ -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); + } } diff --git a/src/models/User.php b/src/models/User.php index 48a4cf8a..0e42e242 100644 --- a/src/models/User.php +++ b/src/models/User.php @@ -533,6 +533,11 @@ public function refreshLastActivity(): bool 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 * diff --git a/src/models/dao/User.php b/src/models/dao/User.php index 1aba6114..b66bf87e 100644 --- a/src/models/dao/User.php +++ b/src/models/dao/User.php @@ -201,4 +201,28 @@ public static function listBySubscriptionExpiredAtBefore(\DateTimeImmutable $bef return self::fromDatabaseRows($statement->fetchAll()); } + + /** + * Return the users that haven't be active since the given date. + * + * @return self[] + */ + public static function listInactiveAndNotNotified(\DateTimeImmutable $inactive_since): array + { + $sql = <<<'SQL' + SELECT * FROM users + WHERE last_activity_at <= :inactive_since + AND deletion_notified_at IS NULL + AND id != :support_user_id + SQL; + + $database = Database::get(); + $statement = $database->prepare($sql); + $statement->execute([ + ':inactive_since' => $inactive_since->format(Database\Column::DATETIME_FORMAT), + ':support_user_id' => models\User::supportUser()->id, + ]); + + return self::fromDatabaseRows($statement->fetchAll()); + } } diff --git a/src/views/mailers/users/inactivity_email.phtml b/src/views/mailers/users/inactivity_email.phtml new file mode 100644 index 00000000..c43fc96b --- /dev/null +++ b/src/views/mailers/users/inactivity_email.phtml @@ -0,0 +1,22 @@ +

+ +

+ +

+
+ + + +

+ +

+ +

+ +

+ +

+ +

+ +

diff --git a/src/views/mailers/users/inactivity_email.txt b/src/views/mailers/users/inactivity_email.txt new file mode 100644 index 00000000..02b0a07b --- /dev/null +++ b/src/views/mailers/users/inactivity_email.txt @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/tests/jobs/scheduled/InactivityNotifierTest.php b/tests/jobs/scheduled/InactivityNotifierTest.php new file mode 100644 index 00000000..2bed37b1 --- /dev/null +++ b/tests/jobs/scheduled/InactivityNotifierTest.php @@ -0,0 +1,164 @@ +assertSame('default', $job->queue); + } + + public function testSchedule(): void + { + $job = new InactivityNotifier(); + + $this->assertSame('+1 day', $job->frequency); + } + + public function testInstall(): void + { + \Minz\Configuration::$jobs_adapter = 'database'; + + $this->assertSame(0, \Minz\Job::count()); + + InactivityNotifier::install(); + + \Minz\Configuration::$jobs_adapter = 'test'; + + $this->assertSame(1, \Minz\Job::count()); + } + + public function testPerformNotifiesUserAboutInactivity(): void + { + $this->freeze(); + $now = \Minz\Time::now(); + $job = new InactivityNotifier(); + $inactivity_months = 5; + $notified_at = null; + $validated_at = \Minz\Time::ago(1, 'year'); + $user = UserFactory::create([ + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => $notified_at, + 'validated_at' => $validated_at, + ]); + + $job->perform(); + + $user = $user->reload(); + $this->assertSame($now->getTimestamp(), $user->deletion_notified_at?->getTimestamp()); + $this->assertEmailsCount(1); + $email_sent = \Minz\Tests\Mailer::take(); + $this->assertNotNull($email_sent); + $this->assertEmailSubject($email_sent, '[Flus] Your account will be deleted soon due to inactivity'); + $this->assertEmailContainsTo($email_sent, $user->email); + $this->assertEmailContainsBody( + $email_sent, + 'You receive this email because you haven’t been active on Flus for several months.' + ); + } + + public function testPerformDoesNotSendEmailToNotValidatedUsers(): void + { + $this->freeze(); + $now = \Minz\Time::now(); + $job = new InactivityNotifier(); + $inactivity_months = 5; + $notified_at = null; + $validated_at = null; + $user = UserFactory::create([ + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => $notified_at, + 'validated_at' => $validated_at, + ]); + + $job->perform(); + + $user = $user->reload(); + $this->assertSame($now->getTimestamp(), $user->deletion_notified_at?->getTimestamp()); + $this->assertEmailsCount(0); + } + + public function testPerformIgnoresAlreadyNotifiedUsers(): void + { + $this->freeze(); + $now = \Minz\Time::now(); + $job = new InactivityNotifier(); + $inactivity_months = 5; + $notified_at = \Minz\Time::ago(1, 'month'); + $validated_at = \Minz\Time::ago(1, 'year'); + $user = UserFactory::create([ + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => $notified_at, + 'validated_at' => $validated_at, + ]); + + $job->perform(); + + $user = $user->reload(); + $this->assertSame($notified_at->getTimestamp(), $user->deletion_notified_at?->getTimestamp()); + $this->assertEmailsCount(0); + } + + public function testPerformIgnoresNotYetInactive(): void + { + $this->freeze(); + $now = \Minz\Time::now(); + $job = new InactivityNotifier(); + $inactivity_months = 4; + $notified_at = null; + $validated_at = \Minz\Time::ago(1, 'year'); + $user = UserFactory::create([ + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => $notified_at, + 'validated_at' => $validated_at, + ]); + + $job->perform(); + + $user = $user->reload(); + $this->assertNull($user->deletion_notified_at); + $this->assertEmailsCount(0); + } + + public function testPerformIgnoresSupportUser(): void + { + $this->freeze(); + $now = \Minz\Time::now(); + $job = new InactivityNotifier(); + /** @var string */ + $support_email = \Minz\Configuration::$application['support_email']; + $inactivity_months = 5; + $notified_at = null; + $validated_at = \Minz\Time::ago(1, 'year'); + $user = UserFactory::create([ + 'email' => \Minz\Email::sanitize($support_email), + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => $notified_at, + 'validated_at' => $validated_at, + ]); + + $job->perform(); + + $user = $user->reload(); + $this->assertNull($user->deletion_notified_at); + $this->assertEmailsCount(0); + } +} From f9ebbfcf5c130ce41df6e416bb10a01dfb7f5a6b Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 28 Nov 2024 15:28:03 +0100 Subject: [PATCH 3/4] Delete inactive users --- src/jobs/scheduled/Cleaner.php | 6 +- src/models/dao/User.php | 17 +++-- tests/jobs/scheduled/CleanerTest.php | 94 ++++++++++++++++++++-------- 3 files changed, 82 insertions(+), 35 deletions(-) diff --git a/src/jobs/scheduled/Cleaner.php b/src/jobs/scheduled/Cleaner.php index 5f7c8e5b..09bfbc02 100644 --- a/src/jobs/scheduled/Cleaner.php +++ b/src/jobs/scheduled/Cleaner.php @@ -4,7 +4,6 @@ use App\models; use App\services; -use App\utils; /** * Job to clean the system. @@ -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 */ diff --git a/src/models/dao/User.php b/src/models/dao/User.php index b66bf87e..a0c8aaa2 100644 --- a/src/models/dao/User.php +++ b/src/models/dao/User.php @@ -124,20 +124,25 @@ public static function countActivePerMonth(int $year): array } /** - * Delete not validated users older than the given date. + * Delete the inactive users that have been notified about it. */ - public static function deleteNotValidatedOlderThan(\DateTimeImmutable $date): bool - { + public static function deleteInactiveAndNotified( + \DateTimeImmutable $inactive_since, + \DateTimeImmutable $notified_since + ): bool { $sql = <<prepare($sql); return $statement->execute([ - $date->format(Database\Column::DATETIME_FORMAT), + ':inactive_since' => $inactive_since->format(Database\Column::DATETIME_FORMAT), + ':notified_since' => $notified_since->format(Database\Column::DATETIME_FORMAT), + ':support_user_id' => models\User::supportUser()->id, ]); } diff --git a/tests/jobs/scheduled/CleanerTest.php b/tests/jobs/scheduled/CleanerTest.php index a80709e9..7c678694 100644 --- a/tests/jobs/scheduled/CleanerTest.php +++ b/tests/jobs/scheduled/CleanerTest.php @@ -137,18 +137,15 @@ public function testPerformKeepsCurrentSession(): void $this->assertTrue(models\Token::exists($token->token)); } - public function testPerformDeletesOldInvalidatedUsers(): void + public function testPerformDeletesInactiveAndNotifiedUsers(): void { - /** @var \DateTimeImmutable */ - $now = $this->fake('dateTime'); - $this->freeze($now); + $this->freeze(); $cleaner_job = new Cleaner(); - /** @var int */ - $months = $this->fake('numberBetween', 7, 24); - $created_at = \Minz\Time::ago($months, 'months'); + $inactivity_months = 6; + $notified_months = 1; $user = UserFactory::create([ - 'created_at' => $created_at, - 'validated_at' => null, + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => \Minz\Time::ago($notified_months, 'months'), ]); $cleaner_job->perform(); @@ -156,18 +153,14 @@ public function testPerformDeletesOldInvalidatedUsers(): void $this->assertFalse(models\User::exists($user->id)); } - public function testPerformKeepsOldValidatedUsers(): void + public function testPerformKeepsInactiveButNotNotifiedUsers(): void { - /** @var \DateTimeImmutable */ - $now = $this->fake('dateTime'); - $this->freeze($now); + $this->freeze(); $cleaner_job = new Cleaner(); - /** @var int */ - $months = $this->fake('numberBetween', 7, 24); - $created_at = \Minz\Time::ago($months, 'months'); + $inactivity_months = 6; $user = UserFactory::create([ - 'created_at' => $created_at, - 'validated_at' => \Minz\Time::now(), + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => null, ]); $cleaner_job->perform(); @@ -175,18 +168,65 @@ public function testPerformKeepsOldValidatedUsers(): void $this->assertTrue(models\User::exists($user->id)); } - public function testPerformKeepsRecentInvalidatedUsers(): void + public function testPerformKeepsRecentNotifiedUser(): void { - /** @var \DateTimeImmutable */ - $now = $this->fake('dateTime'); - $this->freeze($now); + $this->freeze(); $cleaner_job = new Cleaner(); - /** @var int */ - $months = $this->fake('numberBetween', 0, 6); - $created_at = \Minz\Time::ago($months, 'months'); + $inactivity_months = 6; + $notified_months = 0; $user = UserFactory::create([ - 'created_at' => $created_at, - 'validated_at' => null, + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => \Minz\Time::ago($notified_months, 'months'), + ]); + + $cleaner_job->perform(); + + $this->assertTrue(models\User::exists($user->id)); + } + + public function testPerformKeepsRecentInactiveButNotifiedUsers(): void + { + $this->freeze(); + $cleaner_job = new Cleaner(); + $inactivity_months = 5; + $notified_months = 1; + $user = UserFactory::create([ + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => \Minz\Time::ago($notified_months, 'months'), + ]); + + $cleaner_job->perform(); + + $this->assertTrue(models\User::exists($user->id)); + } + + public function testPerformKeepsActiveUsers(): void + { + $this->freeze(); + $cleaner_job = new Cleaner(); + $inactivity_months = 0; + $user = UserFactory::create([ + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => null, + ]); + + $cleaner_job->perform(); + + $this->assertTrue(models\User::exists($user->id)); + } + + public function testPerformKeepsSupportUser(): void + { + $this->freeze(); + $cleaner_job = new Cleaner(); + /** @var string */ + $support_email = \Minz\Configuration::$application['support_email']; + $inactivity_months = 6; + $notified_months = 1; + $user = UserFactory::create([ + 'last_activity_at' => \Minz\Time::ago($inactivity_months, 'months'), + 'deletion_notified_at' => \Minz\Time::ago($notified_months, 'months'), + 'email' => $support_email, ]); $cleaner_job->perform(); From 0d025d5c5bf19e3f88c39d1c371dc592032b8079 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 28 Nov 2024 15:56:59 +0100 Subject: [PATCH 4/4] Update the French translation --- locales/fr_FR/LC_MESSAGES/main.mo | Bin 58169 -> 59375 bytes locales/fr_FR/LC_MESSAGES/main.po | 62 +++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/locales/fr_FR/LC_MESSAGES/main.mo b/locales/fr_FR/LC_MESSAGES/main.mo index 4e43e9cb362a496407340bc486d0c8db3d21941e..b3724c4740bfdd7aad58ced2a849bc06dac3621a 100644 GIT binary patch delta 13194 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 N=2UEP^E1^d{RhGw)JOmT delta 12138 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𝔱(tV48cxK$#DBLi3VdsEKB9gWQ9$Gqfy6l?g!4jC z+iM0V(RR`nowrQlzP>xHzyTY`e&NJV>`~jWzRe@qKhHL*sxd??bz89&^+$^@ z$_>an;a1|&R+Z!w<;t8p%7g3c-S-0(eIFfxIX16B{a57cFo+YhJs^)nAIf*hvxuK5 z_r^ffhrkf>r-*~ZPsC|r9rgcWFz2OW1P<2t2N2q(Qd!epfgsUL|a1NNVLUMKL^)x>}0HuKVUDyJ^xp!SVo~MQJQ=WF2dDB zS@KG1nEcU3(J8}^Kd1}EYdD*_*RVS=l>9fM9C-loF;Pr;A2Ef9Cj5w*9$fzyboP%6}8x$+aaCZ&5zxE;0TC z_Jl8~_{CkxZ-N+O>;E9nAiNmqQWjBs!C~!54{299-0laxmiH;Ffimxz_x$u^t3 z3x0;r;Q``fVmZ;D7)WS)n*EzxBuCvve(9z{+jycLc|P*()Om+IhR#gFcZq(4w%3Vs z#8;F*As&+Zaolfq|4WqP$-598h$F-x;sfFtLYu1sKN?dw>@M|KZ*I8|5E>{O!!YDY2?{NV-76A z+gRT2-%kE$`;_Dk(UE<@L~pxev^{nyuBNUGQA8}X^*8WUz5iC)iY#_U{iib&ofP)H zr-W^%J)d IrH1$a0cb&VFaQ7m diff --git a/locales/fr_FR/LC_MESSAGES/main.po b/locales/fr_FR/LC_MESSAGES/main.po index af847dd0..389a8891 100644 --- a/locales/fr_FR/LC_MESSAGES/main.po +++ b/locales/fr_FR/LC_MESSAGES/main.po @@ -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 \n" "Language-Team: \n" "Language: fr_FR\n" @@ -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." @@ -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." @@ -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." @@ -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,"