Detect and resolve renaming vandalism

This commit is contained in:
Florine W. Dekker 2022-12-12 23:29:44 +01:00
parent 3626175bac
commit 93c0a6f7e9
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
11 changed files with 114 additions and 31 deletions

View File

@ -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",

BIN
composer.lock generated

Binary file not shown.

BIN
package-lock.json generated

Binary file not shown.

View File

@ -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",

View File

@ -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.

View File

@ -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>

View File

@ -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)) {

View File

@ -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");

View File

@ -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);

View File

@ -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;

View File

@ -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"