Detect and resolve renaming vandalism
This commit is contained in:
parent
3626175bac
commit
93c0a6f7e9
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "fwdekker/death-notifier",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"version": "0.19.3", "_comment_version": "Also update version in `package.json`!",
|
||||
"version": "0.19.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.19.3", "_comment_version": "Also update version in `composer.json`!",
|
||||
"version": "0.19.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",
|
||||
|
|
|
@ -14,7 +14,7 @@ filename = .death-notifier.db
|
|||
# File to store logs in.
|
||||
file = .death-notifier.log
|
||||
# Log level. See https://seldaek.github.io/monolog/doc/01-usage.html#log-levels
|
||||
level = 200
|
||||
level = 250
|
||||
|
||||
[mail]
|
||||
# Host name of SMTP server to send mail through.
|
||||
|
|
|
@ -248,20 +248,6 @@
|
|||
</article>
|
||||
|
||||
<div id="settings-part" class="hidden">
|
||||
<article>
|
||||
<header>
|
||||
<h2>Account settings</h2>
|
||||
</header>
|
||||
<form id="logout-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="logout-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<button id="logoutButton">Log out</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
|
@ -351,11 +337,25 @@
|
|||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h3>Delete account</h3>
|
||||
<h4>If you no longer want to use Death Notifier, you can permanently delete your
|
||||
account.</h4>
|
||||
<h3>Account management</h3>
|
||||
<h4>Log out or delete your account.</h4>
|
||||
</hgroup>
|
||||
</header>
|
||||
<form id="logout-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="logout-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<button id="logoutButton">Log out</button>
|
||||
</form>
|
||||
|
||||
<footer>
|
||||
<h4>Delete account</h4>
|
||||
<p>
|
||||
If you no longer want to use Death Notifier, you can permanently delete your account.
|
||||
This choice is permanent and cannot be reverted.
|
||||
</p>
|
||||
<form id="delete-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="delete-form">
|
||||
<output></output>
|
||||
|
@ -365,6 +365,7 @@
|
|||
<input id="delete-email" type="hidden" name="email" />
|
||||
<button id="delete-button" class="outline">Delete account</button>
|
||||
</form>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -67,14 +67,14 @@ function refreshTrackings(): void {
|
|||
const nameLink = document.createElement("a");
|
||||
nameLink.href = "https://en.wikipedia.org/wiki/" + tracking.name;
|
||||
nameLink.innerText = tracking.name;
|
||||
if (tracking.deleted)
|
||||
if (tracking.is_deleted)
|
||||
nameLink.classList.add("red-link");
|
||||
nameCell.append(nameLink);
|
||||
row.append(nameCell);
|
||||
|
||||
const statusCell = document.createElement("td");
|
||||
let statusText;
|
||||
if (tracking.deleted) {
|
||||
if (tracking.is_deleted) {
|
||||
statusText = "article not found";
|
||||
} else {
|
||||
switch (window.btoa(tracking.name)) {
|
||||
|
|
|
@ -48,7 +48,7 @@ class EmulateCronAction extends Action
|
|||
$logger = LoggerUtil::with_name($this::class);
|
||||
// @phpstan-ignore-next-line
|
||||
while (true) {
|
||||
$logger->info("Emulating cron job.");
|
||||
$logger->notice("Emulating cron job.");
|
||||
foreach ($this->actions as $action)
|
||||
$action->handle($inputs);
|
||||
print("Done.\n");
|
||||
|
|
|
@ -104,7 +104,6 @@ class UpdateTrackingsAction extends Action
|
|||
|
||||
// Send mails, log events
|
||||
// TODO: Restrict number of notifications to 1 per hour (excluding "oops we're not sure" message)
|
||||
// TODO: Detect renaming vandalism
|
||||
$logger = LoggerUtil::with_name($this::class);
|
||||
$emails = [];
|
||||
|
||||
|
@ -112,14 +111,14 @@ class UpdateTrackingsAction extends Action
|
|||
$trackers = $this->tracking_list->list_trackers($person_names);
|
||||
|
||||
foreach ($new_deletions as $new_deletion) {
|
||||
$logger->warning("Deleted article $new_deletion.");
|
||||
$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) {
|
||||
$logger->warning("Undeleted article $new_undeletion.");
|
||||
$logger->notice("Undeleted article $new_undeletion.");
|
||||
|
||||
foreach ($trackers[$new_undeletion] as $user_email)
|
||||
$emails[] = new NotifyArticleUndeletedEmail($user_email, $new_undeletion);
|
||||
|
@ -127,7 +126,7 @@ class UpdateTrackingsAction extends Action
|
|||
|
||||
foreach ($new_status_changes as $person_name => $person_status) {
|
||||
if ($person_status === PersonStatus::Alive)
|
||||
$logger->warning("Person $person_name is now alive again.");
|
||||
$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);
|
||||
|
|
|
@ -77,7 +77,7 @@ class VerifyEmailAction extends Action
|
|||
"This email verification link has expired. Log in and request a new verification email."
|
||||
);
|
||||
|
||||
$this->user_list->set_email_verified($_SESSION["uuid"]);
|
||||
$this->user_list->set_email_verified($user_data["uuid"]);
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
@ -32,6 +32,10 @@ class Wikipedia
|
|||
* sufficient.
|
||||
*/
|
||||
private const CATS_PER_QUERY = 500;
|
||||
/**
|
||||
* Number of article moves to follow of deleted articles before giving up.
|
||||
*/
|
||||
private const MAX_MOVE_DEPTH = 5;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -104,9 +108,8 @@ class Wikipedia
|
|||
* @param string|null $continue_name the name of the continue parameter to follow for this request
|
||||
* @return QueryOutput<mixed> the API's responses merged into a single `QueryOutput`
|
||||
* @throws WikipediaException 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
|
||||
private function api_query_batched(array $params, array $titles, ?string $continue_name = null): QueryOutput
|
||||
{
|
||||
$articles = [];
|
||||
$redirects = array_combine($titles, $titles);
|
||||
|
@ -138,6 +141,85 @@ class Wikipedia
|
|||
return new QueryOutput($articles, $redirects, $missing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out the page that a deleted page was moved to, if any.
|
||||
*
|
||||
* @param string $title the title to figure out the new title of after moving
|
||||
* @param int $max_depth the maximum number of recursive steps to take, in case the article that {@see $title} was
|
||||
* moved to was also deleted, and the article that was moved to was also deleted, etc.
|
||||
* @return string|null the new title of the article, or `null` if the article has actually been deleted
|
||||
* @throws WikipediaException if the query fails
|
||||
*/
|
||||
private function api_query_title_after_move(string $title, int $max_depth = Wikipedia::MAX_MOVE_DEPTH): ?string
|
||||
{
|
||||
if ($max_depth <= 0)
|
||||
return null;
|
||||
|
||||
$log_events = $this->api_fetch([
|
||||
"action" => "query",
|
||||
"format" => "json",
|
||||
"list" => "logevents",
|
||||
"letype" => "move",
|
||||
"letitle" => $title
|
||||
])["query"]["logevents"];
|
||||
if (empty($log_events))
|
||||
return null;
|
||||
|
||||
$title_after_move = $log_events[0]["params"]["target_title"];
|
||||
$after_move_page_info = $this->api_fetch([
|
||||
"action" => "query",
|
||||
"format" => "json",
|
||||
"prop" => "info",
|
||||
"titles" => $title_after_move,
|
||||
"redirects" => true,
|
||||
]);
|
||||
|
||||
if (!empty($after_move_page_info->missing))
|
||||
return $this->api_query_title_after_move($title, $max_depth - 1);
|
||||
else if (!empty($after_move_page_info->redirects))
|
||||
return array_values($after_move_page_info->redirects)[0];
|
||||
else
|
||||
return $title_after_move;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a query request to the Wikipedia API in batches of {@see Wikipedia::ARTICLES_PER_QUERY} titles at a time,
|
||||
* and resolves articles that were deleted because of a move.
|
||||
*
|
||||
* @param array<string, mixed> $params the parameters to include in each query
|
||||
* @param string[] $titles the titles of the pages to query
|
||||
* @param string|null $continue_name the name of the continue parameter to follow for this request
|
||||
* @return QueryOutput<mixed> the API's responses merged into a single `QueryOutput`
|
||||
* @throws WikipediaException if the query fails
|
||||
*/
|
||||
private function api_query_batched_resolve_moves(array $params, array $titles,
|
||||
?string $continue_name = null): QueryOutput
|
||||
{
|
||||
$output_base = $this->api_query_batched($params, $titles, $continue_name);
|
||||
|
||||
$not_moved = [];
|
||||
$moves = [];
|
||||
foreach ($output_base->missing as $missing_title) {
|
||||
$title_after_move = $this->api_query_title_after_move($missing_title);
|
||||
if ($title_after_move === null)
|
||||
$not_moved[] = $title_after_move;
|
||||
else
|
||||
$moves[$missing_title] = $title_after_move;
|
||||
}
|
||||
|
||||
$output_of_moves = $this->api_query_batched($params, $moves, $continue_name);
|
||||
if (array_keys($output_of_moves->redirects) !== array_values($output_of_moves->redirects))
|
||||
throw new WikipediaException("Article was moved unexpectedly: " . json_encode($output_of_moves->redirects));
|
||||
if (!empty($output_of_moves->missing))
|
||||
throw new WikipediaException("Article missing unexpectedly: " . json_encode($output_of_moves->missing));
|
||||
|
||||
return new QueryOutput(
|
||||
array_replace($output_base->results, $output_of_moves->results),
|
||||
array_replace($output_base->redirects, $moves),
|
||||
$not_moved
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the current {@see ArticleType} of the article.
|
||||
|
@ -187,6 +269,7 @@ class Wikipedia
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks for all {@see $names} what their current {@see ArticleType} and {@see PersonStatus} is according to
|
||||
* Wikipedia.
|
||||
|
@ -199,7 +282,7 @@ class Wikipedia
|
|||
*/
|
||||
public function query_person_info(array $names): QueryOutput
|
||||
{
|
||||
$output = $this->api_query_batched(
|
||||
$output = $this->api_query_batched_resolve_moves(
|
||||
params: ["prop" => "categories", "cllimit" => strval(self::CATS_PER_QUERY)],
|
||||
titles: $names,
|
||||
continue_name: "clcontinue"
|
||||
|
|
Loading…
Reference in New Issue