Move tracking processing logic to Action classes
This commit is contained in:
parent
18a2d0d779
commit
ca30d9d42c
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "fwdekker/death-notifier",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"version": "0.15.3", "_comment_version": "Also update version in `package.json`!",
|
||||
"version": "0.15.4", "_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.15.3", "_comment_version": "Also update version in `composer.json`!",
|
||||
"version": "0.15.4", "_comment_version": "Also update version in `composer.json`!",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"author": "Florine W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -6,7 +6,7 @@ use com\fwdekker\deathnotifier\Database;
|
|||
use com\fwdekker\deathnotifier\EmulateCronCliAction;
|
||||
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||
use com\fwdekker\deathnotifier\mailer\ProcessEmailQueueCliAction;
|
||||
use com\fwdekker\deathnotifier\mediawiki\Mediawiki;
|
||||
use com\fwdekker\deathnotifier\mediawiki\MediaWiki;
|
||||
use com\fwdekker\deathnotifier\Response;
|
||||
use com\fwdekker\deathnotifier\StartSessionAction;
|
||||
use com\fwdekker\deathnotifier\tracking\AddTrackingAction;
|
||||
|
@ -41,9 +41,9 @@ $logger = Util::create_logger($config["logger"]);
|
|||
$db = new Database($logger->withName("Database"), $config["database"]["filename"]);
|
||||
|
||||
$mailer = new Mailer($config, $logger->withName("Mailer"), $db->conn);
|
||||
$mediawiki = new Mediawiki($logger->withName("Mediawiki"));
|
||||
$mediawiki = new MediaWiki($logger->withName("Mediawiki"));
|
||||
$user_manager = new UserManager($logger->withName("UserManager"), $db->conn, $mailer);
|
||||
$tracking_manager = new TrackingManager($logger->withName("TrackingManager"), $db->conn, $mailer, $mediawiki);
|
||||
$tracking_manager = new TrackingManager($logger->withName("TrackingManager"), $db->conn);
|
||||
|
||||
|
||||
// Handle request
|
||||
|
@ -75,11 +75,11 @@ try {
|
|||
$dispatcher->register_action(new SendPasswordResetAction($user_manager));
|
||||
$dispatcher->register_action(new ResetPasswordAction($user_manager));
|
||||
$dispatcher->register_action(new UserDeleteAction($user_manager));
|
||||
$dispatcher->register_action(new AddTrackingAction($tracking_manager));
|
||||
$dispatcher->register_action(new AddTrackingAction($tracking_manager, $mediawiki));
|
||||
$dispatcher->register_action(new RemoveTrackingAction($tracking_manager));
|
||||
// CLI actions
|
||||
$cli_actions = [
|
||||
new UpdateTrackingsCliAction($config, $tracking_manager),
|
||||
new UpdateTrackingsCliAction($config, $db->conn, $tracking_manager, $mediawiki, $mailer),
|
||||
new ProcessEmailQueueCliAction($config, $mailer),
|
||||
];
|
||||
$dispatcher->register_action($cli_actions[0]);
|
||||
|
|
|
@ -21,11 +21,11 @@ class Database
|
|||
public const LATEST_VERSION = "%%DB_VERSION_NUMBER%%";
|
||||
|
||||
/**
|
||||
* @var Logger The logger to use for logging.
|
||||
* @var Logger the logger to use for logging
|
||||
*/
|
||||
private Logger $logger;
|
||||
/**
|
||||
* @var PDO The PDO object that connects to the database.
|
||||
* @var PDO the PDO object that connects to the database
|
||||
*/
|
||||
public PDO $conn;
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ class EmulateCronCliAction extends Action
|
|||
*/
|
||||
public function handle(): never
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
while (true) {
|
||||
print("Emulating cron jobs.\n");
|
||||
foreach ($this->actions as $action)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* Thrown if something happens that should not be able to happen, and there is no way to recover.
|
||||
*/
|
||||
class InvalidStateException extends Error
|
||||
{
|
||||
// Intentionally left empty
|
||||
}
|
|
@ -9,11 +9,11 @@ namespace com\fwdekker\deathnotifier;
|
|||
class Response
|
||||
{
|
||||
/**
|
||||
* @var mixed The payload corresponding to the client's query.
|
||||
* @var mixed the payload corresponding to the client's query
|
||||
*/
|
||||
public mixed $payload;
|
||||
/**
|
||||
* @var bool `true` if and only if the request was fully completed.
|
||||
* @var bool `true` if and only if the request was fully completed
|
||||
*/
|
||||
public bool $satisfied;
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ use Exception;
|
|||
class StartSessionAction extends Action
|
||||
{
|
||||
/**
|
||||
* @var array the application's configuration
|
||||
* @var array<string, mixed> the application's configuration
|
||||
*/
|
||||
private readonly array $config;
|
||||
/**
|
||||
|
@ -24,7 +24,7 @@ class StartSessionAction extends Action
|
|||
/**
|
||||
* Constructs a new `StartSessionAction`.
|
||||
*
|
||||
* @param mixed $config the application's configuration
|
||||
* @param array<string, mixed> $config the application's configuration
|
||||
* @param UserManager $user_manager the manager to validate the session through
|
||||
*/
|
||||
public function __construct(array $config, UserManager $user_manager)
|
||||
|
|
|
@ -22,19 +22,19 @@ use Exception;
|
|||
abstract class Email
|
||||
{
|
||||
/**
|
||||
* @var string A string identifying the type of email.
|
||||
* @var string a string identifying the type of email
|
||||
*/
|
||||
public string $type;
|
||||
/**
|
||||
* @var string The intended recipient of the email.
|
||||
* @var string the intended recipient of the email
|
||||
*/
|
||||
public string $recipient;
|
||||
/**
|
||||
* @var string The first argument to construct the email.
|
||||
* @var string the first argument to construct the email
|
||||
*/
|
||||
public string $arg1 = "";
|
||||
/**
|
||||
* @var string The second argument to construct the email.
|
||||
* @var string the second argument to construct the email
|
||||
*/
|
||||
public string $arg2 = "";
|
||||
|
||||
|
|
|
@ -17,15 +17,15 @@ use PHPMailer\PHPMailer\SMTP;
|
|||
class Mailer
|
||||
{
|
||||
/**
|
||||
* @var Logger The logger to use for logging.
|
||||
* @var Logger the logger to use for logging
|
||||
*/
|
||||
private Logger $logger;
|
||||
/**
|
||||
* @var array<string, array<string, mixed>> The configuration to use for mailing.
|
||||
* @var array<string, array<string, mixed>> the configuration to use for mailing
|
||||
*/
|
||||
private array $config;
|
||||
/**
|
||||
* @var PDO The database connection to interact with.
|
||||
* @var PDO the database connection to interact with
|
||||
*/
|
||||
private PDO $conn;
|
||||
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\mediawiki;
|
||||
|
||||
use com\fwdekker\deathnotifier\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\Rule;
|
||||
|
||||
|
||||
/**
|
||||
* Verifies that the input refers to a page about a person on Wikipedia.
|
||||
*/
|
||||
class IsPersonPageRule extends Rule
|
||||
{
|
||||
/**
|
||||
* @var MediaWiki the instance to connect to Wikipedia with
|
||||
*/
|
||||
private readonly MediaWiki $mediawiki;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `IsPersonPageRule`.
|
||||
*
|
||||
* @param MediaWiki $mediawiki the instance to connect to Wikipedia with
|
||||
* @param string|null $override_message the message to return if the rule does not apply to some input. If `null`,
|
||||
* the rule implementation can choose an appropriate message
|
||||
*/
|
||||
public function __construct(MediaWiki $mediawiki, ?string $override_message = null)
|
||||
{
|
||||
parent::__construct($override_message);
|
||||
|
||||
$this->mediawiki = $mediawiki;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verifies that the input refers to a page about a person on Wikipedia.
|
||||
*
|
||||
* @param array<int|string, mixed> $inputs the list of inputs in which the value at `key` should be checked
|
||||
* @param string $key the key in `inputs` of the input to check
|
||||
* @return void if `$inputs[$key]` refers to a page about a person on Wikipedia
|
||||
* @throws ValidationException if `$inputs[$key]` is not set or does not refer to a page about a person on Wikipedia
|
||||
*/
|
||||
public function check(array $inputs, string $key): void
|
||||
{
|
||||
if (!isset($inputs[$key])) throw new ValidationException($this->override_message ?? "Field must be set.", $key);
|
||||
|
||||
$person_name = $inputs[$key];
|
||||
try {
|
||||
$info = $this->mediawiki->query_person_info([$person_name]);
|
||||
} catch (MediaWikiException) {
|
||||
// TODO: Log this exception!
|
||||
// $this->logger->error("Failed to query page info.", ["cause" => $exception, "name" => $person_name]);
|
||||
throw new ValidationException(
|
||||
$this->override_message ?? "Could not reach Wikipedia. Maybe the website is down?"
|
||||
);
|
||||
}
|
||||
|
||||
$normalized_name = $info->redirects[$person_name];
|
||||
$status = $info->results[$normalized_name]["status"];
|
||||
$type = $info->results[$normalized_name]["type"];
|
||||
|
||||
if (in_array($normalized_name, $info->missing))
|
||||
throw new ValidationException(
|
||||
$this->override_message ??
|
||||
"Wikipedia does not have an article about " .
|
||||
"<b><a href='https://en.wikipedia.org/wiki/Special:Search?search=" .
|
||||
rawurlencode($normalized_name) . "'>" . htmlentities($person_name) . "</a></b>. " .
|
||||
"Maybe you need to capitalise the surname?",
|
||||
"person_name"
|
||||
);
|
||||
|
||||
if ($type === ArticleType::Disambiguation)
|
||||
throw new ValidationException(
|
||||
$this->override_message ??
|
||||
"<b><a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>" .
|
||||
htmlentities($normalized_name) . "</a></b> refers to multiple articles. " .
|
||||
"<a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>Check Wikipedia</a> " .
|
||||
"to see if your article is listed.",
|
||||
"person_name"
|
||||
);
|
||||
|
||||
if ($type === ArticleType::Other)
|
||||
throw new ValidationException(
|
||||
$this->override_message ??
|
||||
"The Wikipedia article about " .
|
||||
"<b><a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>" .
|
||||
htmlentities($normalized_name) . "</a></b> is not about a real-world person.",
|
||||
"person_name"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,15 +2,13 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\mediawiki;
|
||||
|
||||
use com\fwdekker\deathnotifier\tracking\PersonStatus;
|
||||
use Exception;
|
||||
use Monolog\Logger;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class for interacting with Wikipedia's API.
|
||||
*/
|
||||
class Mediawiki
|
||||
class MediaWiki
|
||||
{
|
||||
/**
|
||||
* The URL of Wikipedia's API endpoint.
|
||||
|
@ -35,7 +33,7 @@ class Mediawiki
|
|||
private const CATS_PER_QUERY = 500;
|
||||
|
||||
/**
|
||||
* @var Logger The logger to use for logging.
|
||||
* @var Logger the logger to use for logging
|
||||
*/
|
||||
private Logger $logger; // @phpstan-ignore-line Unused, but useful for debugging
|
||||
|
||||
|
@ -56,7 +54,7 @@ class Mediawiki
|
|||
*
|
||||
* @param array<string, mixed> $params the request parameters to send to the API
|
||||
* @return mixed a JSON object containing the API's response
|
||||
* @throws Exception if the request fails
|
||||
* @throws MediaWikiException() if the request fails
|
||||
*/
|
||||
private function api_fetch(array $params): mixed
|
||||
{
|
||||
|
@ -68,7 +66,7 @@ class Mediawiki
|
|||
$output = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
if (is_bool($output) || curl_error($ch))
|
||||
throw new Exception(curl_error($ch));
|
||||
throw new MediaWikiException(curl_error($ch));
|
||||
|
||||
return json_decode($output, associative: true);
|
||||
}
|
||||
|
@ -80,7 +78,7 @@ class Mediawiki
|
|||
* @param string|null $continue_name the name of the continue parameter to follow, or `null` if no continuation
|
||||
* should be done
|
||||
* @return mixed[] the query's value of the `query` key as a JSON object
|
||||
* @throws Exception if the query fails
|
||||
* @throws MediaWikiException if the query fails
|
||||
*/
|
||||
private function api_query_continued(array $params, ?string $continue_name = null): array
|
||||
{
|
||||
|
@ -116,7 +114,8 @@ class Mediawiki
|
|||
* @param string[] $titles the titles to query
|
||||
* @param string|null $continue_name the name of the continue parameter used for this request by the API
|
||||
* @return QueryOutput<mixed> the API's response
|
||||
* @throws Exception if the query fails
|
||||
* @throws MediaWikiException if the query fails
|
||||
* @noinspection PhpSameParameterValueInspection `$continue_name` may take other values in the future
|
||||
*/
|
||||
private function api_query_batched(array $params, array $titles, ?string $continue_name): QueryOutput
|
||||
{
|
||||
|
@ -202,12 +201,11 @@ class Mediawiki
|
|||
* Checks for each person what their status is according to Wikipedia's categorization.
|
||||
*
|
||||
* @param string[] $names the names of the people to check aliveness of
|
||||
* @return QueryOutput<array{"status": PersonStatus|null, "type": ArticleType}> a query output with results mapping
|
||||
* each article's normalized title to the person's status (or `null` if the article is not about a person) and to
|
||||
* the article's type
|
||||
* @throws Exception if the query fails
|
||||
* @return QueryOutput<array{"type": ArticleType, "status": PersonStatus|null}> a query output with results mapping
|
||||
* each article's normalized title to the article's type and, if the article is about a person, the person's status
|
||||
* @throws MediaWikiException if the query fails
|
||||
*/
|
||||
public function query_page_info(array $names): QueryOutput
|
||||
public function query_person_info(array $names): QueryOutput
|
||||
{
|
||||
$output = $this->api_query_batched(
|
||||
params: ["prop" => "categories", "cllimit" => strval(self::CATS_PER_QUERY)],
|
||||
|
@ -220,8 +218,8 @@ class Mediawiki
|
|||
array_column($output->results, "title"),
|
||||
array_map(
|
||||
fn($it) => [
|
||||
"type" => $this->article_type($it),
|
||||
"status" => $this->person_status($it),
|
||||
"type" => $this->article_type($it)
|
||||
],
|
||||
$output->results
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\mediawiki;
|
||||
|
||||
use Exception;
|
||||
|
||||
|
||||
/**
|
||||
* Thrown if something goes wrong while interacting with the MediaWiki API.
|
||||
*/
|
||||
class MediaWikiException extends Exception {
|
||||
// Empty
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace com\fwdekker\deathnotifier\tracking;
|
||||
namespace com\fwdekker\deathnotifier\mediawiki;
|
||||
|
||||
|
||||
/**
|
|
@ -5,8 +5,16 @@ namespace com\fwdekker\deathnotifier\tracking;
|
|||
use com\fwdekker\deathnotifier\Action;
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
use com\fwdekker\deathnotifier\mediawiki\ArticleType;
|
||||
use com\fwdekker\deathnotifier\mediawiki\IsPersonPageRule;
|
||||
use com\fwdekker\deathnotifier\mediawiki\MediaWiki;
|
||||
use com\fwdekker\deathnotifier\mediawiki\MediaWikiException;
|
||||
use com\fwdekker\deathnotifier\Response;
|
||||
use com\fwdekker\deathnotifier\ValidationException;
|
||||
use com\fwdekker\deathnotifier\validator\HasLengthRule;
|
||||
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
|
||||
use Exception;
|
||||
use InvalidStateException;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -14,15 +22,23 @@ use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
|
|||
*/
|
||||
class AddTrackingAction extends Action
|
||||
{
|
||||
/**
|
||||
* @var TrackingManager the manager to add the tracking to
|
||||
*/
|
||||
private readonly TrackingManager $tracking_manager;
|
||||
/**
|
||||
* @var MediaWiki the instance to connect to Wikipedia with
|
||||
*/
|
||||
private readonly MediaWiki $mediawiki;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `AddTrackingAction`.
|
||||
*
|
||||
* @param TrackingManager $tracking_manager the manager to add the tracking to
|
||||
* @param MediaWiki $mediawiki the instance to connect to Wikipedia with
|
||||
*/
|
||||
public function __construct(TrackingManager $tracking_manager)
|
||||
public function __construct(TrackingManager $tracking_manager, MediaWiki $mediawiki)
|
||||
{
|
||||
parent::__construct(
|
||||
ActionMethod::POST,
|
||||
|
@ -32,21 +48,54 @@ class AddTrackingAction extends Action
|
|||
rule_lists: [
|
||||
"person_name" => [
|
||||
new IsNotBlankRule(),
|
||||
new HasLengthRule(TrackingManager::MIN_TITLE_LENGTH, TrackingManager::MAX_TITLE_LENGTH)
|
||||
new HasLengthRule(TrackingManager::MIN_TITLE_LENGTH, TrackingManager::MAX_TITLE_LENGTH),
|
||||
new IsPersonPageRule($mediawiki)
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
$this->tracking_manager = $tracking_manager;
|
||||
$this->mediawiki = $mediawiki;
|
||||
}
|
||||
|
||||
|
||||
function handle(): mixed
|
||||
/**
|
||||
* Adds a tracking by the current user of `$_POST["person_name"]`.
|
||||
*
|
||||
* @return array{"name": string, "input": string, "renamed": bool} the
|
||||
* @throws ActionException if the Wikipedia API could not be reached
|
||||
* @throws ValidationException if the user is already tracking this person
|
||||
*/
|
||||
function handle(): array
|
||||
{
|
||||
$response = $this->tracking_manager->add_tracking($_SESSION["uuid"], $_POST["person_name"]);
|
||||
if (!$response->satisfied)
|
||||
throw new ActionException($response->payload["message"], $response->payload["target"]);
|
||||
$user_uuid = $_SESSION["uuid"];
|
||||
$person_name = strval($_POST["person_name"]);
|
||||
|
||||
return $response->payload;
|
||||
// Query API
|
||||
try {
|
||||
$info = $this->mediawiki->query_person_info([$person_name]);
|
||||
} catch (MediaWikiException $exception) {
|
||||
// TODO: Log this?
|
||||
// $this->logger->error("Failed to query page info.", ["cause" => $exception, "name" => $person_name]);
|
||||
throw new ActionException("Could not reach Wikipedia. Maybe the website is down?");
|
||||
}
|
||||
|
||||
$normalized_name = $info->redirects[$person_name];
|
||||
$status = $info->results[$normalized_name]["status"];
|
||||
if ($status === null)
|
||||
throw new InvalidStateException("Article is about person, but person has no status.");
|
||||
|
||||
// Add tracking (Transaction is not necessary)
|
||||
if ($this->tracking_manager->has_tracking($user_uuid, $normalized_name))
|
||||
throw new ValidationException("You are already tracking <b>$normalized_name</b>.");
|
||||
|
||||
$this->tracking_manager->add_tracking($user_uuid, $normalized_name, $status);
|
||||
|
||||
// Respond
|
||||
return [
|
||||
"name" => $normalized_name,
|
||||
"input" => $person_name,
|
||||
"renamed" => strtolower($person_name) !== strtolower($normalized_name)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,22 @@ use com\fwdekker\deathnotifier\ActionException;
|
|||
use com\fwdekker\deathnotifier\ActionMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Lists all trackings of the current user.
|
||||
*/
|
||||
class ListTrackingsAction extends Action
|
||||
{
|
||||
/**
|
||||
* @var TrackingManager the manager to fetch trackings with
|
||||
*/
|
||||
private readonly TrackingManager $tracking_manager;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `ListTrackingsAction`.
|
||||
*
|
||||
* @param TrackingManager $tracking_manager the manager to fetch trackings with
|
||||
*/
|
||||
public function __construct(TrackingManager $tracking_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
|
@ -25,12 +36,13 @@ class ListTrackingsAction extends Action
|
|||
}
|
||||
|
||||
|
||||
function handle(): mixed
|
||||
/**
|
||||
* Returns all trackings of the current user.
|
||||
*
|
||||
* @return mixed[] all trackings of the current user
|
||||
*/
|
||||
function handle(): array
|
||||
{
|
||||
$response = $this->tracking_manager->list_trackings($_SESSION["uuid"]);
|
||||
if (!$response->satisfied)
|
||||
throw new ActionException($response->payload["message"], $response->payload["target"]);
|
||||
|
||||
return $response->payload;
|
||||
return $this->tracking_manager->list_trackings($_SESSION["uuid"]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class NotifyArticleDeletedEmail extends Email
|
|||
public const TYPE = "notify-article-deleted";
|
||||
|
||||
/**
|
||||
* @var string The name of the article that was deleted.
|
||||
* @var string the name of the article that was deleted
|
||||
*/
|
||||
public string $name;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class NotifyArticleUndeletedEmail extends Email
|
|||
public const TYPE = "notify-article-undeleted";
|
||||
|
||||
/**
|
||||
* @var string The name of the article that was re-created.
|
||||
* @var string the name of the article that was re-created
|
||||
*/
|
||||
public string $name;
|
||||
|
||||
|
|
|
@ -16,11 +16,11 @@ class NotifyStatusChangedEmail extends Email
|
|||
public const TYPE = "notify-status-changed";
|
||||
|
||||
/**
|
||||
* @var string The name of the person whose status has changed.
|
||||
* @var string the name of the person whose status has changed
|
||||
*/
|
||||
public string $name;
|
||||
/**
|
||||
* @var string The new status of the person.
|
||||
* @var string the new status of the person
|
||||
*/
|
||||
public string $new_status;
|
||||
|
||||
|
|
|
@ -8,11 +8,22 @@ use com\fwdekker\deathnotifier\ActionMethod;
|
|||
use com\fwdekker\deathnotifier\validator\IsNotBlankRule;
|
||||
|
||||
|
||||
/**
|
||||
* Removes a tracking of the current user.
|
||||
*/
|
||||
class RemoveTrackingAction extends Action
|
||||
{
|
||||
/**
|
||||
* @var TrackingManager the manager to remove the tracking with
|
||||
*/
|
||||
private readonly TrackingManager $tracking_manager;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `RemoveTrackingAction`.
|
||||
*
|
||||
* @param TrackingManager $tracking_manager the manager to remove the tracking with
|
||||
*/
|
||||
public function __construct(TrackingManager $tracking_manager)
|
||||
{
|
||||
parent::__construct(
|
||||
|
@ -27,12 +38,15 @@ class RemoveTrackingAction extends Action
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the current user's tracking of `$_POST["person_name"]`.
|
||||
*
|
||||
* @return mixed `null`
|
||||
*/
|
||||
function handle(): mixed
|
||||
{
|
||||
$response = $this->tracking_manager->remove_tracking($_SESSION["uuid"], $_POST["person_name"]);
|
||||
if (!$response->satisfied)
|
||||
throw new ActionException($response->payload["message"], $response->payload["target"]);
|
||||
$this->tracking_manager->remove_tracking($_SESSION["uuid"], $_POST["person_name"]);
|
||||
|
||||
return $response->payload;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,8 @@
|
|||
namespace com\fwdekker\deathnotifier\tracking;
|
||||
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||
use com\fwdekker\deathnotifier\mediawiki\ArticleType;
|
||||
use com\fwdekker\deathnotifier\mediawiki\Mediawiki;
|
||||
use com\fwdekker\deathnotifier\Response;
|
||||
use Exception;
|
||||
use com\fwdekker\deathnotifier\mediawiki\PersonStatus;
|
||||
use Monolog\Logger;
|
||||
use PDO;
|
||||
|
||||
|
@ -27,21 +24,13 @@ class TrackingManager
|
|||
public const MAX_TITLE_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* @var Logger The logger to log with.
|
||||
* @var Logger the logger to log with
|
||||
*/
|
||||
private Logger $logger;
|
||||
/**
|
||||
* @var PDO The database connection to interact with.
|
||||
* @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;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -49,15 +38,11 @@ class TrackingManager
|
|||
*
|
||||
* @param Logger $logger the logger to log with
|
||||
* @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(Logger $logger, PDO $conn, Mailer $mailer, Mediawiki $mediawiki)
|
||||
public function __construct(Logger $logger, PDO $conn)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->conn = $conn;
|
||||
$this->mailer = $mailer;
|
||||
$this->mediawiki = $mediawiki;
|
||||
}
|
||||
|
||||
|
||||
|
@ -96,112 +81,72 @@ class TrackingManager
|
|||
*
|
||||
* @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
|
||||
* @param PersonStatus $status the status of the person to track
|
||||
* @return void
|
||||
*/
|
||||
public function add_tracking(string $user_uuid, string $person_name): Response
|
||||
public function add_tracking(string $user_uuid, string $person_name, PersonStatus $status): void
|
||||
{
|
||||
try {
|
||||
$info = $this->mediawiki->query_page_info([$person_name]);
|
||||
} catch (Exception $exception) {
|
||||
$this->logger->error("Failed to query page info.", ["cause" => $exception, "name" => $person_name]);
|
||||
return Response::unsatisfied("Could not reach Wikipedia. Maybe the website is down?");
|
||||
}
|
||||
|
||||
$normalized_name = $info->redirects[$person_name];
|
||||
$status = $info->results[$normalized_name]["status"];
|
||||
$type = $info->results[$normalized_name]["type"];
|
||||
|
||||
if (in_array($normalized_name, $info->missing))
|
||||
return Response::unsatisfied(
|
||||
"Wikipedia does not have an article about " .
|
||||
"<b><a href='https://en.wikipedia.org/wiki/Special:Search?search=" .
|
||||
rawurlencode($normalized_name) . "'>" . htmlentities($person_name) . "</a></b>. " .
|
||||
"Maybe you need to capitalise the surname?",
|
||||
"person_name"
|
||||
);
|
||||
if ($type === ArticleType::Disambiguation)
|
||||
return Response::unsatisfied(
|
||||
"<b><a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>" .
|
||||
htmlentities($normalized_name) . "</a></b> refers to multiple articles. " .
|
||||
"<a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>Check Wikipedia</a> " .
|
||||
"to see if your article is listed.",
|
||||
"person_name"
|
||||
);
|
||||
if ($type === ArticleType::Other)
|
||||
return Response::unsatisfied(
|
||||
"The Wikipedia article about " .
|
||||
"<b><a href='https://en.wikipedia.org/wiki/" . rawurlencode($normalized_name) . "'>" .
|
||||
htmlentities($normalized_name) . "</a></b> is not about a person.",
|
||||
"person_name"
|
||||
);
|
||||
if ($status === null)
|
||||
return Response::unsatisfied("Failed to understand the person's status. This is a bug. Try again maybe?");
|
||||
|
||||
// Insert person and tracking
|
||||
return Database::transaction(
|
||||
Database::transaction(
|
||||
$this->conn,
|
||||
function () use ($user_uuid, $person_name, $normalized_name, $status) {
|
||||
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1
|
||||
FROM trackings
|
||||
WHERE user_uuid=:uuid AND person_name=:name);");
|
||||
$stmt->bindValue(":uuid", $user_uuid);
|
||||
$stmt->bindValue(":name", $normalized_name);
|
||||
$stmt->execute();
|
||||
if ($stmt->fetch()[0] === 1)
|
||||
return Response::unsatisfied(
|
||||
"You are already tracking <b>" . htmlentities($normalized_name) . "</b>.",
|
||||
"person_name"
|
||||
);
|
||||
|
||||
function () use ($user_uuid, $person_name, $status) {
|
||||
$stmt = $this->conn->prepare("INSERT OR IGNORE INTO people (name) VALUES (:name);");
|
||||
$stmt->bindValue(":name", $normalized_name);
|
||||
$stmt->bindValue(":name", $person_name);
|
||||
$stmt->execute();
|
||||
|
||||
$stmt = $this->conn->prepare("UPDATE people SET status=:status WHERE name=:name;");
|
||||
$stmt->bindValue(":name", $normalized_name);
|
||||
$stmt->bindValue(":name", $person_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->bindValue(":person_name", $person_name);
|
||||
$stmt->execute();
|
||||
|
||||
return Response::satisfied([
|
||||
"name" => $normalized_name,
|
||||
"input" => $person_name,
|
||||
"renamed" => strtolower($person_name) !== strtolower($normalized_name)
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if and only if the indicated user currently tracks the indicated person.
|
||||
*
|
||||
* @param string $user_uuid the user to check
|
||||
* @param string $person_name the person to check
|
||||
* @return bool `true` if and only if the indicated user currently tracks the indicated person
|
||||
*/
|
||||
public function has_tracking(string $user_uuid, string $person_name): bool
|
||||
{
|
||||
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1
|
||||
FROM trackings
|
||||
WHERE user_uuid=:uuid AND person_name=:name);");
|
||||
$stmt->bindValue(":uuid", $user_uuid);
|
||||
$stmt->bindValue(":name", $person_name);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch()[0] === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`
|
||||
* @return void
|
||||
*/
|
||||
public function remove_tracking(string $user_uuid, string $person_name): Response
|
||||
public function remove_tracking(string $user_uuid, string $person_name): void
|
||||
{
|
||||
$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 Response::satisfied();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all trackings of the indicated user.
|
||||
* Returns 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
|
||||
* @return array<array{"name": string, "status": string, "is_deleted": bool}> all trackings of the indicated user
|
||||
*/
|
||||
public function list_trackings(string $user_uuid): Response
|
||||
public function list_trackings(string $user_uuid): array
|
||||
{
|
||||
$stmt = $this->conn->prepare("SELECT people.name, people.status, people.is_deleted
|
||||
FROM trackings
|
||||
|
@ -210,9 +155,7 @@ class TrackingManager
|
|||
AND trackings.person_name=people.name;");
|
||||
$stmt->bindValue(":user_uuid", $user_uuid);
|
||||
$stmt->execute();
|
||||
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return Response::satisfied($results);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -221,18 +164,18 @@ class TrackingManager
|
|||
* @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
|
||||
public function list_trackers(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");
|
||||
$stmt = $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;");
|
||||
$stmt->bindParam(":person_name", $person_name);
|
||||
$stmt->execute();
|
||||
return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), "email");
|
||||
}
|
||||
|
||||
|
||||
|
@ -248,136 +191,122 @@ class TrackingManager
|
|||
return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), "name");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates trackings for the given names.
|
||||
*
|
||||
* @param string[] $names the names of the trackings to update
|
||||
* @return void
|
||||
*/
|
||||
public function update_trackings(array $names): void
|
||||
{
|
||||
if (empty($names)) return;
|
||||
|
||||
try {
|
||||
$people_statuses = $this->mediawiki->query_page_info($names);
|
||||
} catch (Exception $exception) {
|
||||
$this->logger->error("Failed to retrieve page information.", ["cause" => $exception, "pages" => $names]);
|
||||
return;
|
||||
}
|
||||
|
||||
Database::transaction($this->conn, function () use ($people_statuses) {
|
||||
$this->process_redirects($people_statuses->redirects);
|
||||
$this->process_deletions($people_statuses->missing);
|
||||
$this->process_statuses($people_statuses->results);
|
||||
return Response::satisfied();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> $renamings a map of all changes, from old name to new name
|
||||
* @return void
|
||||
*/
|
||||
private function process_redirects(array $redirects): void
|
||||
public function rename_persons(array $renamings): void
|
||||
{
|
||||
// Query to rename person
|
||||
$rename = $this->conn->prepare("UPDATE people SET name=:new_name WHERE name=:old_name;");
|
||||
$rename->bindParam(":old_name", $from);
|
||||
$rename->bindParam(":new_name", $to);
|
||||
Database::transaction($this->conn, function () use ($renamings) {
|
||||
// Query to rename person
|
||||
$rename = $this->conn->prepare("UPDATE people SET name=:new_name WHERE name=:old_name;");
|
||||
$rename->bindParam(":old_name", $from);
|
||||
$rename->bindParam(":new_name", $to);
|
||||
|
||||
// Query to see if row with new name already exists
|
||||
$merge_needed = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM people WHERE name=:new_name);");
|
||||
$merge_needed->bindParam(":new_name", $to);
|
||||
// Query to see if row with new name already exists
|
||||
$merge_needed = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM people WHERE name=:new_name);");
|
||||
$merge_needed->bindParam(":new_name", $to);
|
||||
|
||||
// Queries to merge old name row with new name row
|
||||
$merge_update = $this->conn->prepare("UPDATE OR IGNORE trackings
|
||||
SET person_name=:new_name
|
||||
WHERE person_name=:old_name;");
|
||||
$merge_update->bindParam(":old_name", $from);
|
||||
$merge_update->bindParam(":new_name", $to);
|
||||
// Queries to merge old name row with new name row
|
||||
$merge_update = $this->conn->prepare("UPDATE OR IGNORE trackings
|
||||
SET person_name=:new_name
|
||||
WHERE person_name=:old_name;");
|
||||
$merge_update->bindParam(":old_name", $from);
|
||||
$merge_update->bindParam(":new_name", $to);
|
||||
|
||||
$merge_remove = $this->conn->prepare("DELETE FROM people WHERE name=:old_name;");
|
||||
$merge_remove->bindParam(":old_name", $from);
|
||||
$merge_remove = $this->conn->prepare("DELETE FROM people WHERE name=:old_name;");
|
||||
$merge_remove->bindParam(":old_name", $from);
|
||||
|
||||
// Perform queries
|
||||
foreach ($redirects as $from => $to) {
|
||||
if ($from === $to) continue;
|
||||
// Perform queries
|
||||
foreach ($renamings as $from => $to) {
|
||||
if ($from === $to) continue;
|
||||
|
||||
$merge_needed->execute();
|
||||
if ($merge_needed->fetch()[0] === 1) {
|
||||
$merge_update->execute();
|
||||
$merge_remove->execute();
|
||||
} else {
|
||||
$rename->execute();
|
||||
$merge_needed->execute();
|
||||
if ($merge_needed->fetch()[0] === 1) {
|
||||
$merge_update->execute();
|
||||
$merge_remove->execute();
|
||||
} else {
|
||||
$rename->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes people from the database.
|
||||
*
|
||||
* @param string[] $deletions list of names of people to remove from the database
|
||||
* @return void
|
||||
* @return string[] list of names of people who were actually removed from the database
|
||||
*/
|
||||
private function process_deletions(array $deletions): void
|
||||
public function delete_persons(array $deletions): array
|
||||
{
|
||||
// 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);
|
||||
$actual_deletions = [];
|
||||
|
||||
foreach ($deletions as $deleted_name) {
|
||||
$delete->execute();
|
||||
$deleted = sizeof($delete->fetchAll(PDO::FETCH_ASSOC)) > 0;
|
||||
if ($deleted)
|
||||
foreach ($this->get_recipients($deleted_name) as $user_email)
|
||||
$this->mailer->queue_email(new NotifyArticleDeletedEmail($user_email, $deleted_name));
|
||||
}
|
||||
Database::transaction($this->conn, function () use ($deletions, &$actual_deletions) {
|
||||
// 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)
|
||||
$actual_deletions[] = $deleted_name;
|
||||
}
|
||||
});
|
||||
|
||||
return $actual_deletions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates peoples' statuses.
|
||||
*
|
||||
* @param array<string, array{"status": PersonStatus|null, "type": ArticleType}> $statuses the current statuses of
|
||||
* @param array<string, array{"type": ArticleType, "status": PersonStatus|null}> $statuses the current statuses of
|
||||
* people
|
||||
* @return void
|
||||
* @return array{"undeletions": string[], "status_changes": array<string, string>} the list of articles that were
|
||||
* actually undeleted, and a mapping of articles that were actually changes to the new status
|
||||
*/
|
||||
private function process_statuses(array $statuses): void
|
||||
public function update_statuses(array $statuses): array
|
||||
{
|
||||
// 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);
|
||||
$undeletions = [];
|
||||
$status_changes = [];
|
||||
|
||||
// Query to update status, returning `name` to determine whether something changed
|
||||
$set_status = $this->conn->prepare("UPDATE people
|
||||
SET status=:status
|
||||
WHERE name=:name AND status<>:status
|
||||
RETURNING name;");
|
||||
$set_status->bindParam(":status", $person_status);
|
||||
$set_status->bindParam(":name", $person_name);
|
||||
Database::transaction($this->conn, function () use ($statuses, &$undeletions, &$status_changes) {
|
||||
// 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);
|
||||
|
||||
foreach ($statuses as $person_name => $person_info) {
|
||||
if ($person_info["status"] === null) continue;
|
||||
$person_status = $person_info["status"]->value;
|
||||
// Query to update status, returning `name` to determine whether something changed
|
||||
$set_status = $this->conn->prepare("UPDATE people
|
||||
SET status=:status
|
||||
WHERE name=:name AND status<>:status
|
||||
RETURNING name;");
|
||||
$set_status->bindParam(":status", $person_status_string);
|
||||
$set_status->bindParam(":name", $person_name);
|
||||
|
||||
$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));
|
||||
foreach ($statuses as $person_name => $person_info) {
|
||||
if ($person_info["status"] === null) continue;
|
||||
$person_status_string = $person_info["status"]->value;
|
||||
|
||||
$set_status->execute();
|
||||
$status_changed = sizeof($set_status->fetchAll(PDO::FETCH_ASSOC)) > 0;
|
||||
if ($status_changed)
|
||||
foreach ($this->get_recipients($person_name) as $user_email)
|
||||
$this->mailer->queue_email(new NotifyStatusChangedEmail($user_email, $person_name, $person_status));
|
||||
}
|
||||
$undelete->execute();
|
||||
$undeleted = sizeof($undelete->fetchAll(PDO::FETCH_ASSOC)) > 0;
|
||||
if ($undeleted) $undeletions[] = $person_name;
|
||||
|
||||
$set_status->execute();
|
||||
$status_changed = sizeof($set_status->fetchAll(PDO::FETCH_ASSOC)) > 0;
|
||||
if ($status_changed) $status_changes[$person_name] = $person_status_string;
|
||||
}
|
||||
});
|
||||
|
||||
return ["undeletions" => $undeletions, "status_changes" => $status_changes];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,13 @@
|
|||
|
||||
namespace com\fwdekker\deathnotifier\tracking;
|
||||
|
||||
use com\fwdekker\deathnotifier\ActionException;
|
||||
use com\fwdekker\deathnotifier\CliAction;
|
||||
use com\fwdekker\deathnotifier\Database;
|
||||
use com\fwdekker\deathnotifier\mailer\Mailer;
|
||||
use com\fwdekker\deathnotifier\mediawiki\MediaWiki;
|
||||
use com\fwdekker\deathnotifier\mediawiki\MediaWikiException;
|
||||
use PDO;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -10,23 +16,42 @@ use com\fwdekker\deathnotifier\CliAction;
|
|||
*/
|
||||
class UpdateTrackingsCliAction extends CliAction
|
||||
{
|
||||
/**
|
||||
* @var PDO the database connection to interact with
|
||||
*/
|
||||
private readonly PDO $conn;
|
||||
/**
|
||||
* @var TrackingManager the manager through which trackings should be updated
|
||||
*/
|
||||
private readonly TrackingManager $tracking_manager;
|
||||
/**
|
||||
* @var MediaWiki the instance to connect to Wikipedia with
|
||||
*/
|
||||
private readonly MediaWiki $mediawiki;
|
||||
/**
|
||||
* @var Mailer the mailer to send emails with
|
||||
*/
|
||||
private readonly Mailer $mailer;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new `UpdateTrackingsAction`.
|
||||
*
|
||||
* @param mixed $config the application's configuration
|
||||
* @param PDO $conn the database connection to interact with
|
||||
* @param TrackingManager $tracking_manager the manager through which trackings should be updated
|
||||
* @param MediaWiki $mediawiki the instance to connect to Wikipedia with
|
||||
* @param Mailer $mailer the mailer to send emails with
|
||||
*/
|
||||
public function __construct(mixed $config, TrackingManager $tracking_manager)
|
||||
public function __construct(mixed $config, PDO $conn, TrackingManager $tracking_manager, MediaWiki $mediawiki,
|
||||
Mailer $mailer)
|
||||
{
|
||||
parent::__construct($config, "update-trackings");
|
||||
|
||||
$this->conn = $conn;
|
||||
$this->tracking_manager = $tracking_manager;
|
||||
$this->mediawiki = $mediawiki;
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
|
||||
|
@ -34,10 +59,55 @@ class UpdateTrackingsCliAction extends CliAction
|
|||
* Updates all trackings that users have added.
|
||||
*
|
||||
* @return mixed `null`
|
||||
* @throws ActionException if the Wikipedia API could not be reached
|
||||
*/
|
||||
public function handle(): mixed
|
||||
{
|
||||
$this->tracking_manager->update_trackings($this->tracking_manager->list_all_unique_person_names());
|
||||
$names = $this->tracking_manager->list_all_unique_person_names();
|
||||
if (empty($names)) return;
|
||||
|
||||
// Fetch changes
|
||||
try {
|
||||
$people_statuses = $this->mediawiki->query_person_info($names);
|
||||
} catch (MediaWikiException) {
|
||||
throw new ActionException("Could not reach Wikipedia. Maybe the website is down?");
|
||||
// TODO: Log this exception
|
||||
// $this->logger->error("Failed to retrieve page information.", ["cause" => $exception, "pages" => $names]);
|
||||
}
|
||||
|
||||
// Process changes
|
||||
$actual_deletions = [];
|
||||
$undeletions = [];
|
||||
$status_changes = [];
|
||||
Database::transaction(
|
||||
$this->conn,
|
||||
function () use (
|
||||
$people_statuses,
|
||||
&$actual_deletions,
|
||||
&$undeletions,
|
||||
&$status_changes
|
||||
) {
|
||||
$this->tracking_manager->rename_persons($people_statuses->redirects);
|
||||
$actual_deletions = $this->tracking_manager->delete_persons($people_statuses->missing);
|
||||
["undeletions" => $undeletions, "status_changes" => $status_changes] =
|
||||
$this->tracking_manager->update_statuses($people_statuses->results);
|
||||
}
|
||||
);
|
||||
|
||||
// Send mails
|
||||
// TODO: Restrict number of notifications to 1 per hour (excluding "oops we're not sure" message)
|
||||
// TODO: Reuse `stmt`s for listing trackers and queueing emails to reduce overheads
|
||||
foreach ($actual_deletions as $deletion)
|
||||
foreach ($this->tracking_manager->list_trackers($deletion) as $user_email)
|
||||
$this->mailer->queue_email(new NotifyArticleDeletedEmail($user_email, $deletion));
|
||||
|
||||
foreach ($undeletions as $undeletion)
|
||||
foreach ($this->tracking_manager->list_trackers($undeletion) as $user_email)
|
||||
$this->mailer->queue_email(new NotifyArticleUndeletedEmail($user_email, $undeletion));
|
||||
|
||||
foreach ($status_changes as $person_name => $person_status)
|
||||
foreach ($this->tracking_manager->list_trackers($person_name) as $user_email)
|
||||
$this->mailer->queue_email(new NotifyStatusChangedEmail($user_email, $person_name, $person_status));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class ChangedEmailEmail extends Email
|
|||
public const TYPE = "changed-email";
|
||||
|
||||
/**
|
||||
* @var string The token to verify the email address with.
|
||||
* @var string the token to verify the email address with
|
||||
*/
|
||||
public string $token;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class RegisterEmail extends Email
|
|||
public const TYPE = "register";
|
||||
|
||||
/**
|
||||
* @var string The token to verify the email address with.
|
||||
* @var string the token to verify the email address with
|
||||
*/
|
||||
public string $token;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class ResetPasswordEmail extends Email
|
|||
public const TYPE = "reset-password";
|
||||
|
||||
/**
|
||||
* @var string The token to reset the password with.
|
||||
* @var string the token to reset the password with
|
||||
*/
|
||||
public string $token;
|
||||
|
||||
|
|
|
@ -41,15 +41,15 @@ class UserManager
|
|||
|
||||
|
||||
/**
|
||||
* @var Logger The logger to use for logging.
|
||||
* @var Logger the logger to use for logging
|
||||
*/
|
||||
private Logger $logger; // @phpstan-ignore-line Unused, but useful for debugging
|
||||
/**
|
||||
* @var PDO The database connection to interact with.
|
||||
* @var PDO the database connection to interact with
|
||||
*/
|
||||
private PDO $conn;
|
||||
/**
|
||||
* @var Mailer The mailer to send emails with.
|
||||
* @var Mailer the mailer to send emails with
|
||||
*/
|
||||
private Mailer $mailer;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class VerifyEmailEmail extends Email
|
|||
public const TYPE = "verify-email";
|
||||
|
||||
/**
|
||||
* @var string The token to verify the email address with.
|
||||
* @var string the token to verify the email address with
|
||||
*/
|
||||
public string $token;
|
||||
|
||||
|
|
|
@ -11,17 +11,17 @@ use com\fwdekker\deathnotifier\ValidationException;
|
|||
class HasLengthRule extends Rule
|
||||
{
|
||||
/**
|
||||
* @var int|null The minimum length (inclusive), or `null` if there is no minimum length.
|
||||
* @var int|null the minimum length (inclusive), or `null` if there is no minimum length
|
||||
*/
|
||||
private readonly ?int $min_length;
|
||||
/**
|
||||
* @var int|null The maximum length (inclusive), or `null` if there is no maximum length.
|
||||
* @var int|null the maximum length (inclusive), or `null` if there is no maximum length
|
||||
*/
|
||||
private readonly ?int $max_length;
|
||||
|
||||
|
||||
/**
|
||||
* Verifies that the input is of the specific length.
|
||||
* Constructs a new `HasLengthRule`.
|
||||
*
|
||||
* @param int|null $min_length the minimum length (inclusive), or `null` if there is no minimum length
|
||||
* @param int|null $max_length the maximum length (inclusive), or `null` if there is no maximum length
|
||||
|
|
|
@ -17,7 +17,7 @@ class IsEqualToRule extends Rule
|
|||
|
||||
|
||||
/**
|
||||
* Constructs an `IsEqualToRule` that checks for equality with the specified value.
|
||||
* Constructs a new `IsEqualToRule`.
|
||||
*
|
||||
* @param string $expected the value that checked values should be equal to
|
||||
*/
|
||||
|
|
|
@ -12,7 +12,7 @@ use com\fwdekker\deathnotifier\ValidationException;
|
|||
abstract class Rule
|
||||
{
|
||||
/**
|
||||
* @var string|null The message to return if the rule does not apply to some input. If `null`, the rule
|
||||
* @var string|null the message to return if the rule does not apply to some input. If `null`, the rule
|
||||
* implementation can choose an appropriate message.
|
||||
*/
|
||||
public ?string $override_message;
|
||||
|
|
Loading…
Reference in New Issue