User:AnomieBOT/source/tasks/DeletionSortingCleaner.pm
Appearance
Approved 2010-07-06 Wikipedia:Bots/Requests for approval/AnomieBOT 40 |
Supplemental BFRA approved 2020-06-06 Wikipedia:Bots/Requests for approval/AnomieBOT 79 |
package tasks::DeletionSortingCleaner;
=pod
=begin metadata
Bot: AnomieBOT
Task: DeletionSortingCleaner
BRFA: Wikipedia:Bots/Requests for approval/AnomieBOT 40
Status: Approved 2010-07-06
+BRFA: Wikipedia:Bots/Requests for approval/AnomieBOT 79
+Status: Approved 2020-06-06
Created: 2010-06-18
Perform certain tasks for [[WP:WikiProject Deletion sorting]]:
* Subst various AfD templates that should be substed
* Archive discussions for closed XfDs
* Remove duplicate XfD listings
If necessary, the bot may be kept off a deletion sorting subpage by adding
{{[[Template:bots|bots]]|optout=AnomieBOT/DeletionSortingCleaner}} to that page.
=end metadata
=cut
use utf8;
use strict;
use AnomieBOT::Task;
use URI::Escape;
use Data::Dumper;
use POSIX;
use vars qw/@ISA/;
@ISA=qw/AnomieBOT::Task/;
sub new {
my $class=shift;
my $self=$class->SUPER::new();
$self->{'pages'}=undef;
$self->{'lasttime'}=0;
$self->{'broken'}=0;
bless $self, $class;
return $self;
}
=pod
=for info
Approved 2010-07-06<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 40]]
=for info
Supplemental BFRA approved 2020-06-06<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 79]]
=cut
sub approved {
return 3;
}
sub run {
my ($self, $api)=@_;
my $res;
$api->task('DeletionSortingCleaner', 0, 10, qw/d::Talk d::Templates d::Redirects/);
# Get all redirects to templates we need to subst in XfD pages
my %xfdtemplates=$api->redirects_to_resolved('Template:At','Template:Afd top','Template:Afd bottom', 'Template:Afd-privacy');
if(exists($xfdtemplates{''})){
$api->warn("Failed to get list of XfD templates: ".$xfdtemplates{''}{'error'}."\n");
return 60;
}
# Only check twice per day
if($self->{'lasttime'}==0){
if(exists($api->store->{'lasttime'})){
my $t=$api->store->{'lasttime'};
$self->{'lasttime'}=$t if($t=~/^\d+$/ && $t<=time());
}
$self->{'broken'}=$api->store->{'broken'} if(exists($api->store->{'broken'}));
}
my $starttime=time();
my $t=$self->{'lasttime'}+($self->{'broken'}?3600:43200)-$starttime;
return $t if $t>0;
my $screwup=' ([[User:'.$api->user.'/shutoff/DeletionSortingCleaner|errors?]])';
my $broken=0;
# Load list of deletion sorting subpages to process
if ( ! defined( $self->{'pages'} ) ) {
$res=$api->query(titles=>'Wikipedia:WikiProject Deletion sorting/Compact',prop=>'links',plnamespace=>4,pllimit=>'max');
if($res->{'code'} ne 'success'){
$api->warn("Failed to get list of pages to process: ".$res->{'error'}."\n");
return 60;
}
$self->{'pages'}=[ sort grep m!^Wikipedia:WikiProject Deletion sorting/!, map $_->{'title'}, @{(values %{$res->{'query'}{'pages'}})[0]{'links'}} ];
unless(@{$self->{'pages'}}){
$api->warn("No pages in list?");
$self->{'broken'}=1;
$api->store->{'broken'}=1;
return 3600;
}
}
my $endtime=time()+300;
while ( @{$self->{'pages'}} ) {
return 0 if $api->halting;
my $page = shift @{$self->{'pages'}};
# First, load the page to archive from. Allow for opting out using
# {{bots|optout=AnomieBOT/DeletionSortingCleaner}}
my $tok=$api->edittoken($page, OptOut=>$api->user.'/DeletionSortingCleaner');
if($tok->{'code'} eq 'shutoff'){
$api->warn("Task disabled: ".$tok->{'content'}."\n");
return 300;
}
if($tok->{'code'} eq 'botexcluded'){
$api->warn("Bot excluded from $page: ".$tok->{'error'}."\n") unless $tok->{'type'} eq 'optout';
next;
}
if($tok->{'code'} ne 'success'){
$api->warn("Failed to get edit token for $page: ".$tok->{'error'}."\n");
$broken=1;
next;
}
next if exists($tok->{'missing'});
# Go through all templates in the page looking for transclusions of XfD
# pages. For each one, subst any substable templates and then see if it
# looks closed.
my $intxt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'};
my @archive=@{$api->store->{"archive $page"} // []}; # Load saved archivals
my @summary=();
my $fail=undef;
my $dups=0;
my %dups=();
my $outtxt=$api->process_templates($intxt, sub {
return undef if defined($fail);
my $name=shift;
return undef unless $name=~m!^(?i:Wikipedia|WP) *: *((?:[Aa]rticles|[Mm]iscellany) for deletion)/(.+)$!;
my $name2=$2;
# Normalize. People do weird things sometimes.
$name = "Wikipedia:\u$1/$2";
if(exists($dups{$name})){
$dups=1;
return '';
}
$dups{$name}=1;
my $cannoteditreason=undef;
REDO:
my $xfdtok=$api->edittoken($name);
if($xfdtok->{'code'} eq 'shutoff'){
$api->warn("Task disabled: ".$xfdtok->{'content'}."\n");
$fail=300;
return undef;
}
if($xfdtok->{'code'} eq 'pageprotected'){
$cannoteditreason=$xfdtok->{'error'};
my $res=$api->query(
prop => 'revisions',
titles => $name,
rvprop => 'ids|timestamp|content|flags|user|size|comment',
rvslots => 'main',
);
$xfdtok=(values %{$res->{'query'}{'pages'}})[0];
$xfdtok->{'code'}=$res->{'code'};
$xfdtok->{'error'}=$res->{'error'};
}
if($xfdtok->{'code'} ne 'success'){
# Don't worry about this error, just assume the discussion is
# not closed and retry next time around.
$api->warn("Failed to get edit token for $name: ".$xfdtok->{'error'}."\n");
$broken=1;
return undef;
}
if(exists($xfdtok->{'missing'})){
# WTF?
$api->warn("XfD page $name linked from $page does not exist\n");
return undef;
}
my %substlist=();
my $xfdintxt=$xfdtok->{'revisions'}[0]{'slots'}{'main'}{'*'};
my $xfdouttxt=$api->process_templates($xfdintxt, sub {
my $name=shift;
shift; # $params
my $wikitext=shift;
return undef unless exists($xfdtemplates{"Template:$name"});
$substlist{$name}=1;
$wikitext=~s/^\{\{/{{subst:/; # }}
return $wikitext;
});
if($xfdintxt ne $xfdouttxt){
# We found templates to subst, so save the changed page and
# then reload it.
if(defined($cannoteditreason)){
$api->log("Editprotected to subst templates in $name");
my $talk=$name; $talk=~s/:/ talk:/;
my @t=map "{{[[Template:$_|$_]]}}", keys %substlist;
$t[$#t]='and '.$t[$#t] if @t>1;
my $t=join((@t>2)?', ':' ', @t);
my $s=(@t==1)?'':'s';
my $res=$api->whine("Editprotected request: please subst $t", "{{editprotected}} The template$s $t in [[$name|this page]] should be substed, but I cannot do so because \l$cannoteditreason. Please do so. Thanks.", Summary=>"[BOT] Editprotected request: please subst $t", Pagename=>$talk);
if($res->{'code'} ne 'success'){
# Don't worry about this error, just assume the
# discussion is not closed and retry next time around.
$api->warn("Editprotected request failed for $name: ".$res->{'error'}."\n");
$broken=1;
return undef;
}
} else {
$api->log("Substing templates in $name");
my $res=$api->edit($xfdtok, $xfdouttxt, "[[Wikipedia:Template substitution|substituting]] closure templates".$screwup, 1, 1);
if($res->{'code'} ne 'success'){
# Don't worry about this error, just assume the
# discussion is not closed and retry next time around.
$api->warn("Save failed for $name: ".$res->{'error'}."\n");
$broken=1;
return undef;
}
goto REDO;
}
}
# If closed, remove from the main page and note it for archival.
# If still open, do nothing.
return undef unless $xfdintxt=~m!<div class="[^"]*(?<=[" ])[xt]fd-closed[ "]!;
my ($result,$date)=('(unknown)','(unknown)');
$result=$1 if $xfdintxt=~m!(?:result was|result of the discussion was:(?:'')?)\s*(?:'''|<(?:b|strong)>)(.+?)(?:'''|</(?:b|strong)>)!;
$date=$1 if $xfdintxt=~m!(\d\d:\d\d, \d+ \w+ \d{4} \(UTC\))!;
unshift @archive, "* [[$name|$name2]] - (".length($xfdintxt).") - $result - <small>closed $date</small>";
push @summary, "[[$name]]";
return '';
});
return $fail if defined($fail);
# Calculate the changes needed to the archive page, if any. We do this
# before saving the original page to minimize chances of being able to
# save one but not the other.
my ($apage,$atok,$atxt)=(undef,undef,undef);
if(@archive){
my ($i, $sz) = (1, 0);
($apage = "$page/archive") =~ s/^Wikipedia://;
$res = $api->query(
generator => 'allpages',
gapnamespace => 4,
gapprefix => $apage,
gaplimit => 'max',
prop => 'info',
);
for my $p (values %{$res->{'query'}{'pages'}}) {
if ( $p->{'title'} eq "$page/archive" && $i <= 1) {
($i,$sz) = (1,$p->{'length'});
} elsif ( $p->{'title'} =~ /^\Q$page\/archive\E (\d+)$/ && $i <= $1 ) {
($i,$sz) = ($1,$p->{'length'});
}
}
$i++ if $sz > 1048576;
$apage = $i < 2 ? "$page/archive" : "$page/archive $i";
$atok=$api->edittoken($apage);
if($atok->{'code'} eq 'shutoff'){
$api->warn("Task disabled: ".$atok->{'content'}."\n");
return 300;
}
if($atok->{'code'} ne 'success'){
$api->warn("Failed to get edit token for $apage: ".$atok->{'error'}."\n");
$broken=1;
next;
}
my $n=$page; $n=~s!^[^/]*/!!;
my $aintxt=$atok->{'revisions'}[0]{'slots'}{'main'}{'*'} // '';
$atxt=$aintxt;
if(exists($atok->{'missing'})){
# Doesn't exist, create boilerplate
$atxt="<noinclude>{{deletionlistarchive|$n}}</noinclude>\n\n==$n==\n\n===Articles for Deletion===\n<!-- add old AfD discussions at the top -->\n\n<!-- end of old AfD discussions -->";
}
my $a=join("\n",@archive);
$atxt=~s/<!-- add old AfD discussions at the top -->/<!-- add old AfD discussions at the top -->\n$a/;
if($aintxt eq $atxt){
$api->whine("Broken deletion sorting archive page for $n", "The deletion sorting archive page [[$apage]] is lacking the marker <code><nowiki><!-- add old AfD discussions at the top --></nowiki></code>, which is needed for me to know where to put the archived AfDs. I can't do anything to that page until someone fixes it.");
$broken=1;
next;
}
}
# Now do the saving
if($outtxt ne $intxt){
$api->log("Archiving closed XfDs and/or removing duplicates from $page...");
my $summary;
if(@summary){
$summary="[[$apage|Archiving closed XfDs]]" . ( $dups ? ' and removing duplicate XfDs' : '' ) . $screwup . ": " . join(" ", @summary);
$summary="[[$apage|Archiving closed XfDs]]" . ( $dups ? ' and removing duplicate XfDs' : '' ) . $screwup . ": [" . scalar(@summary) . " discussions]" if length($summary)>500;
} else {
$summary = "Removing duplicate XfDs" . $screwup;
}
$res=$api->edit($tok, $outtxt, $summary, 0, 1);
if($res->{'code'} ne 'success'){
$api->warn("Save failed for $page: ".$res->{'error'}."\n");
$broken=1;
next;
}
# Now that we saved the original page, we must save the archival
# records just in case the next edit fails.
$api->store->{"archive $page"}=[@archive];
}
if(defined($atok)){
$api->log("Archiving closed XfDs to $apage...");
$res=$api->edit($atok, $atxt, "Archiving closed XfDs from [[$page]]".$screwup, 0, 1);
if($res->{'code'} ne 'success'){
$api->warn("Save failed for $apage".$res->{'error'}."\n");
$broken=1;
next;
}
# Now that we saved the archival page, clear the saved value.
delete $api->store->{"archive $page"};
}
return 0 if time()>$endtime;
}
# Save checked revision
$self->{'pages'}=undef;
$self->{'lasttime'}=$starttime;
$self->{'broken'}=$broken;
$api->store->{'lasttime'}=$starttime;
$api->store->{'broken'}=$broken;
return $starttime+($broken?3600:43200)-time();
}
1;