Add "alive again" notifications and similar
This commit is contained in:
parent
6669b62d66
commit
2fe92e8d2e
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "fwdekker/death-notifier",
|
"name": "fwdekker/death-notifier",
|
||||||
"description": "Get notified when a famous person dies.",
|
"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",
|
"type": "project",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://git.fwdekker.com/tools/death-notifier",
|
"homepage": "https://git.fwdekker.com/tools/death-notifier",
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "death-notifier",
|
"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.",
|
"description": "Get notified when a famous person dies.",
|
||||||
"author": "Florine W. Dekker",
|
"author": "Florine W. Dekker",
|
||||||
"browser": "dist/bundle.js",
|
"browser": "dist/bundle.js",
|
||||||
|
|
|
@ -37,7 +37,6 @@
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<p id="globalMessage" class="formValidationInfo">
|
<p id="globalMessage" class="formValidationInfo">
|
||||||
<output class="validationInfo" for="globalMessage"></output>
|
<output class="validationInfo" for="globalMessage"></output>
|
||||||
<button type="button" class="closeButton">×</button>
|
|
||||||
</p>
|
</p>
|
||||||
<p id="sharedValidationInfo" class="formValidationInfo">
|
<p id="sharedValidationInfo" class="formValidationInfo">
|
||||||
<output class="validationInfo" for="sharedValidationInfo"></output>
|
<output class="validationInfo" for="sharedValidationInfo"></output>
|
||||||
|
|
|
@ -45,14 +45,14 @@ function refreshTrackings(): void {
|
||||||
const statusCell = document.createElement("td");
|
const statusCell = document.createElement("td");
|
||||||
let statusText;
|
let statusText;
|
||||||
if (tracking.deleted) {
|
if (tracking.deleted) {
|
||||||
statusText = "page not found";
|
statusText = "article not found";
|
||||||
} else {
|
} else {
|
||||||
switch (tracking.name) {
|
switch (tracking.name) {
|
||||||
case "Adolf Hitler":
|
case "Adolf Hitler":
|
||||||
statusText = "deceased 🥳";
|
statusText = "dead 🥳";
|
||||||
break;
|
break;
|
||||||
case "Vladimir Putin":
|
case "Vladimir Putin":
|
||||||
statusText = tracking.status === "alive" ? "alive ☹️" : "deceased 🥳";
|
statusText = tracking.status === "alive" ? "alive ☹️" : "dead 🥳";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
statusText = tracking.status;
|
statusText = tracking.status;
|
||||||
|
|
|
@ -102,6 +102,7 @@ class Database
|
||||||
// Execute migration code
|
// Execute migration code
|
||||||
if (Comparator::lessThan($db_version, "0.5.0")) self::migrate_0_5_0();
|
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.8.0")) self::migrate_0_8_0();
|
||||||
|
if (Comparator::lessThan($db_version, "0.10.0")) self::migrate_0_10_0();
|
||||||
|
|
||||||
// Update version
|
// Update version
|
||||||
$stmt = $this->conn->prepare("UPDATE meta SET v=:version WHERE k='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
|
* @return void
|
||||||
* @noinspection SqlResolve Function necessarily refers to old scheme which is not detected by tools
|
* @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.
|
* 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,
|
$this->conn->exec("CREATE TABLE email_tasks(type TEXT NOT NULL,
|
||||||
recipient TEXT NOT NULL,
|
recipient TEXT NOT NULL,
|
||||||
arg1 TEXT DEFAULT(NULL),
|
arg1 TEXT NOT NULL DEFAULT(''),
|
||||||
PRIMARY KEY (type, recipient, arg1));");
|
arg2 TEXT NOT NULL DEFAULT(''),
|
||||||
|
PRIMARY KEY (type, recipient, arg1, arg2));");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,11 +66,12 @@ class Mailer
|
||||||
*/
|
*/
|
||||||
public function queue_email(Email $email): Response
|
public function queue_email(Email $email): Response
|
||||||
{
|
{
|
||||||
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, recipient, arg1)
|
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO email_tasks (type, recipient, arg1, arg2)
|
||||||
VALUES (:type, :recipient, :arg1);");
|
VALUES (:type, :recipient, :arg1, :arg2);");
|
||||||
$stmt->bindValue(":type", $email->type);
|
$stmt->bindValue(":type", $email->type);
|
||||||
$stmt->bindValue(":recipient", $email->recipient);
|
$stmt->bindValue(":recipient", $email->recipient);
|
||||||
$stmt->bindValue(":arg1", $email->arg1);
|
$stmt->bindValue(":arg1", $email->arg1);
|
||||||
|
$stmt->bindValue(":arg2", $email->arg2);
|
||||||
|
|
||||||
return $stmt->execute() ? Response::satisfied() : Response::unsatisfied(null);
|
return $stmt->execute() ? Response::satisfied() : Response::unsatisfied(null);
|
||||||
}
|
}
|
||||||
|
@ -102,19 +104,20 @@ class Mailer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get queue
|
// 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();
|
$stmt->execute();
|
||||||
$email_tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$email_tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// Process queue
|
// Process queue
|
||||||
$stmt = $this->conn->prepare("DELETE FROM email_tasks
|
$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(":type", $type);
|
||||||
$stmt->bindParam(":recipient", $recipient);
|
$stmt->bindParam(":recipient", $recipient);
|
||||||
$stmt->bindParam(":arg1", $arg1);
|
$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 {
|
try {
|
||||||
$email = Email::deserialize($type, $recipient, $arg1 ?? "");
|
$email = Email::deserialize($type, $recipient, $arg1, $arg2);
|
||||||
$mailer->Subject = $email->getSubject();
|
$mailer->Subject = $email->getSubject();
|
||||||
$mailer->Body = $email->getBody($this->config);
|
$mailer->Body = $email->getBody($this->config);
|
||||||
|
|
||||||
|
@ -161,9 +164,13 @@ abstract class Email
|
||||||
*/
|
*/
|
||||||
public string $recipient;
|
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 $type the type of email to deserialize
|
||||||
* @param string $recipient the intended recipient of the email
|
* @param string $recipient the intended recipient of the email
|
||||||
* @param string $arg1 the first argument to construct 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
|
* @return Email a deserialized email
|
||||||
* @throws \Exception if the `type` is not recognized
|
* @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) {
|
return match ($type) {
|
||||||
RegisterEmail::TYPE => new RegisterEmail($recipient, $arg1),
|
RegisterEmail::TYPE => new RegisterEmail($recipient, $arg1),
|
||||||
|
@ -201,7 +209,9 @@ abstract class Email
|
||||||
ChangedEmailEmail::TYPE => new ChangedEmailEmail($recipient, $arg1),
|
ChangedEmailEmail::TYPE => new ChangedEmailEmail($recipient, $arg1),
|
||||||
ChangedPasswordEmail::TYPE => new ChangedPasswordEmail($recipient),
|
ChangedPasswordEmail::TYPE => new ChangedPasswordEmail($recipient),
|
||||||
ResetPasswordEmail::TYPE => new ResetPasswordEmail($recipient, $arg1),
|
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."),
|
default => throw new \Exception("Unknown email type $type."),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -392,8 +402,6 @@ class ChangedPasswordEmail extends Email {
|
||||||
{
|
{
|
||||||
$this->type = self::TYPE;
|
$this->type = self::TYPE;
|
||||||
$this->recipient = $recipient;
|
$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.
|
* 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;
|
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 $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)
|
public function __construct(string $recipient, string $name)
|
||||||
{
|
{
|
||||||
|
@ -506,7 +513,7 @@ class NotifyDeathEmail extends Email
|
||||||
|
|
||||||
public function getSubject(): string
|
public function getSubject(): string
|
||||||
{
|
{
|
||||||
return "$this->name may have died";
|
return "$this->name article has been deleted";
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBody(array $config): string
|
public function getBody(array $config): string
|
||||||
|
@ -514,11 +521,126 @@ class NotifyDeathEmail extends Email
|
||||||
$base_path = $config["server"]["base_path"];
|
$base_path = $config["server"]["base_path"];
|
||||||
|
|
||||||
return
|
return
|
||||||
"Someone has edited the Wikipedia page of $this->name to state that they have died. " .
|
"The Wikipedia article about $this->name has been deleted. " .
|
||||||
"For more information, read their Wikipedia page at " .
|
"Death Notifier is now unable to send you a notification if $this->name dies. " .
|
||||||
"https://en.wikipedia.org/wiki/" . rawurlencode($this->name) .
|
"If the Wikipedia article is ever re-created, Death Notifier will automatically resume tracking this " .
|
||||||
|
"article, and you will receive another notification." .
|
||||||
"\n\n" .
|
"\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 " .
|
"To unsubscribe from these messages, go to the Death Notifier website, log in, and change your email " .
|
||||||
"preferences." .
|
"preferences." .
|
||||||
"\n\n" .
|
"\n\n" .
|
||||||
|
|
|
@ -22,9 +22,10 @@ class Mediawiki
|
||||||
"death-notifier/%%VERSION_NUMBER%% " .
|
"death-notifier/%%VERSION_NUMBER%% " .
|
||||||
"(https://git.fwdekker.com/tools/death-notifier; florine@fwdekker.com)";
|
"(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;
|
private const CATS_PER_QUERY = 500;
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ class Mediawiki
|
||||||
*/
|
*/
|
||||||
private function api_query(array $params, array $titles): QueryOutput
|
private function api_query(array $params, array $titles): QueryOutput
|
||||||
{
|
{
|
||||||
$pages = [];
|
$articles = [];
|
||||||
$redirects = array_combine($titles, $titles);
|
$redirects = array_combine($titles, $titles);
|
||||||
$missing = [];
|
$missing = [];
|
||||||
|
|
||||||
|
@ -103,11 +104,11 @@ class Mediawiki
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($response["pages"] as $page_id => $page) {
|
foreach ($response["pages"] as $article_id => $article) {
|
||||||
if ($page_id < 0)
|
if ($article_id < 0)
|
||||||
$missing[] = strval($page["title"]);
|
$missing[] = strval($article["title"]);
|
||||||
else
|
else
|
||||||
$pages[strval($page["title"])] = $page;
|
$articles[strval($article["title"])] = $article;
|
||||||
}
|
}
|
||||||
|
|
||||||
$response_normalized = array_column($response["normalized"] ?? [], "to", "from");
|
$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
|
* @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 pages
|
* @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);
|
$output = $this->api_query(["prop" => "info"], $titles);
|
||||||
return new QueryOutput(array_fill_keys(array_keys($output->results), ""), $output->redirects, $output->missing);
|
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
|
* @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 a page about a person on
|
* @return PersonStatus|null the person's status, or `null` if the title does not refer to an article about a person
|
||||||
* Wikipedia
|
* 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;
|
return null;
|
||||||
|
|
||||||
$category_titles = array_column($person_page["categories"], "title");
|
$category_titles = array_column($article["categories"], "title");
|
||||||
$deceased_regex = "/^Category:([0-9]{1,4}s? (BC |AD )?deaths|Year of death (missing|unknown))$/";
|
$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))))
|
if (!empty(array_filter($category_titles, fn($it) => preg_match($dead_regex, $it))))
|
||||||
return PersonStatus::Deceased;
|
return PersonStatus::Dead;
|
||||||
elseif (in_array("Category:Possibly living people", $category_titles))
|
elseif (in_array("Category:Possibly living people", $category_titles))
|
||||||
return PersonStatus::PossiblyAlive;
|
return PersonStatus::PossiblyAlive;
|
||||||
elseif (in_array("Category:Missing people", $category_titles))
|
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.
|
* 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
|
* @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_combine(
|
||||||
array_column($output->results, "title"),
|
array_column($output->results, "title"),
|
||||||
array_map(fn($it) => $this->person_status($it), $output->results)
|
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;
|
public readonly array $redirects;
|
||||||
/**
|
/**
|
||||||
* @var string[] list of missing pages
|
* @var string[] list of missing articles
|
||||||
*/
|
*/
|
||||||
public readonly array $missing;
|
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, 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 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)
|
public function __construct(array $results, array $redirects, array $missing)
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,11 +12,11 @@ use PDO;
|
||||||
class TrackingManager
|
class TrackingManager
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The minimum length of a Wikipedia page title.
|
* The minimum length of a Wikipedia article title.
|
||||||
*/
|
*/
|
||||||
public const MIN_TITLE_LENGTH = 1;
|
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;
|
public const MAX_TITLE_LENGTH = 255;
|
||||||
|
|
||||||
|
@ -96,21 +96,21 @@ class TrackingManager
|
||||||
public function add_tracking(string $user_uuid, string $person_name): Response
|
public function add_tracking(string $user_uuid, string $person_name): Response
|
||||||
{
|
{
|
||||||
// TODO: Reject if person is already tracked
|
// 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
|
// Validate that article exists
|
||||||
$pages_exist = $this->mediawiki->pages_exist([$person_name]);
|
$article_exists = $this->mediawiki->articles_exist([$person_name]);
|
||||||
$normalized_name = $pages_exist->redirects[$person_name];
|
$normalized_name = $article_exists->redirects[$person_name];
|
||||||
if (in_array($normalized_name, $pages_exist->missing))
|
if (in_array($normalized_name, $article_exists->missing))
|
||||||
return Response::unsatisfied("Page does not exist.", "person_name");
|
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]))
|
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];
|
$status = $statuses[$normalized_name];
|
||||||
|
|
||||||
// Insert person and tracking
|
// 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 = $this->conn->prepare("INSERT OR IGNORE INTO people (name) VALUES (:name);");
|
||||||
$stmt->bindValue(":name", $normalized_name);
|
$stmt->bindValue(":name", $normalized_name);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
@ -166,6 +166,26 @@ class TrackingManager
|
||||||
return Response::satisfied($results);
|
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.
|
* Lists all unique names being tracked in the database.
|
||||||
|
@ -182,14 +202,14 @@ class TrackingManager
|
||||||
/**
|
/**
|
||||||
* Updates trackings for the given names.
|
* 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
|
* @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) {
|
Database::transaction($this->conn, function () use ($people_statuses) {
|
||||||
$this->process_redirects($people_statuses->redirects);
|
$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
|
* @param array<string, string> $redirects a map of all changes, from old name to new name
|
||||||
* @return void
|
* @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
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function process_deletions(array $deletions): void
|
private function process_deletions(array $deletions): void
|
||||||
{
|
{
|
||||||
$stmt = $this->conn->prepare("UPDATE people SET is_deleted=1 WHERE name=:name;");
|
// Query to delete person, returning `name` to determine whether something changed
|
||||||
$stmt->bindParam(":name", $deleted_name);
|
$delete = $this->conn->prepare("UPDATE people
|
||||||
foreach ($deletions as $deleted_name)
|
SET is_deleted=1
|
||||||
// TODO: Inform user that page has been deleted
|
WHERE name=:name AND is_deleted<>1
|
||||||
$stmt->execute();
|
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
|
private function process_statuses(array $statuses): void
|
||||||
{
|
{
|
||||||
// Query to mark person as no longer deleted
|
// TODO: Restrict number of notifications to 1 per hour (excluding "oops we're not sure" message)
|
||||||
$undelete = $this->conn->prepare("UPDATE people SET is_deleted=0 WHERE name=:name;");
|
// 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);
|
$undelete->bindParam(":name", $person_name);
|
||||||
|
|
||||||
// Query to update status, returning `name` to determine whether something changed
|
// 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(":status", $person_status);
|
||||||
$set_status->bindParam(":name", $person_name);
|
$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) {
|
foreach ($statuses as $person_name => $person_status_enum) {
|
||||||
if ($person_status_enum === null) continue;
|
if ($person_status_enum === null) continue;
|
||||||
$person_status = $person_status_enum->value;
|
$person_status = $person_status_enum->value;
|
||||||
|
|
||||||
$undelete->execute();
|
$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();
|
$set_status->execute();
|
||||||
$status_changed = sizeof($set_status->fetchAll(PDO::FETCH_ASSOC)) > 0;
|
$status_changed = sizeof($set_status->fetchAll(PDO::FETCH_ASSOC)) > 0;
|
||||||
|
if ($status_changed)
|
||||||
if ($status_changed) {
|
foreach ($this->get_recipients($person_name) as $user_email)
|
||||||
$get_recipients->execute();
|
$this->mailer->queue_email(new NotifyStatusChangedEmail($user_email, $person_name, $person_status));
|
||||||
$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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -314,5 +339,5 @@ enum PersonStatus: string
|
||||||
case Alive = "alive";
|
case Alive = "alive";
|
||||||
case PossiblyAlive = "possibly alive";
|
case PossiblyAlive = "possibly alive";
|
||||||
case Missing = "missing";
|
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
|
public function check(array $inputs, string $key): ?Response
|
||||||
{
|
{
|
||||||
return !isset($inputs[$key]) || !filter_var($inputs[$key], FILTER_VALIDATE_EMAIL)
|
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;
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue