death-notifier/src/main/php/TrackingManager.php

256 lines
9.5 KiB
PHP

<?php
namespace php;
use PDO;
/**
* Manages interaction with the database in the context of trackings.
*/
class TrackingManager
{
/**
* The minimum length of a Wikipedia page title.
*/
public const MIN_TITLE_LENGTH = 1;
/**
* The maximum length of a Wikipedia page title.
*/
public const MAX_TITLE_LENGTH = 255;
/**
* @var PDO The database connection to interact with.
*/
private PDO $conn;
/**
* @var Mailer The mailer to send emails with.
*/
private Mailer $mailer;
/**
* @var Mediawiki The Mediawiki instance to use for interacting with Wikipedia.
*/
private Mediawiki $mediawiki;
/**
* Constructs a new tracking manager.
*
* @param PDO $conn the database connection to interact with
* @param Mailer $mailer the mailer to send emails with
* @param Mediawiki $mediawiki the Mediawiki instance to use for interacting with Wikipedia
*/
public function __construct(PDO $conn, Mailer $mailer, Mediawiki $mediawiki)
{
$this->conn = $conn;
$this->mailer = $mailer;
$this->mediawiki = $mediawiki;
}
/**
* Populates the database with the necessary structures for users.
*
* @return void
*/
public function install(): void
{
$this->conn->exec("CREATE TABLE trackings(user_uuid TEXT NOT NULL,
person_name TEXT NOT NULL,
PRIMARY KEY (user_uuid, person_name),
FOREIGN KEY (user_uuid) REFERENCES users (uuid)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY (person_name) REFERENCES people (name)
ON DELETE CASCADE
ON UPDATE CASCADE);");
$this->conn->exec("CREATE TABLE people(name TEXT NOT NULL UNIQUE PRIMARY KEY,
status TEXT NOT NULL DEFAULT '',
is_deleted INT NOT NULL DEFAULT 0);");
$this->conn->exec("CREATE TRIGGER people_cull_orphans
AFTER DELETE ON trackings
FOR EACH ROW
WHEN (SELECT COUNT(*) FROM trackings WHERE person_name=OLD.person_name)=0
BEGIN
DELETE FROM people WHERE name=OLD.person_name;
END;");
}
/**
* Adds a tracking to the database.
*
* @param string $user_uuid the user to whom the tracking belongs
* @param string $person_name the name of the person to track
* @return Response a satisfied `Response` with payload `null` if the tracking was added, or an unsatisfied
* `Response` otherwise
*/
public function add_tracking(string $user_uuid, string $person_name): Response
{
$pages_exist = $this->mediawiki->pages_exist([$person_name]);
$normalized_name = $pages_exist->redirects[$person_name];
if (in_array($normalized_name, $pages_exist->missing))
return new Response(
payload: ["target" => "personName", "message" => "Page does not exist."],
satisfied: false
);
$statuses = $this->mediawiki->people_statuses([$normalized_name])->results;
if (!isset($statuses[$normalized_name]))
return new Response(
payload: ["target" => "personName", "message" => "Page does not refer to a person."],
satisfied: false
);
$status = $statuses[$normalized_name];
// Insert person and tracking
$this->conn->beginTransaction();
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO people (name) VALUES (:name);");
$stmt->bindValue(":name", $normalized_name);
$stmt->execute();
$stmt = $this->conn->prepare("UPDATE people SET status=:status WHERE name=:name;");
$stmt->bindValue(":name", $normalized_name);
$stmt->bindValue(":status", $status->value);
$stmt->execute();
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO trackings (user_uuid, person_name)
VALUES (:user_uuid, :person_name);");
$stmt->bindValue(":user_uuid", $user_uuid);
$stmt->bindValue(":person_name", $normalized_name);
$stmt->execute();
$this->conn->commit();
return new Response(payload: null, satisfied: true);
}
/**
* Removes a tracking from the database.
*
* @param string $user_uuid the user to whom the tracking belongs
* @param string $person_name the name of the tracked person to remove
* @return Response a satisfied `Response` with payload `null`
*/
public function remove_tracking(string $user_uuid, string $person_name): Response
{
$stmt = $this->conn->prepare("DELETE FROM trackings
WHERE user_uuid=:user_uuid AND person_name=:person_name;");
$stmt->bindValue(":user_uuid", $user_uuid);
$stmt->bindValue(":person_name", $person_name);
$stmt->execute();
return new Response(payload: null, satisfied: true);
}
/**
* Lists all trackings of the indicated user.
*
* @param string $user_uuid the user to return the trackings of
* @return Response a satisfied `Response` with the trackings as its payload
*/
public function list_trackings(string $user_uuid): Response
{
$stmt = $this->conn->prepare("SELECT people.name, people.status, people.is_deleted
FROM trackings
INNER JOIN people
ON trackings.user_uuid=:user_uuid
AND trackings.person_name=people.name;");
$stmt->bindValue(":user_uuid", $user_uuid);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
return new Response(payload: $results, satisfied: true);
}
/**
* Lists all unique names being tracked in the database.
*
* @return array<string> all unique names in the database
*/
public function list_all_unique_person_names(): array
{
$stmt = $this->conn->prepare("SELECT ALL name FROM people;");
$stmt->execute();
return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), "name");
}
/**
* Updates trackings for the given names.
*
* @param string[] $people_names the names of the pages to update the tracking of
* @return void
*/
public function update_trackings(array $people_names): void
{
if (empty($people_names)) return;
// Fetch statuses from Mediawiki
$people_statuses = $this->mediawiki->people_statuses($people_names);
// Begin transaction
$this->conn->beginTransaction();
// Process redirects
$stmt = $this->conn->prepare("UPDATE people SET name=:new_name WHERE name=:old_name;");
$stmt->bindParam(":old_name", $from);
$stmt->bindParam(":new_name", $to);
foreach ($people_statuses->redirects as $from => $to)
if ($from !== $to)
$stmt->execute();
// Process deletions
$stmt = $this->conn->prepare("UPDATE people SET is_deleted=1 WHERE name=:name;");
$stmt->bindParam(":name", $missing_title);
foreach ($people_statuses->missing as $missing_title)
// TODO: Inform user that page has been deleted
$stmt->execute();
// Process status changes
$stmt_undelete = $this->conn->prepare("UPDATE people SET is_deleted=0 WHERE name=:name;");
$stmt_undelete->bindParam(":name", $person_name);
$stmt_status = $this->conn->prepare("UPDATE people
SET status=:status
WHERE name=:name AND status<>:status
RETURNING name;");
$stmt_status->bindParam(":status", $person_status);
$stmt_status->bindParam(":name", $person_name);
$stmt_emails = $this->conn->prepare("SELECT users.email
FROM users
LEFT JOIN trackings
WHERE trackings.person_name=:person_name
AND trackings.user_uuid=users.uuid;");
$stmt_emails->bindParam(":person_name", $person_name);
foreach ($people_statuses->results as $person_name => $person_status) {
$stmt_undelete->execute();
$stmt_status->execute();
// TODO: Write "alive again" notification
if (sizeof($stmt_status->fetchAll(PDO::FETCH_ASSOC)) > 0 && $person_status === PersonStatus::Alive) {
$stmt_emails->execute();
foreach (array_column($stmt_emails->fetchAll(PDO::FETCH_ASSOC), "email") as $user_email)
$this->mailer->queue_death_notification($user_email, $person_name);
}
}
// Commit transaction
$this->conn->commit();
}
}
/**
* The status assigned to a person.
*/
enum PersonStatus: string
{
case Alive = "alive";
case PossiblyAlive = "possibly alive";
case Missing = "missing";
case Deceased = "deceased";
}