User:AnomieBOT/source/tasks/DeletionSortingCleaner.pm: Difference between revisions

Content deleted Content added
AnomieBOT (talk | contribs)
Updating published sources: d::Templates: * When getting the "sanitized" template name, strip bidi and whitespace as MediaWiki does. d::Talk: * Add to the standard note a note that if a reply is needed, go to the bot's talk page. DeletionSortingClean
AnomieBOT (talk | contribs)
Updating published sources: DeletionSortingCleaner: * Supplemental BRFA approved!
 
(29 intermediate revisions by the same user not shown)
Line 1:
{{ombox|type=notice|text= TrialApproved approved (7 days)2010-07-06<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 40]]}}
{{ombox|type=notice|text= Supplemental BFRA approved 2020-06-06<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 79]]}}
<source lang="perl">
{{ombox|type=notice|text= Supplemental BFRA approved 2025-08-22<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 85]]}}
<syntaxhighlight lang="perl">
package tasks::DeletionSortingCleaner;
 
Line 10 ⟶ 12:
Task: DeletionSortingCleaner
BRFA: Wikipedia:Bots/Requests for approval/AnomieBOT 40
Status: InApproved trial (7 days)2010-07-06
+BRFA: Wikipedia:Bots/Requests for approval/AnomieBOT 79
Rate: Max 6 edits/minute
+Status: Approved 2020-06-06
+BRFA: Wikipedia:Bots/Requests for approval/AnomieBOT 85
+Status: Approved 2025-08-22
Created: 2010-06-18
 
Perform certain tasks for [[WP:WikiProject Deletion sorting]]:
* Subst various AfD templates that should be substed
* Archive discussions for closed AfDsXfDs
* Remove duplicate XfD listings
* Remove deleted XfD listings
 
If necessary, the bot may be kept off a deletion sorting subpage by adding
{{tlx[[Template:bots|bots]]|2=optout=AnomieBOT/DeletionSortingCleaner}} to that page.
 
=end metadata
Line 38 ⟶ 45:
my $class=shift;
my $self=$class->SUPER::new();
$self->{'pages'}=undef;
$self->{'lasttime'}=0;
$self->{'broken'}=0;
Line 47 ⟶ 55:
 
=for info
TrialApproved approved (7 days)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]]
 
=for info
Supplemental BFRA approved 2025-08-22<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 85]]
 
=cut
 
sub approved {
return 23;
}
 
Line 59 ⟶ 73:
my $res;
 
$api->task('DeletionSortingCleaner', 0, 10, qw/d::Talk d::Templates d::Redirects d::Trial/);
$res=$api->check_trial(1277292789+7*86400, 'AnomieBOT 40');
return $$res if $res;
 
# Get all redirects to templates we need to subst in XfD pages
my %xfdtemplates=$api->redirects_toredirects_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");
Line 82 ⟶ 94:
return $t if $t>0;
 
my $screwup=' Errors? ([[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');
$res=$api->query(titles=>'Wikipedia:WikiProject Deletion sorting/Computer-readable.json',prop=>'revisions',rvprop=>"content",rvslots=>"main",rvlimit=>1,formatversion=>2);
if($res->{'code'} ne 'success'){
if($res->{'code'} ne 'success'){
$api->warn("Failed to get list of pages to process: ".$res->{'error'}."\n");
return 60;
}
my $json;
my @pages=sort grep m!^Wikipedia:WikiProject Deletion sorting/!, map $_->{'title'}, @{(values %{$res->{'query'}{'pages'}})[0]{'links'}};
eval { $json = JSON->new->decode( $res->{'query'}{'pages'}[0]{'revisions'}[0]{'slots'}{'main'}{'content'} // '' ); };
unless(@pages){
$api->warnif ("No pages$@ in list?"); {
$api->warn("Failed to parse Wikipedia:WikiProject Deletion sorting/Computer-readable.json: $@\n");
$self->{'broken'}=1;
$api->store->{'broken'}=1 return 300;
return 3600;}
my %pages = ();
for my $g (values %$json) {
for my $p (@$g) {
$pages{"Wikipedia:WikiProject Deletion sorting/$p"} = 1;
}
}
$self->{'pages'}=[ sort keys %pages ];
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'}};
 
for my $page (@pages){
# First, load the page to archive from. Allow for opting out using
# {{bots|optout=AnomieBOT/DeletionSortingCleaner}}
Line 121 ⟶ 151:
# pages. For each one, subst any substable templates and then see if it
# looks closed.
my $intxt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'};
$api->log("Processing $page...");
my $intxt=$tok->{'revisions'}[0]{'*'};
my @archive=@{$api->store->{"archive $page"} // []}; # Load saved archivals
my @summary=();
my $fail=undef;
my $dups=0;
my %dups=();
my $deleted=0;
my $outtxt=$api->process_templates($intxt, sub {
return undef if defined($fail);
my $name=shift;
shift; # $params
my $wikitext=shift;
 
return undef unless $name=~m!^(?i:Wikipedia|WP) *: *((?:Articles[Aa]rticles|[Mm]iscellany) for deletion)/(.+)$!;
my $name2=$12;
 
# Normalize. People do weird things sometimes.
$name = "Wikipedia:\u$1/$2";
 
if(exists($dups{$name})){
$dups=1;
return '';
}
$dups{$name}=1;
 
my $cannoteditreason=undef;
Line 146 ⟶ 190:
titles => $name,
rvprop => 'ids|timestamp|content|flags|user|size|comment',
rvslots => 'main',
);
$xfdtok=(values %{$res->{'query'}{'pages'}})[0];
Line 156 ⟶ 201:
$api->warn("Failed to get edit token for $name: ".$xfdtok->{'error'}."\n");
$broken=1;
return undef;
}
if(exists($xfdtok->{'missing'})){
# Check for a deletion log entry.
my $res2 = $api->query(
letitle => $name,
list => 'logevents',
letype => 'delete',
lelimit => 1,
leprop => 'user|timestamp|comment',
);
if($res2->{'code'} ne 'success'){
$api->warn("Failed to retrieve logs for $name: " . $res2->{'error'} . "\n");
$fail = 60;
return undef;
}
if(exists($res2->{'query'}{'logevents'}[0])){
my $log = $res2->{'query'}{'logevents'}[0];
 
# Check whether the deletion log entry could reasonably
# belong to this listing.
my %q = (
titles => $page,
prop => 'revisions',
rvprop => 'ids|timestamp|content',
rvslots => 'main',
rvlimit => 10,
formatversion => 2,
);
my %cont = ();
my $wt = $wikitext;
do {
my $res3 = $api->query( %q );
if($res3->{'code'} ne 'success'){
$api->warn("Failed to retrieve revisions for $page: " . $res3->{'error'} . "\n");
$fail = 60;
return undef;
}
for my $rev (@{$res3->{'query'}{'pages'}[0]{'revisions'}}) {
next unless exists( $rev->{'slots'}{'main'}{'content'} ); # Revdel?
my $c = $rev->{'slots'}{'main'}{'content'};
my $found = ( $c =~ /\Q$wt\E/ );
if ( ! $found ) {
$api->process_templates($c, sub {
return undef if $found;
my $name2=shift;
shift; # $params
my $wikitext2=shift;
 
return undef unless $name2=~m!^(?i:Wikipedia|WP) *: *((?:[Aa]rticles|[Mm]iscellany) for deletion)/(.+)$!;
 
# Normalize. People do weird things sometimes.
$name2 = "Wikipedia:\u$1/$2";
 
if ( $name2 eq $name ) {
$wt = $wikitext2;
$found = 1;
}
return undef;
});
}
if ( ! $found ) {
$api->warn("XfD page $name linked from $page does not exist, deleted at $log->{'timestamp'} but newer rev $rev->{'revid'} from $rev->{'timestamp'} did not transclude it.\n");
return undef;
}
if ( $rev->{'timestamp'} le $log->{'timestamp'} ) {
$res3->{'continue'} = {};
last;
}
}
%cont = %{ $res3->{'continue'} // {} };
} while ( %cont );
 
# Deleted since it was listed, so remove from page.
$deleted=1;
push @summary, "[[$name]] (was deleted)";
return '';
}
 
$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;
Line 168 ⟶ 293:
return undef unless exists($xfdtemplates{"Template:$name"});
$substlist{$name}=1;
$wikitext=~s/^\{\{/{{subst:/; # }} }}
return $wikitext;
});
Line 205 ⟶ 330:
# If closed, remove from the main page and note it for archival.
# If still open, do nothing.
return undef unless $xfdintxt=~m!<div class="boilerplate[^"]*(?<=[" metadata])[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 '';
});
Line 217 ⟶ 343:
# 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 ($atok=i, $api->edittokensz) = ("$page/archive"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");
Line 225 ⟶ 370:
}
if($atok->{'code'} ne 'success'){
$api->warn("Failed to get edit token for $page/archiveapage: ".$atok->{'error'}."\n");
$broken=1;
next;
}
my $n=$page; $n=~s!^[^/]*/!!;
my $aintxt=$atok->{'revisions'}[0]{'slots'}{'main'}{'*'} // '';
$atxt=$aintxt;
if(exists($atok->{'missing'})){
Line 239 ⟶ 384:
$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 [[$page/archiveapage]] 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;
Line 247 ⟶ 392:
# Now do the saving
if($outtxt ne $intxt){
$res=$api->editlog($tok, $outtxt, "Archiving closed XfDs toand/or removing duplicates from [[$page/archive]]...".$screwup, 0, 1);
my $what = '';
$what .= "[[$apage|Archiving closed XfDs]]" if @archive;
$what .= ( @archive ? ' and removing' : 'Removing' ) . ( $deleted ? ' deleted' : '' ) . ( $dups ? ( $deleted ? ' and duplicate' : ' duplicate' ) : '' ) . ' XfDs' if $deleted || $dups;
 
my $summary;
$summary=$what . $screwup . ": " . join(" ", @summary);
$summary=$what . $screwup . ": [" . scalar(@summary) . " discussions]" if length($summary)>500;
$res=$api->edit($tok, $outtxt, $summary, 0, 1);
if($res->{'code'} ne 'success'){
$api->warn("Save failed for $page: ".$res->{'error'}."\n");
Line 258 ⟶ 411:
}
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 $page/archive: apage".$res->{'error'}."\n");
$broken=1;
next;
Line 267 ⟶ 421:
delete $api->store->{"archive $page"};
}
 
return 0 if time()>$endtime;
}
 
# Save checked revision
$self->{'pages'}=undef;
$self->{'lasttime'}=$starttime;
$self->{'broken'}=$broken;
Line 279 ⟶ 436:
1;
 
</syntaxhighlight>
</source>