Add "alive again" notifications and similar
This commit is contained in:
parent
6669b62d66
commit
2fe92e8d2e
|
@ -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",
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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",
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
<div class="column">
|
||||
<p id="globalMessage" class="formValidationInfo">
|
||||
<output class="validationInfo" for="globalMessage"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<p id="sharedValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="sharedValidationInfo"></output>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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" .
|
||||
|
|
|
@ -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<string> 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<string> 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<string> $people_names the names of the people to check aliveness of
|
||||
* @param array<string> $names the names of the people to check aliveness of
|
||||
* @return QueryOutput<PersonStatus|null> 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<string, T> $results the results of the query, either raw from the API or processed in some way
|
||||
* @param array<string, string> $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)
|
||||
{
|
||||
|
|
|
@ -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<string, string> $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";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue