Detect missing people, add CLI for updating

This commit is contained in:
Florine W. Dekker 2022-08-16 23:29:12 +02:00
parent ca2937047d
commit e33195d629
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
9 changed files with 71 additions and 50 deletions

View File

@ -1,7 +1,7 @@
{
"name": "fwdekker/death-notifier",
"description": "Get notified when a famous person dies.",
"version": "0.0.9",
"version": "0.0.10",
"type": "project",
"license": "MIT",
"homepage": "https://git.fwdekker.com/tools/death-notifier",

BIN
composer.lock generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "death-notifier",
"version": "0.0.9",
"version": "0.0.10",
"description": "Get notified when a famous person dies.",
"author": "Florine W. Dekker",
"browser": "dist/bundle.js",

View File

@ -128,7 +128,7 @@ function validate_has_arguments(mixed ...$arguments): ?Response
$response = null;
if (isset($_POST["action"])) {
// Process POST
// POST requests; alter state
switch ($_POST["action"]) {
case "register":
$response = validate_csrf()
@ -195,17 +195,33 @@ if (isset($_POST["action"])) {
$response = new Response("Unknown POST action '" . $_POST["action"] . "'.", false);
break;
}
} else if (isset($_GET["action"])) {
} elseif (isset($_GET["action"])) {
// GET requests; do not alter state
$response = match ($_GET["action"]) {
"get-user-data" => validate_logged_in() ?? $user_manager->get_user_data($_SESSION["uuid"]),
"list-trackings" => validate_logged_in() ?? $tracking_manager->list_trackings($_SESSION["uuid"]),
default => new Response("Unknown GET action '" . $_GET["action"] . "'.", false),
};
} elseif ($argc > 1) {
// CLI
if (hash_equals($config["admin"]["update_secret"], "REPLACE THIS WITH A SECRET VALUE"))
exit("Default value for 'cli_secret' detected. Feature disabled.");
if (hash_equals($config["admin"]["update_secret"], $argv[2]))
exit("Incorrect value for 'cli_secret'.");
if ($argv[1] === "update-all-trackings") {
$logger->info("Updating all trackings.");
$tracking_manager->update_trackings($tracking_manager->list_all_trackings());
exit("Successfully updated all trackings.");
} else {
exit("Unknown CLI action '" . $argv[1] . "'.");
}
} else {
$response = new Response("Unknown method.", false);
// No action given, nothing done, so that's a success
$response = new Response(null, true);
}
//header("Content-type:application/json;charset=utf-8");
header("Content-type:application/json;charset=utf-8");
exit(json_encode(array(
"message" => $response->message,
"satisfied" => $response->satisfied,

View File

@ -1,9 +1,19 @@
;<?php exit(); ?>
[admin]
# Password to use the CLI of `api.php`. Until this value is changed from its default, the feature is disabled
cli_secret = REPLACE THIS WITH A SECRET VALUE
[database]
# Relative path to SQLite database
filename = .death-notifier.db
[logger]
# File to store logs in
file = .death-notifier.log
# Log level. See https://seldaek.github.io/monolog/doc/01-usage.html#log-levels
level = 300
[mail]
# Whether to enable mailing
enabled = false
@ -19,9 +29,3 @@ password = TODO
from_name = TODO
# Email address to send test emails to
to_address_test = TODO
[logger]
# File to store logs in
file = death-notifier.log
# Log level. See https://seldaek.github.io/monolog/doc/01-usage.html#log-levels
level = 300

View File

@ -26,7 +26,7 @@ function refreshTrackings() {
const trackingPersonName = document.createElement("td");
trackingPersonName.innerText = tracking["person_name"];
const trackingIsDeceased = document.createElement("td");
trackingIsDeceased.innerText = tracking["is_deceased"] === 1 ? "yes" : "no";
trackingIsDeceased.innerText = tracking["status"];
const trackingDelete = document.createElement("td");
const trackingDeleteButton = document.createElement("button");
trackingDeleteButton.innerText = "remove";

View File

@ -18,9 +18,6 @@ class Database
*/
public static function connect(string $filename): PDO
{
return new PDO("sqlite:" . $filename, options: array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => true
));
return new PDO("sqlite:" . $filename, options: array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
}
}

View File

@ -21,10 +21,6 @@ class Mediawiki
private const USER_AGENT =
"death-notifier/%%VERSION_NUMBER%% " .
"(https://git.fwdekker.com/tools/death-notifier; florine@fwdekker.com)";
/**
* Regex matching Wikipedia categories indicating deceased people.
*/
private const DECEASED_CATEGORY_REGEX = "/^Category:[0-9]{1,4} (BC |AD )?deaths$/";
/**
* @var Logger The logger to use for logging.
@ -82,6 +78,7 @@ class Mediawiki
"action" => "query",
"format" => "json",
"prop" => "info",
"redirects" => true,
"titles" => implode("|", array_slice($titles, $i, 50))
))["query"];
@ -94,6 +91,10 @@ class Mediawiki
array_map($page_exists, $response_pages)
)
);
if (isset($response["normalized"]))
foreach ($response["normalized"] as $redirect)
$pages[$redirect["from"]] = $pages[$redirect["to"]];
}
return $pages;
@ -109,38 +110,41 @@ class Mediawiki
}
/**
* Returns `true` if the person is deceased, `false` if the person is alive, and `null` if the person does not have
* a page on Wikipedia.
* Returns a string describing `person`'s status ("deceased", "alive", "possibly alive", "missing"), or `null` if
* the title does not refer to a page about a person on Wikipedia.
*
* @param mixed $person_page the page as returned by the Wikipedia API
* @return bool|null `true` if the person is deceased, `false` if the person is alive, and `null` if the person does
* not have a page on Wikipedia
* @return string|null a string describing `person`'s status ("deceased", "alive", "possibly alive", "missing"), or
* `null` if the title does not refer to a page about a person on Wikipedia
*/
private function person_is_deceased(mixed $person_page): ?bool
private function person_status(mixed $person_page): ?string
{
if (array_key_exists("missing", $person_page) || array_key_exists("invalid", $person_page))
return null;
// TODO: Detect missing people, detect presumed dead people
$is_alive = in_array("Category:Living people", array_column($person_page["categories"], "title"));
$is_deceased = !empty(array_filter(
$person_page["categories"],
fn($it) => preg_match(self::DECEASED_CATEGORY_REGEX, $it["title"])
));
if (!$is_alive && !$is_deceased)
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))$/";
return $is_deceased;
if (!empty(array_filter($category_titles, fn($it) => preg_match($deceased_regex, $it))))
return "deceased";
elseif (in_array("Category:Possibly living people", $category_titles))
return "possibly alive";
elseif (in_array("Category:Missing people", $category_titles))
return "missing";
elseif (in_array("Category:Living people", $category_titles))
return "alive";
else
return null;
}
/**
* Checks for each person whether they are alive according to Wikipedia's categorization.
* Checks for each person what their status (dead, alive, missing) is according to Wikipedia's categorization.
*
* @param array<string> $people_names the names of the people to check aliveness of
* @return array<string, ?bool> maps each requested person's name to a boolean indicating whether they are deceased.
* If a page does not exist, it is mapped to a `null`
* @return array<string, ?string> maps each requested person's name to a string describing their status ("deceased",
* "alive", "possibly alive", "missing"). If a page does not exist, it is mapped to `null`
*/
public function people_are_deceased(array $people_names): array
public function people_statuses(array $people_names): array
{
try {
$pages = [];
@ -161,7 +165,7 @@ class Mediawiki
$pages,
array_combine(
array_column($response_pages, "title"),
array_map(fn($it) => $this->person_is_deceased($it), $response_pages)
array_map(fn($it) => $this->person_status($it), $response_pages)
)
);

View File

@ -52,7 +52,7 @@ class TrackingManager
public function install(): void
{
$conn = Database::connect($this->db_filename);
$conn->exec("CREATE TABLE trackings(user_uuid text not null, person_name text not null, is_deceased int not null default 0, PRIMARY KEY (user_uuid, person_name));");
$conn->exec("CREATE TABLE trackings(user_uuid text not null, person_name text not null, status text not null, PRIMARY KEY (user_uuid, person_name));");
}
/**
@ -69,8 +69,8 @@ class TrackingManager
if (!$this->mediawiki->pages_exist([$person_name])[$person_name])
return new Response("Page does not exist.", false);
$is_deceased = $this->mediawiki->people_are_deceased([$person_name])[$person_name];
if ($is_deceased === null)
$status = $this->mediawiki->people_statuses([$person_name])[$person_name];
if ($status === null)
return new Response("Page does not refer to a person.", false);
$conn = Database::connect($this->db_filename);
@ -85,10 +85,10 @@ class TrackingManager
return new Response("Tracking already exists.", false);
}
$stmt = $conn->prepare("INSERT INTO trackings (user_uuid, person_name, is_deceased) VALUES (:user_uuid, :person_name, :is_deceased);");
$stmt = $conn->prepare("INSERT INTO trackings (user_uuid, person_name, status) VALUES (:user_uuid, :person_name, :status);");
$stmt->bindValue(":user_uuid", $user_uuid);
$stmt->bindValue(":person_name", $person_name);
$stmt->bindValue(":is_deceased", $is_deceased);
$stmt->bindValue(":status", $status);
$stmt->execute();
$conn->commit();
@ -125,7 +125,7 @@ class TrackingManager
public function list_trackings(string $user_uuid): Response
{
$conn = Database::connect($this->db_filename);
$stmt = $conn->prepare("SELECT person_name, is_deceased FROM trackings WHERE user_uuid=:user_uuid;");
$stmt = $conn->prepare("SELECT person_name, status FROM trackings WHERE user_uuid=:user_uuid;");
$stmt->bindValue(":user_uuid", $user_uuid);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
@ -155,13 +155,13 @@ class TrackingManager
public function update_trackings(array $people_names): void
{
// TODO: Handle removed pages
$people_statuses = $this->mediawiki->people_are_deceased($people_names);
$people_statuses = $this->mediawiki->people_statuses($people_names);
$conn = Database::connect($this->db_filename);
$stmt = $conn->prepare("UPDATE trackings SET is_deceased=:is_deceased WHERE person_name=:person_name;");
$stmt->bindParam(":is_deceased", $person_is_deceased);
$stmt = $conn->prepare("UPDATE trackings SET status=:status WHERE person_name=:person_name;");
$stmt->bindParam(":status", $person_status);
$stmt->bindParam(":person_name", $person_name);
foreach ($people_statuses as $person_name => $person_is_deceased)
foreach ($people_statuses as $person_name => $person_status)
$stmt->execute();
}
}