From 2fe92e8d2e583d0fc830bb45d4621de45d6975ab Mon Sep 17 00:00:00 2001 From: "Florine W. Dekker" Date: Wed, 16 Nov 2022 21:33:04 +0100 Subject: [PATCH] Add "alive again" notifications and similar --- composer.json | 2 +- composer.lock | Bin 14959 -> 14959 bytes package-lock.json | Bin 225101 -> 225103 bytes package.json | 2 +- src/main/index.html | 1 - src/main/js/Main.ts | 6 +- src/main/php/Database.php | 35 ++++++- src/main/php/Mailer.php | 174 ++++++++++++++++++++++++++----- src/main/php/Mediawiki.php | 59 +++++------ src/main/php/TrackingManager.php | 117 +++++++++++++-------- src/main/php/Validator.php | 2 +- 11 files changed, 289 insertions(+), 109 deletions(-) diff --git a/composer.json b/composer.json index 409b086..f9b4254 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "fwdekker/death-notifier", "description": "Get notified when a famous person dies.", - "version": "0.9.2", "_comment_version": "Also update version in `package.json`!", + "version": "0.10.0", "_comment_version": "Also update version in `package.json`!", "type": "project", "license": "MIT", "homepage": "https://git.fwdekker.com/tools/death-notifier", diff --git a/composer.lock b/composer.lock index 250aa8e0bb54a1e490906033d0e73d32a0b849d2..1a36da15ec797a33f0880f7ed626683ed2e30deb 100644 GIT binary patch delta 45 zcmaD~^1ft)Fr$J=a*AbIqLHz2qM?PUL5i_uibax{simPss&R6XMWXR$V@4ZG0AJA! A%>V!Z delta 45 zcmaD~^1ft)Frz}UnL%<|TB4<8a-xN?L1Ln%S+cQ_iLt4nscDjNlDXk#V@4ZG0A}?K AzyJUM diff --git a/package-lock.json b/package-lock.json index ca75fe9f467e4222ee846943af1f3bfbc5ce3f05..c0e0d9856a6ebfd6cab066f01ccf6d2b9b7efa08 100644 GIT binary patch delta 33 rcmV++0N(%2-VM**4UjSfF)%JLku{T($e9#@+lAW!h1&wP+XHfL`?L=0 delta 36 qcmX?qkN507-U&v`mU>1L%_f7W$&CEMjSpHMFt$Em+WLSwKMw!|L=GST diff --git a/package.json b/package.json index 46151c0..30231fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "death-notifier", - "version": "0.9.2", "_comment_version": "Also update version in `composer.json`!", + "version": "0.10.0", "_comment_version": "Also update version in `composer.json`!", "description": "Get notified when a famous person dies.", "author": "Florine W. Dekker", "browser": "dist/bundle.js", diff --git a/src/main/index.html b/src/main/index.html index 6c1612b..16cda1e 100644 --- a/src/main/index.html +++ b/src/main/index.html @@ -37,7 +37,6 @@

-

diff --git a/src/main/js/Main.ts b/src/main/js/Main.ts index 14d6369..0d3640d 100644 --- a/src/main/js/Main.ts +++ b/src/main/js/Main.ts @@ -45,14 +45,14 @@ function refreshTrackings(): void { const statusCell = document.createElement("td"); let statusText; if (tracking.deleted) { - statusText = "page not found"; + statusText = "article not found"; } else { switch (tracking.name) { case "Adolf Hitler": - statusText = "deceased 🥳"; + statusText = "dead 🥳"; break; case "Vladimir Putin": - statusText = tracking.status === "alive" ? "alive ☹️" : "deceased 🥳"; + statusText = tracking.status === "alive" ? "alive ☹️" : "dead 🥳"; break; default: statusText = tracking.status; diff --git a/src/main/php/Database.php b/src/main/php/Database.php index 6f154d3..78c4e3b 100644 --- a/src/main/php/Database.php +++ b/src/main/php/Database.php @@ -102,6 +102,7 @@ class Database // Execute migration code if (Comparator::lessThan($db_version, "0.5.0")) self::migrate_0_5_0(); if (Comparator::lessThan($db_version, "0.8.0")) self::migrate_0_8_0(); + if (Comparator::lessThan($db_version, "0.10.0")) self::migrate_0_10_0(); // Update version $stmt = $this->conn->prepare("UPDATE meta SET v=:version WHERE k='version';"); @@ -134,7 +135,7 @@ class Database } /** - * Migrates the database from a previous version to one compatible with v0.5.0. + * Migrates the database from a previous version to one compatible with v0.8.0. * * @return void * @noinspection SqlResolve Function necessarily refers to old scheme which is not detected by tools @@ -161,6 +162,38 @@ class Database } } + /** + * Migrates the database from a previous version to one compatible with v0.10.0. + * + * @return void + * @noinspection SqlResolve Function necessarily refers to old scheme which is not detected by tools + */ + private function migrate_0_10_0(): void + { + $this->logger->info("Migrating to v0.10.0."); + + $res = + $this->conn->exec("CREATE TABLE new_email_tasks(type TEXT NOT NULL, + recipient TEXT NOT NULL, + arg1 TEXT NOT NULL DEFAULT(''), + arg2 TEXT NOT NULL DEFAULT(''), + PRIMARY KEY (type, recipient, arg1, arg2));") !== false && + $this->conn->exec("INSERT INTO new_email_tasks (type, recipient, arg1) + SELECT type, recipient, arg1 + FROM email_tasks;") !== false && + $this->conn->exec("DROP TABLE email_tasks;") !== false && + $this->conn->exec("ALTER TABLE new_email_tasks RENAME TO email_tasks;") !== false && + $this->conn->exec("UPDATE email_tasks + SET type='notify-status-changed' AND arg2='dead' + WHERE type='notify-death'") !== false; + + if (!$res) { + $this->logger->error("Failed migrating to v0.10.0.", ["error" => $this->conn->errorInfo()]); + $this->conn->rollBack(); + Util::http_exit(500); + } + } + /** * Executes `lambda` within a single transaction, allowing nesting. diff --git a/src/main/php/Mailer.php b/src/main/php/Mailer.php index 59c2fc1..6be149d 100644 --- a/src/main/php/Mailer.php +++ b/src/main/php/Mailer.php @@ -52,8 +52,9 @@ class Mailer { $this->conn->exec("CREATE TABLE email_tasks(type TEXT NOT NULL, recipient TEXT NOT NULL, - arg1 TEXT DEFAULT(NULL), - PRIMARY KEY (type, recipient, arg1));"); + arg1 TEXT NOT NULL DEFAULT(''), + arg2 TEXT NOT NULL DEFAULT(''), + PRIMARY KEY (type, recipient, arg1, arg2));"); } @@ -65,11 +66,12 @@ class Mailer */ public function queue_email(Email $email): Response { - $stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, recipient, arg1) - VALUES (:type, :recipient, :arg1);"); + $stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, recipient, arg1, arg2) + VALUES (:type, :recipient, :arg1, :arg2);"); $stmt->bindValue(":type", $email->type); $stmt->bindValue(":recipient", $email->recipient); $stmt->bindValue(":arg1", $email->arg1); + $stmt->bindValue(":arg2", $email->arg2); return $stmt->execute() ? Response::satisfied() : Response::unsatisfied(null); } @@ -102,19 +104,20 @@ class Mailer } // Get queue - $stmt = $this->conn->prepare("SELECT type, recipient, arg1 FROM email_tasks;"); + $stmt = $this->conn->prepare("SELECT type, recipient, arg1, arg2 FROM email_tasks;"); $stmt->execute(); $email_tasks = $stmt->fetchAll(PDO::FETCH_ASSOC); // Process queue $stmt = $this->conn->prepare("DELETE FROM email_tasks - WHERE type=:type AND recipient=:recipient AND arg1 IS :arg1;"); + WHERE type=:type AND recipient=:recipient AND arg1=:arg1 AND arg2=:arg2;"); $stmt->bindParam(":type", $type); $stmt->bindParam(":recipient", $recipient); $stmt->bindParam(":arg1", $arg1); - foreach ($email_tasks as ["type" => $type, "recipient" => $recipient, "arg1" => $arg1]) { + $stmt->bindParam(":arg2", $arg2); + foreach ($email_tasks as ["type" => $type, "recipient" => $recipient, "arg1" => $arg1, "arg2" => $arg2]) { try { - $email = Email::deserialize($type, $recipient, $arg1 ?? ""); + $email = Email::deserialize($type, $recipient, $arg1, $arg2); $mailer->Subject = $email->getSubject(); $mailer->Body = $email->getBody($this->config); @@ -161,9 +164,13 @@ abstract class Email */ public string $recipient; /** - * @var string|null The first argument to construct the email. + * @var string The first argument to construct the email. */ - public ?string $arg1; + public string $arg1 = ""; + /** + * @var string The second argument to construct the email. + */ + public string $arg2 = ""; /** @@ -190,10 +197,11 @@ abstract class Email * @param string $type the type of email to deserialize * @param string $recipient the intended recipient of the email * @param string $arg1 the first argument to construct the email + * @param string $arg2 the second argument to construct the email * @return Email a deserialized email * @throws \Exception if the `type` is not recognized */ - public static function deserialize(string $type, string $recipient, string $arg1): Email + public static function deserialize(string $type, string $recipient, string $arg1, string $arg2): Email { return match ($type) { RegisterEmail::TYPE => new RegisterEmail($recipient, $arg1), @@ -201,7 +209,9 @@ abstract class Email ChangedEmailEmail::TYPE => new ChangedEmailEmail($recipient, $arg1), ChangedPasswordEmail::TYPE => new ChangedPasswordEmail($recipient), ResetPasswordEmail::TYPE => new ResetPasswordEmail($recipient, $arg1), - NotifyDeathEmail::TYPE => new NotifyDeathEmail($recipient, $arg1), + NotifyArticleDeletedEmail::TYPE => new NotifyArticleDeletedEmail($recipient, $arg1), + NotifyArticleUndeletedEmail::TYPE => new NotifyArticleUndeletedEmail($recipient, $arg1), + NotifyStatusChangedEmail::TYPE => new NotifyStatusChangedEmail($recipient, $arg1, $arg2), default => throw new \Exception("Unknown email type $type."), }; } @@ -392,8 +402,6 @@ class ChangedPasswordEmail extends Email { { $this->type = self::TYPE; $this->recipient = $recipient; - - $this->arg1 = null; } @@ -473,26 +481,25 @@ class ResetPasswordEmail extends Email } /** - * An email to inform a user someone has died. + * An email to inform a user that a tracked article has been deleted. */ -class NotifyDeathEmail extends Email -{ +class NotifyArticleDeletedEmail extends Email { /** * A string identifying the type of email. */ - public const TYPE = "notify-death"; + public const TYPE = "notify-article-deleted"; /** - * @var string The name of the person who died. + * @var string The name of the article that was deleted. */ public string $name; /** - * Constructs an email to inform a user someone has died. + * Constructs an email to inform a user that a tracked article has been deleted. * * @param string $recipient the intended recipient of the email - * @param string $name the name of the person who died + * @param string $name the name of the article that was deleted */ public function __construct(string $recipient, string $name) { @@ -506,7 +513,7 @@ class NotifyDeathEmail extends Email public function getSubject(): string { - return "$this->name may have died"; + return "$this->name article has been deleted"; } public function getBody(array $config): string @@ -514,11 +521,126 @@ class NotifyDeathEmail extends Email $base_path = $config["server"]["base_path"]; return - "Someone has edited the Wikipedia page of $this->name to state that they have died. " . - "For more information, read their Wikipedia page at " . - "https://en.wikipedia.org/wiki/" . rawurlencode($this->name) . + "The Wikipedia article about $this->name has been deleted. " . + "Death Notifier is now unable to send you a notification if $this->name dies. " . + "If the Wikipedia article is ever re-created, Death Notifier will automatically resume tracking this " . + "article, and you will receive another notification." . "\n\n" . - "You are receiving this message because of your preferences in your Death Notifier account. " . + "You are receiving this message because of the preferences in your Death Notifier account. " . + "To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " . + "preferences." . + "\n\n" . + $base_path; + } +} + +/** + * An email to inform a user that a tracked article has been re-created. + */ +class NotifyArticleUndeletedEmail extends Email { + /** + * A string identifying the type of email. + */ + public const TYPE = "notify-article-undeleted"; + + /** + * @var string The name of the article that was re-created. + */ + public string $name; + + + /** + * Constructs an email to inform a user that a tracked article has been re-created. + * + * @param string $recipient the intended recipient of the email + * @param string $name the name of the article that was re-created + */ + public function __construct(string $recipient, string $name) + { + $this->type = self::TYPE; + $this->recipient = $recipient; + + $this->name = $name; + $this->arg1 = $name; + } + + + public function getSubject(): string + { + return "$this->name article has been re-created"; + } + + public function getBody(array $config): string + { + $base_path = $config["server"]["base_path"]; + + return + "The Wikipedia article about $this->name has been re-created. " . + "Death Notifier will once again track the article and notify you if $this->name dies." . + "\n\n" . + "You are receiving this message because of the preferences in your Death Notifier account. " . + "To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " . + "preferences." . + "\n\n" . + $base_path; + } +} + +/** + * An email to inform a user a tracker person's status has changed. + */ +class NotifyStatusChangedEmail extends Email +{ + /** + * A string identifying the type of email. + */ + public const TYPE = "notify-status-changed"; + + /** + * @var string The name of the person whose status has changed. + */ + public string $name; + /** + * @var string The new status of the person. + */ + public string $new_status; + + + /** + * Constructs an email to inform a user someone has died. + * + * @param string $recipient the intended recipient of the email + * @param string $name the name of the person who died + * @param string $new_status the new status of the person + */ + public function __construct(string $recipient, string $name, string $new_status) + { + $this->type = self::TYPE; + $this->recipient = $recipient; + + $this->name = $name; + $this->arg1 = $name; + + $this->new_status = $new_status; + $this->arg2 = $new_status; + } + + + public function getSubject(): string + { + return "$this->name may be $this->new_status"; + } + + public function getBody(array $config): string + { + $base_path = $config["server"]["base_path"]; + + return + "Someone has edited Wikipedia to state that $this->name is $this->new_status. " . + "For more information, read their Wikipedia article at " . + "https://en.wikipedia.org/wiki/" . rawurlencode($this->name) . + "\n\n" . + "You are receiving this message because of the preferences in your Death Notifier account. " . "To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " . "preferences." . "\n\n" . diff --git a/src/main/php/Mediawiki.php b/src/main/php/Mediawiki.php index 10d51fd..3f3589a 100644 --- a/src/main/php/Mediawiki.php +++ b/src/main/php/Mediawiki.php @@ -22,9 +22,10 @@ class Mediawiki "death-notifier/%%VERSION_NUMBER%% " . "(https://git.fwdekker.com/tools/death-notifier; florine@fwdekker.com)"; /** - * Number of categories to return per page per query. + * Number of categories to return per article per query. * - * Since the record for a single page is 252 categories, setting this to the maximum of 500 is more than sufficient. + * Since the record for a single article is 252 categories, setting this to the maximum of 500 is more than + * sufficient. */ private const CATS_PER_QUERY = 500; @@ -76,7 +77,7 @@ class Mediawiki */ private function api_query(array $params, array $titles): QueryOutput { - $pages = []; + $articles = []; $redirects = array_combine($titles, $titles); $missing = []; @@ -103,11 +104,11 @@ class Mediawiki exit(); } - foreach ($response["pages"] as $page_id => $page) { - if ($page_id < 0) - $missing[] = strval($page["title"]); + foreach ($response["pages"] as $article_id => $article) { + if ($article_id < 0) + $missing[] = strval($article["title"]); else - $pages[strval($page["title"])] = $page; + $articles[strval($article["title"])] = $article; } $response_normalized = array_column($response["normalized"] ?? [], "to", "from"); @@ -121,38 +122,38 @@ class Mediawiki } } - return new QueryOutput($pages, $redirects, $missing); + return new QueryOutput($articles, $redirects, $missing); } /** - * Determines for each title whether the page at Wikipedia exists. + * Determines for each title whether the article at Wikipedia exists. * - * @param string[] $titles the titles of the pages to check - * @return QueryOutput a query output where the result is a flat array with the non-missing pages + * @param string[] $titles the titles of the articles to check + * @return QueryOutput a query output where the result is a flat array with the non-missing articles */ - public function pages_exist(array $titles): QueryOutput + public function articles_exist(array $titles): QueryOutput { $output = $this->api_query(["prop" => "info"], $titles); return new QueryOutput(array_fill_keys(array_keys($output->results), ""), $output->redirects, $output->missing); } /** - * Returns the person's status, or `null` if the title does not refer to a page about a person on Wikipedia. + * Returns the person's status, or `null` if the title does not refer to an article about a person on Wikipedia. * - * @param mixed $person_page the page as returned by the Wikipedia API - * @return PersonStatus|null the person's status, or `null` if the title does not refer to a page about a person on - * Wikipedia + * @param mixed $article the article object as returned by the Wikipedia API + * @return PersonStatus|null the person's status, or `null` if the title does not refer to an article about a person + * on Wikipedia */ - private function person_status(mixed $person_page): ?PersonStatus + private function person_status(mixed $article): ?PersonStatus { - if (array_key_exists("missing", $person_page) || array_key_exists("invalid", $person_page)) + if (array_key_exists("missing", $article) || array_key_exists("invalid", $article)) return null; - $category_titles = array_column($person_page["categories"], "title"); - $deceased_regex = "/^Category:([0-9]{1,4}s? (BC |AD )?deaths|Year of death (missing|unknown))$/"; + $category_titles = array_column($article["categories"], "title"); + $dead_regex = "/^Category:([0-9]{1,4}s? (BC |AD )?deaths|Year of death (missing|unknown))$/"; - if (!empty(array_filter($category_titles, fn($it) => preg_match($deceased_regex, $it)))) - return PersonStatus::Deceased; + if (!empty(array_filter($category_titles, fn($it) => preg_match($dead_regex, $it)))) + return PersonStatus::Dead; elseif (in_array("Category:Possibly living people", $category_titles)) return PersonStatus::PossiblyAlive; elseif (in_array("Category:Missing people", $category_titles)) @@ -166,20 +167,20 @@ class Mediawiki /** * Checks for each person what their status is according to Wikipedia's categorization. * - * @param array $people_names the names of the people to check aliveness of + * @param array $names the names of the people to check aliveness of * @return QueryOutput a query output with a response indicating for each person the status */ - public function people_statuses(array $people_names): QueryOutput + public function query_statuses(array $names): QueryOutput { - $output = $this->api_query(["prop" => "categories", "cllimit" => strval(self::CATS_PER_QUERY)], $people_names); + $output = $this->api_query(["prop" => "categories", "cllimit" => strval(self::CATS_PER_QUERY)], $names); - $pages = + $articles = array_combine( array_column($output->results, "title"), array_map(fn($it) => $this->person_status($it), $output->results) ); - return new QueryOutput($pages, $output->redirects, $output->missing); + return new QueryOutput($articles, $output->redirects, $output->missing); } } @@ -200,7 +201,7 @@ class QueryOutput */ public readonly array $redirects; /** - * @var string[] list of missing pages + * @var string[] list of missing articles */ public readonly array $missing; @@ -210,7 +211,7 @@ class QueryOutput * * @param array $results the results of the query, either raw from the API or processed in some way * @param array $redirects mapping of queried names to normalized/redirected names - * @param string[] $missing list of missing pages + * @param string[] $missing list of missing articles */ public function __construct(array $results, array $redirects, array $missing) { diff --git a/src/main/php/TrackingManager.php b/src/main/php/TrackingManager.php index 840eb32..a822019 100644 --- a/src/main/php/TrackingManager.php +++ b/src/main/php/TrackingManager.php @@ -12,11 +12,11 @@ use PDO; class TrackingManager { /** - * The minimum length of a Wikipedia page title. + * The minimum length of a Wikipedia article title. */ public const MIN_TITLE_LENGTH = 1; /** - * The maximum length of a Wikipedia page title. + * The maximum length of a Wikipedia article title. */ public const MAX_TITLE_LENGTH = 255; @@ -96,21 +96,21 @@ class TrackingManager public function add_tracking(string $user_uuid, string $person_name): Response { // TODO: Reject if person is already tracked - // TODO: Show link to Wikipedia page to search if page does not exist + // TODO: Show link to Wikipedia page to search if article does not exist - // Validate that page exists - $pages_exist = $this->mediawiki->pages_exist([$person_name]); - $normalized_name = $pages_exist->redirects[$person_name]; - if (in_array($normalized_name, $pages_exist->missing)) - return Response::unsatisfied("Page does not exist.", "person_name"); + // Validate that article exists + $article_exists = $this->mediawiki->articles_exist([$person_name]); + $normalized_name = $article_exists->redirects[$person_name]; + if (in_array($normalized_name, $article_exists->missing)) + return Response::unsatisfied("Article does not exist.", "person_name"); - $statuses = $this->mediawiki->people_statuses([$normalized_name])->results; + $statuses = $this->mediawiki->query_statuses([$normalized_name])->results; if (!isset($statuses[$normalized_name])) - return Response::unsatisfied("Page does not refer to a person.", "person_name"); + return Response::unsatisfied("Article does not refer to a person.", "person_name"); $status = $statuses[$normalized_name]; // Insert person and tracking - return Database::transaction($this->conn, function() use ($user_uuid, $normalized_name, $status) { + return Database::transaction($this->conn, function () use ($user_uuid, $normalized_name, $status) { $stmt = $this->conn->prepare("INSERT OR IGNORE INTO people (name) VALUES (:name);"); $stmt->bindValue(":name", $normalized_name); $stmt->execute(); @@ -166,6 +166,26 @@ class TrackingManager return Response::satisfied($results); } + /** + * Returns the email addresses of all users who should be notified of events relating to the given person. + * + * @param string $person_name the person to receive subscribed email addresses for + * @return string[] the email addresses of all users who should be notified of events relating to the given person + */ + private function get_recipients(string $person_name): array + { + $get_recipients = $this->conn->prepare("SELECT users.email + FROM users + LEFT JOIN trackings + WHERE trackings.person_name=:person_name + AND trackings.user_uuid=users.uuid + AND users.email_verification_token IS NULL + AND users.email_notifications_enabled=1;"); + $get_recipients->bindParam(":person_name", $person_name); + $get_recipients->execute(); + return array_column($get_recipients->fetchAll(PDO::FETCH_ASSOC), "email"); + } + /** * Lists all unique names being tracked in the database. @@ -182,14 +202,14 @@ class TrackingManager /** * Updates trackings for the given names. * - * @param string[] $people_names the names of the pages to update + * @param string[] $names the names of the trackings to update * @return void */ - public function update_trackings(array $people_names): void + public function update_trackings(array $names): void { - if (empty($people_names)) return; + if (empty($names)) return; - $people_statuses = $this->mediawiki->people_statuses($people_names); + $people_statuses = $this->mediawiki->query_statuses($names); Database::transaction($this->conn, function () use ($people_statuses) { $this->process_redirects($people_statuses->redirects); @@ -199,7 +219,7 @@ class TrackingManager } /** - * Renames pages. + * Renames people in the database. * * @param array $redirects a map of all changes, from old name to new name * @return void @@ -240,18 +260,31 @@ class TrackingManager } /** - * Deletes pages. + * Deletes people from the database. * - * @param string[] $deletions list of pages to delete from the database + * @param string[] $deletions list of names of people to remove from the database * @return void */ private function process_deletions(array $deletions): void { - $stmt = $this->conn->prepare("UPDATE people SET is_deleted=1 WHERE name=:name;"); - $stmt->bindParam(":name", $deleted_name); - foreach ($deletions as $deleted_name) - // TODO: Inform user that page has been deleted - $stmt->execute(); + // Query to delete person, returning `name` to determine whether something changed + $delete = $this->conn->prepare("UPDATE people + SET is_deleted=1 + WHERE name=:name AND is_deleted<>1 + RETURNING name;"); + $delete->bindParam(":name", $deleted_name); + + foreach ($deletions as $deleted_name) { + $delete->execute(); + $deleted = sizeof($delete->fetchAll(PDO::FETCH_ASSOC)) > 0; + if ($deleted) { + $this->logger->info("deleting", [$deleted_name]); + foreach ($this->get_recipients($deleted_name) as $user_email) { + $this->logger->info("sending to", [$user_email]); + $this->mailer->queue_email(new NotifyArticleDeletedEmail($user_email, $deleted_name)); + } + } + } } /** @@ -262,8 +295,12 @@ class TrackingManager */ private function process_statuses(array $statuses): void { - // Query to mark person as no longer deleted - $undelete = $this->conn->prepare("UPDATE people SET is_deleted=0 WHERE name=:name;"); + // TODO: Restrict number of notifications to 1 per hour (excluding "oops we're not sure" message) + // Query to mark person as no longer deleted, returning `name` to determine whether something changed + $undelete = $this->conn->prepare("UPDATE people + SET is_deleted=0 + WHERE name=:name AND is_deleted<>0 + RETURNING name;"); $undelete->bindParam(":name", $person_name); // Query to update status, returning `name` to determine whether something changed @@ -274,33 +311,21 @@ class TrackingManager $set_status->bindParam(":status", $person_status); $set_status->bindParam(":name", $person_name); - // Query to determine which users to notify - $get_recipients = $this->conn->prepare("SELECT users.email - FROM users - LEFT JOIN trackings - WHERE trackings.person_name=:person_name - AND trackings.user_uuid=users.uuid - AND users.email_verification_token IS NULL - AND users.email_notifications_enabled=1;"); - $get_recipients->bindParam(":person_name", $person_name); - foreach ($statuses as $person_name => $person_status_enum) { if ($person_status_enum === null) continue; $person_status = $person_status_enum->value; $undelete->execute(); + $undeleted = sizeof($undelete->fetchAll(PDO::FETCH_ASSOC)) > 0; + if ($undeleted) + foreach ($this->get_recipients($person_name) as $user_email) + $this->mailer->queue_email(new NotifyArticleUndeletedEmail($user_email, $person_name)); + $set_status->execute(); $status_changed = sizeof($set_status->fetchAll(PDO::FETCH_ASSOC)) > 0; - - if ($status_changed) { - $get_recipients->execute(); - $recipients = array_column($get_recipients->fetchAll(PDO::FETCH_ASSOC), "email"); - - // TODO: Write "alive again" notification - if ($person_status_enum === PersonStatus::Deceased) - foreach ($recipients as $user_email) - $this->mailer->queue_email(new NotifyDeathEmail($user_email, $person_name)); - } + if ($status_changed) + foreach ($this->get_recipients($person_name) as $user_email) + $this->mailer->queue_email(new NotifyStatusChangedEmail($user_email, $person_name, $person_status)); } } } @@ -314,5 +339,5 @@ enum PersonStatus: string case Alive = "alive"; case PossiblyAlive = "possibly alive"; case Missing = "missing"; - case Deceased = "deceased"; + case Dead = "dead"; } diff --git a/src/main/php/Validator.php b/src/main/php/Validator.php index 9a5a6d2..2d26f33 100644 --- a/src/main/php/Validator.php +++ b/src/main/php/Validator.php @@ -125,7 +125,7 @@ class IsEmailRule extends Rule public function check(array $inputs, string $key): ?Response { return !isset($inputs[$key]) || !filter_var($inputs[$key], FILTER_VALIDATE_EMAIL) - ? Response::unsatisfied($this->override_message ?? "Please enter a valid email address.", $key) + ? Response::unsatisfied($this->override_message ?? "Enter a valid email address.", $key) : null; } }