death-notifier/src/main/php/com/fwdekker/deathnotifier/tracking/UpdateTrackingsAction.php

313 lines
10 KiB
PHP

<?php
namespace com\fwdekker\deathnotifier\tracking;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\LoggerUtil;
use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\UnexpectedException;
use com\fwdekker\deathnotifier\validation\EqualsCliPasswordRule;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\RuleSet;
use com\fwdekker\deathnotifier\wikipedia\PersonStatus;
use com\fwdekker\deathnotifier\wikipedia\Wikipedia;
use com\fwdekker\deathnotifier\wikipedia\WikipediaException;
use Monolog\Logger;
/**
* Updates all trackings that users have added.
*
* @see NotifyStatusChangedEmail
* @see NotifyArticleDeletedEmail
* @see NotifyArticleUndeletedEmail
*/
class UpdateTrackingsAction extends Action
{
/**
* @var Logger the logger to log with
*/
private readonly Logger $logger;
/**
* @var Logger the database logger to log with
*/
private readonly Logger $db_logger;
/**
* @var TrackingList the list of trackings to update
*/
private readonly TrackingList $tracking_list;
/**
* @var Wikipedia the API of Wikipedia
*/
private readonly Wikipedia $wikipedia;
/**
* @var EmailQueue the queue to add notifications to
*/
private readonly EmailQueue $mailer;
/**
* Constructs a new `UpdateTrackingsAction`.
*
* @param TrackingList $tracking_list the list of trackings to update
* @param Wikipedia $wikipedia the API of Wikipedia
* @param EmailQueue $mailer the queue to add notifications to
*/
public function __construct(TrackingList $tracking_list, Wikipedia $wikipedia, EmailQueue $mailer)
{
$this->logger = LoggerUtil::with_name($this::class);
$this->db_logger = LoggerUtil::db_with_name($this::class);
$this->tracking_list = $tracking_list;
$this->wikipedia = $wikipedia;
$this->mailer = $mailer;
}
/**
* Updates all trackings that users have added.
*
* @param array<int|string, mixed> $inputs `"password": string`: the CLI password
* @return null
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if the CLI password is wrong
* @throws UnexpectedException if Wikipedia could not be reached
* @noinspection PhpDocRedundantThrowsInspection can be thrown through {@see TrackingList::transaction()}
*/
public function handle(array $inputs): mixed
{
(new RuleSet(["password" => [new EqualsCliPasswordRule()]]))->check($inputs);
// Process changes
$new_deletions = [];
$new_undeletions = [];
$new_status_changes = [];
$this->tracking_list->transaction(function () use (&$new_deletions, &$new_undeletions, &$new_status_changes) {
$names = $this->tracking_list->list_all_unique_person_names();
if (empty($names))
return;
try {
$people_statuses = $this->wikipedia->query_people_info($names, resolve_moves: true);
} catch (WikipediaException $exception) {
$this->logger->error("Failed to query page info.", ["cause" => $exception, "pages" => $names]);
throw new UnexpectedException("Could not reach Wikipedia. Maybe the website is down?");
}
$this->tracking_list->rename_persons($people_statuses->redirects);
$new_deletions = $this->tracking_list->delete_persons($people_statuses->missing);
$new_undeletions = $this->tracking_list->undelete_persons(array_keys($people_statuses->results));
$new_status_changes = $this->tracking_list->update_statuses(
array_combine(
array_keys($people_statuses->results),
array_map(fn($it) => $it["status"], $people_statuses->results)
)
);
});
// Send mails, log events
// TODO: Restrict number of notifications to 1 per hour (excluding "oops we're not sure" message)
// TODO: Wait for some time after person has been marked as dead before sending the email
$emails = [];
$person_names = $new_deletions + $new_undeletions + array_keys($new_status_changes);
$trackers = $this->tracking_list->list_trackers($person_names);
foreach ($new_deletions as $new_deletion) {
$this->db_logger->notice("Deleted article $new_deletion.");
foreach ($trackers[$new_deletion] as $user_email)
$emails[] = new NotifyArticleDeletedEmail($user_email, $new_deletion);
}
foreach ($new_undeletions as $new_undeletion) {
$this->db_logger->notice("Undeleted article $new_undeletion.");
foreach ($trackers[$new_undeletion] as $user_email)
$emails[] = new NotifyArticleUndeletedEmail($user_email, $new_undeletion);
}
foreach ($new_status_changes as $person_name => $person_status) {
if ($person_status === PersonStatus::Alive)
$this->db_logger->notice("Person $person_name is now alive again.");
foreach ($trackers[$person_name] as $user_email)
$emails[] = new NotifyStatusChangedEmail($user_email, $person_name, $person_status->value);
}
$this->mailer->enqueue($emails);
return null;
}
}
/**
* An email to inform a user a tracker person's status has changed.
*
* @see UpdateTrackingsAction
*/
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 a new `NotifyStatusChangedEmail`.
*
* @param string $recipient the intended recipient of this 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)
{
parent::__construct($this::TYPE . "/" . $name, $recipient);
$this->name = $name;
$this->new_status = $new_status;
}
public function get_subject(): string
{
return "$this->name may be $this->new_status";
}
public function get_body(): string
{
$base_path = Config::get("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" .
$base_path;
}
}
/**
* An email to inform a user that a tracked article has been deleted.
*
* @see UpdateTrackingsAction
*/
class NotifyArticleDeletedEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "notify-article-deleted";
/**
* @var string the name of the article that was deleted
*/
public string $name;
/**
* Constructs a new `NotifyArticleDeletedEmail`.
*
* @param string $recipient the intended recipient of this email
* @param string $name the name of the article that was deleted
*/
public function __construct(string $recipient, string $name)
{
parent::__construct($this::TYPE . "/" . $name, $recipient);
$this->name = $name;
}
public function get_subject(): string
{
return "$this->name article has been deleted";
}
public function get_body(): string
{
$base_path = Config::get("server.base_path");
return
"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 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.
*
* @see UpdateTrackingsAction
*/
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 a new `NotifyArticleUndeletedEmail`.
*
* @param string $recipient the intended recipient of this email
* @param string $name the name of the article that was re-created
*/
public function __construct(string $recipient, string $name)
{
parent::__construct($this::TYPE . "/" . $name, $recipient);
$this->name = $name;
}
public function get_subject(): string
{
return "$this->name article has been re-created";
}
public function get_body(): string
{
$base_path = Config::get("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;
}
}