User:AnomieBOT/source/tasks/PUICloser.pm
Appearance
Approved 2008-12-30 Wikipedia:Bots/Requests for approval/AnomieBOT 18 |
First supplemental BFRA approved 2012-08-29 Wikipedia:Bots/Requests for approval/AnomieBOT 67 |
Second supplemental BFRA approved 2013-05-07 Wikipedia:Bots/Requests for approval/AnomieBOT 68 |
package tasks::PUICloser;
=pod
=begin metadata
Bot: AnomieBOT
Task: PUICloser
BRFA: Wikipedia:Bots/Requests for approval/AnomieBOT 18
Status: Inactive 2016-05-03
+BRFA: Wikipedia:Bots/Requests for approval/AnomieBOT 67
+Status: Approved 2012-08-29
+BRFA: Wikipedia:Bots/Requests for approval/AnomieBOT 68
+Status: Approved 2013-05-07
Created: 2008-12-27
Peform the following tasks at [[WP:PUF]]:
* Create the daily PUF subpage.
* Fix the headers on the daily PUF subpages, if they get removed or damaged.
* Close discussions where the file has been deleted.
* Close discussions where the file does not exist.
* Close discussions where the file is on Commons.
* Subst {{tl|puf top}} and {{tl|puf bottom}}, when editing the page anyway.
* Maintain the list at [[WP:PUF#Holding cell]].
=end metadata
=cut
use utf8;
use strict;
use AnomieBOT::Task qw/:time onlylist/;
use URI::Escape;
use Data::Dumper;
use vars qw/@ISA/;
@ISA=qw/AnomieBOT::Task/;
my @ok_nonfree_templates=(
'Template:PD-UK',
);
my $addNewDay = 0;
my @months=('','January','February','March','April','May','June','July','August','September','October','November','December');
my $is_closed_re=qr((?:\{\{\s*(?i:Template\s*:\s*)?[pP]u[if][_ ]top\s*[|}]|<div class="[^"]*(?<=[" ])xfd-closed[ "]));
my %db=(
nonsense => 'G1',
test => 'G2',
vandalism => 'G3',
pagemove => 'G3',
hoax => 'G3',
repost => 'G4',
banned => 'G5',
histmerge => 'G6',
move => 'G6',
copypaste => 'G6',
xfd => 'G6',
maintenance => 'G6',
house => 'G6',
disambig => 'G6',
movedab => 'G6',
unpatrolled => 'G6',
author => 'G7',
self => 'G7',
blanked => 'G7',
talk => 'G8',
subpage => 'G8',
imagepage => 'G8',
redirnone => 'G8',
templatecat => 'G8',
attack => 'G10',
blp => 'G10',
attackorg => 'G10',
spam => 'G11',
promo => 'G11',
copyvio => 'G12',
redundantfile => 'F1',
redundantimage => 'F1',
nofile => 'F2',
noimage => 'F2',
noncom => 'F3',
unksource => 'F4',
unfree => 'F5',
norat => 'F6',
badfairuse => 'F7',
nowcommons => 'F8',
imgcopyvio => 'F9',
filecopyvio => 'F9',
badfiletype => 'F10',
nopermission => 'F11',
);
sub new {
my $class=shift;
my $self=$class->SUPER::new();
$self->{'lasttime'}=0;
$self->{'ok-nonfree'}=undef;
bless $self, $class;
return $self;
}
=pod
=for info
Approved 2008-12-30<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 18]]
=for info
First supplemental BFRA approved 2012-08-29<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 67]]
=for info
Second supplemental BFRA approved 2013-05-07<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 68]]
=cut
sub approved {
return -2;
}
sub run {
my ($self, $api)=@_;
my $res;
$api->task('PUICloser', 0, 10, qw/d::Redirects d::Timestamp d::Talk d::Sections/);
my %p=$api->redirects_to_resolved('Template:Delrevxfd');
if(exists($p{''})){
if($p{''}{'code'} eq 'shutoff'){
$api->warn("Task disabled: ".$p{''}{'content'}."\n");
return 300;
}
$api->warn("Failed to get notice template redirects: ".$p{''}{'error'}."\n");
return 60;
}
my @p=map { s/^Template://; my ($a,$b)=split(//,$_,2); "(?i:\Q$a\E)\Q$b\E"; } keys %p;
my $p=join('|',@p);
my $noticere=qr/(?:(?i:<noinclude>\s*)?\{\{\s*(?i:Template\s*:\s*)?(?:$p)\s*(?:\|.*?)?\}\}\s*(?i:<\/noinclude>\s*)?)/;
my %tosubst=$api->redirects_to_resolved('Template:Puf top', 'Template:Puf bottom');
if(exists($tosubst{''})){
if($tosubst{''}{'code'} eq 'shutoff'){
$api->warn("Task disabled: ".$tosubst{''}{'content'}."\n");
return 300;
}
$api->warn("Failed to get top/bottom template redirects: ".$tosubst{''}{'error'}."\n");
return 60;
}
my %x=$api->redirects_to_resolved(@ok_nonfree_templates);
if(exists($x{''})){
$api->warn("Failed to get ok-nonfree redirects: ".$x{''}{'error'}."\n");
return 60;
}
$self->{'ok-nonfree'}=[keys %x];
# Only check once per hour, except make sure we run as soon after
# 23:00 and 00:00 as possible.
if($self->{'lasttime'}==0 && exists($api->store->{'lasttime'})){
my $t=$api->store->{'lasttime'};
$self->{'lasttime'}=$t if($t=~/^\d+$/ && $t<=time());
}
my $starttime=time();
my $t=$self->{'lasttime'}+3600;
if(($self->{'lasttime'}%86400)<82800 && ($t%86400)>=82500){
# Past or close enough to 23:00, set next run time to 23:00
$t=$t+82800-($t%86400);
} elsif(($t%86400)<($self->{'lasttime'}%86400)){
# Past 00:00, set to 00:00
$t=$t-($t%86400);
} elsif(($t%86400)>=86100){
# Close enough to 00:00, set to 00:00
$t=$t+86400-($t%86400);
}
$t-=time();
return $t if $t>0;
my $screwup=' Errors? [[User:'.$api->user.'/shutoff/PUICloser]]';
# Get the content of all versions of "puf top" since the last run
my $lastrun = strftime( '%F', gmtime( $self->{'lasttime'} > 1239573236 ? $self->{'lasttime'} : 1239573236 ) );
my $re='\{\{\s*[pP]u[if][ _]?top\s*(?s:\|.*?)?\}\}';
my %cont=();
my $first=1;
while($first || %cont) {
my $t=$api->query(
titles => 'Template:puf top',
prop => 'revisions',
rvprop => 'timestamp|content',
rvslots => 'main',
rvlimit => 1,
%cont,
);
if($t->{'code'} ne 'success'){
$api->warn("Failed to load revisions for Template:puf top: ".$t->{'error'}."\n");
return 60;
}
%cont=exists($t->{'query-continue'})?%{$t->{'query-continue'}{'revisions'}}:();
$t=(values(%{$t->{'query'}{'pages'}}))[0]{'revisions'}[0];
%cont=() if $t->{'timestamp'} lt $lastrun;
$t=$t->{'slots'}{'main'}{'*'};
$t=~s!<noinclude>.*</noinclude>!!gs;
$t=~s!</?includeonly>!!g;
$t=~s!\{\{(?:safe)?subst:#if:\{\{\{1\|\}\}\}\|The result of the discussion was: \{\{\{1\|\}\}\}\}\}!\x07!g;
unless($t =~ m/^\s*$is_closed_re/o){
next unless $first;
$api->whine("[[Template:Puf top]] is broken", "Help! The template {{tl|puf top}} is missing the \"is_closed\" regex, or this regex is not at the beginning of the template's output. To avoid confusion, I'm not going to process any PUFs until it's fixed or I'm fixed.");
return 300;
}
if($t =~ m/\x07\s*$/){
next unless $first;
$api->whine("[[Template:Puf top]] is broken", "Help! The template {{tl|puf top}} does not end with some constant text, i.e. <nowiki>{{{1|}}}</nowiki> is at the very end of the template. To avoid confusion, I'm not going to process any PUFs until it's fixed or I'm fixed.");
return 300;
}
if($t =~ m/\{\{\{/){
next unless $first;
$api->whine("[[Template:Puf top]] is broken", "Help! The template {{tl|puf top}} contains unknown parameters. To avoid confusion, I'm not going to process any PUFs until it's fixed or I'm fixed.");
return 300;
}
$t=quotemeta($t);
$t=~s/\\\x07/(?s:.*?)/g;
$re.="|$t";
$first=0;
}
# Get the list of possible pages.
# First, get the links from the holding cell
$res=$api->query(
action => 'parse',
page => 'Wikipedia:Possibly unfree files',
section => 1,
prop => 'sections|links',
);
if($res->{'code'} eq 'shutoff'){
$api->warn("Task disabled: ".$res->{'content'}."\n");
return 300;
}
if($res->{'code'} ne 'success'){
$api->warn("Failed to get PUF page list: ".$res->{'error'}."\n");
return 60;
}
if(($res->{'parse'}{'sections'}[0]{'line'}//'') ne 'Holding cell'){
$api->warn("Failed to find holding cell in PUF\n");
$api->whine("Cannot find holding cell in [[WP:PUF]]", "Help! Section 1 in [[WP:PUF]] does not seem to be the holding cell. Please fix the page, or fix me.");
return 60;
}
my @pages=map $_->{'*'}, @{$res->{'parse'}{'links'}};
@pages=grep m{^Wikipedia:Possibly unfree files/}, @pages;
# Second, add in the past 8 days
my @t=gmtime(time+3600);
for(my $i=0; $i<8; $i++){
my $t=strftime("Wikipedia:Possibly unfree files/%Y %B %e", @t);
$t=~s/ / /g;
push @pages, $t;
$t[3]--;
}
my @closedpages=();
MAINLOOP: foreach my $title (@pages){
return 0 if $api->halting;
next unless $title=~m{^Wikipedia:Possibly unfree files/(\d{4}) ([A-Z][a-z]+) (\d{1,2})$};
my $tt="/$1 $2 $3";
my ($y,$m,$d)=($1,$2,$3);
for($m=$#months; $m>0; $m--){
last if $2 eq $months[$m];
}
next unless $m>0;
my $date=[$d,$m,$y];
$api->log("Checking PUFs in $title");
my $tok=$api->edittoken($title);
if($tok->{'code'} eq 'shutoff'){
$api->warn("Task disabled: ".$tok->{'content'}."\n");
return 300;
}
if($tok->{'code'} ne 'success'){
$api->warn("Failed to get edit token for $title: ".$tok->{'error'}."\n");
return 60;
}
my $intxt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'} // '';
my $outtxt=$intxt;
# Fix header if necessary
my $fixedhead=0;
my $pageheader=_makepagehead($date);
if($outtxt!~/^\Q$pageheader\E/){
my $dt=$months[$date->[1]].' '.$date->[0];
my $oldtxt;
do {
$oldtxt=$outtxt;
$outtxt=~s/^(?:|.*?\n)===\s*\Q$dt\E\s*===[^\n]*(?:\n|$)//s;
$outtxt=~s/^\s*//;
} while($oldtxt ne $outtxt);
$outtxt="$pageheader\n$outtxt";
}
$fixedhead=($outtxt ne $intxt);
# Fix any simple mispositioned headers: armor any good headers, then
# fix any mispositioned ones, then unarmor.
my ($marker, $i)=('', 0);
do {
$marker = "\x02--$i--\x03";
$i++;
} while($outtxt=~/$marker/);
$outtxt=~s/(?:^|(?<=\n))((====+)[^=](?:.*[^=])?\2\s*?\n\s*$noticere*$is_closed_re)/$1$marker/go;
my $fixed=($outtxt=~s/(?:^|(?<=\n))((?>$re).*\n)\s*((====+)[^=](?:.*[^=])?\3\s*?\n)/$2$1/go);
$outtxt=~s/$marker//go;
# Split into level-4+ sections, and check if each is closed
my @sections=$api->split_sections($outtxt, "456");
my %img=();
my $ct=0;
my @closed=();
my @moved=();
for(my $i=0; $i<@sections; $i++){
my $s=$sections[$i];
if($s->{'body'}=~m/^\s*$noticere*$is_closed_re/o){
# Someone closed a section, so merge in all its subsections
my $j;
for($j=$i+1; $j<@sections && $sections[$j]->{'level'} > $s->{'level'}; $j++){}
if($j>$i+1){
$s->{'body'}=~s/\s*$/\n\n/;
$s->{'body'}.=$api->join_sections(splice(@sections, $i+1, $j-$i-1));
}
}
$_ = $s->{'body'};
my $bad=/(?>^\s*$noticere*\S).*$is_closed_re/so;
next if !$bad && m/^\s*$noticere*$is_closed_re/o;
if($bad || /$is_closed_re/so){
$api->log("Crap, $title is b0rken");
$api->warn("Crap, $title is b0rken\n");
$api->whine("[[$title]] is broken", "Help! A section in [[$title]] contains the \"is_closed\" regex but not at the beginning of the section. Probably someone put the {{tl|puf top}} before a section header instead of after. Anyway, I can't do anything to that page until someone fixes it.");
next MAINLOOP;
}
next unless defined($s->{'level'});
$ct++;
next unless $s->{'title'}=~/^\s*\[\[(?:: *Image|:? *Media|: *File) *:((?:[^#<>\[\]{|}]|�*(?:3[489]|61);|�*(?:2[267]|3[dD]);|&|"|')*)(?>(?:\|.*?)?\]\])\s*$/io;
my $img=$1;
$img=~s/�*34;|�*22;|"/"/g;
$img=~s/�*39;|�*27;|'/'/g;
$img=~s/�*61;|�*3[dD];/=/g;
$img=~s/�*38;|�*26;|&/&/g;
$img=uri_unescape($img);
$img=~s/^\s*|\s*$//g;
if($img eq 'Image_name.ext' || $img eq 'Image name.ext' ||
$img eq 'File_name.ext' || $img eq 'File name.ext'){
$s->{'body'}=~s/^\s*|\s*$//g;
$s->{'body'}="{{subst:puf top|1='''Erroneous Nomination'''. When following the [[WP:PUF#Instructions|listing instructions (step 2)]], you need to replace \"<code>File_name.ext</code>\" with the actual name of the file. You'll also want to put your reason for deletion just after \"<code>reason=</code>\". Feel free to just replace this entire section with the corrected template. If you are still having trouble, ask for help at [[WT:PUF]] or at my talk page. ~~~~}}\n".$s->{'body'}."\n{{subst:puf bottom}}\n";
$ct--;
push @closed, "[[:File:$img]]";
} elsif($img eq ''){
$s->{'body'}=~s/^\s*|\s*$//g;
$s->{'body'}="{{subst:puf top|1='''Erroneous Nomination'''. No image name is specified. Feel free to just replace this entire section with the corrected template. If you are still having trouble, ask for help at [[WT:FFD]] or at my talk page. ~~~~}}\n".$s->{'body'}."\n{{subst:puf bottom}}\n";
$ct--;
push @closed, "[[:File:$img]]";
} else {
$img{"File:$img"}=$s;
}
}
if($ct==0 && @closed==0 && !$fixedhead){
push @closedpages, $tt;
next;
}
# Check if the unclosed files still exist. If not, close the
# discussion.
my @commentednonfree=();
my @titles=keys %img;
while(@titles){
my @img=splice(@titles, 0, 500);
$res=$api->query(
titles => join('|', @img),
prop => 'info|imageinfo|categories|templates',
iiprop => 'canonicaltitle',
clcategories => 'Category:All non-free media|Category:All free media',
onlylist('tltemplates',500,@ok_nonfree_templates),
tllimit => 'max',
);
if($res->{'code'} ne 'success'){
$api->warn("Failed to retrieve file info for $title: ".$res->{'error'}."\n");
return 60;
}
my %map=();
%map=map { $_->{'to'}, $_->{'from'} } @{$res->{'query'}{'normalized'}} if exists($res->{'query'}{'normalized'});
PAGE: foreach (values %{$res->{'query'}{'pages'}}){
my $img=$api->apply_redirect_map( $_->{'title'}, \%map );
if(!exists($img{$img})){
my $d=Dumper($_); $d=~s/\s+/ /g;
$api->warn("How odd, this was apparently returned even though it wasn't requested: $d\n");
next PAGE;
}
# File exists here?
if($_->{'imagerepository'} eq 'local' && $_->{'imageinfo'}[0]{'canonicaltitle'} eq $_->{'title'}){
# Comment if it's non-free
next PAGE unless exists($_->{'categories'});
next PAGE unless grep($_->{'title'} eq 'Category:All non-free media', @{$_->{'categories'}});
next PAGE if grep($_->{'title'} eq 'Category:All free media', @{$_->{'categories'}});
next PAGE if $img{$img}{'body'}=~/<!-- AnomieBOT non-free notice -->/;
# Certain templates make the non-freeness OK
foreach my $t (@{$self->{'ok-nonfree'}}) {
next PAGE if(exists($_->{'templates'}) && grep($_->{'title'} eq $t, @{$_->{'templates'}}));
}
$img{$img}{'body'}=~s/\s*$//;
$img{$img}{'body'}.="\n* '''Note:''' This image is currently tagged as non-free. If there is a dispute with the rationale, please tag the image with {{tl|dfu}} or list it at [[WP:FFD]].<!-- AnomieBOT non-free notice --> ~~~~\n";
push @commentednonfree, "[[:".$_->{'title'}."]]";
next PAGE;
}
my $msg=undef;
# First, check for a deletion.
my $r=$api->query(
letitle => $_->{'title'},
list => 'logevents',
letype => 'delete',
lelimit => 1,
leprop => 'user|timestamp|comment',
);
if($r->{'code'} ne 'success'){
$api->warn("Failed to retrieve logs for ".$_->{'title'}.": ".$r->{'error'}."\n");
return 60;
}
if(exists($r->{'query'}{'logevents'}[0])){
my $log=$r->{'query'}{'logevents'}[0];
# Skip for now if it was deleted within the past hour, to
# give the closing admin a chance to close it themself.
my $t=$api->ISO2timestamp($log->{'timestamp'});
next PAGE if(time()-$t<3600);
# Check whether the deletion log entry could reasonably
# belong to this PUF: the deletion log entry should be
# dated on or after the PUF page date. Give them a day of
# leeway just in case.
$t=_date_add(_make_date($t),1,0,0);
if(_cmp_date($t,$date)>=0){
# Close it!
$msg="'''Delete'''; deleted";
if($log->{'comment'}=~/CSD(?:#|\]\] | )([GIF]\d+)/i ||
$log->{'comment'}=~/db-([gif]\d+)/){
my $c=uc($1);
$msg.=" as [[WP:CSD#$c|$c]]";
} elsif($log->{'comment'}=~/db-([a-z])/ && exists($db{$1})){
my $c=$db{$1};
$msg.=" as [[WP:CSD#$c|$c]]";
} elsif($log->{'comment'}=~/\x7b\x7b\s*isd\s*[|\x7d]/){
$msg.=" as [[WP:CSD#F1|F1]]";
}
$msg.=" by {{admin|".$log->{'user'}."}}";
$msg.=' A file with this name on [[commons:|Commons]] is now visible.' if $_->{'imagerepository'} eq 'shared';
}
}
if(!defined($msg) && $_->{'imageinfo'}[0]{'canonicaltitle'} ne $_->{'title'} && exists($_->{'redirect'})){
# It's a redirect. Is it because it was moved, or because
# it was created that way?
# Get target
my %targets=$api->resolve_redirects($_->{'title'});
if(exists($targets{''})){
$api->log("Could not resolve redirect ".$_->{'title'}.": ".$targets{''}{'error'});
next PAGE;
}
my $target=$targets{$_->{'title'}};
my $r=$api->query(
letitle => $_->{'title'},
list => 'logevents',
letype => 'move',
lelimit => 1,
);
if($r->{'code'} ne 'success'){
$api->warn("Failed to retrieve logs for ".$_->{'title'}.": ".$r->{'error'}."\n");
return 60;
}
if(exists($r->{'query'}{'logevents'}[0])){
my $log=$r->{'query'}{'logevents'}[0];
# Skip for now if it was moved within the past hour,
# to see if the mover closes it themself.
my $ts=$api->ISO2timestamp($log->{'timestamp'});
next PAGE if(time()-$ts<3600);
# Check whether the move log entry could reasonably
# belong to this PUF: the move log entry should be
# dated on or after the PUF page date. Give them a day
# of leeway just in case.
my $t=_date_add(_make_date($ts),1,0,0);
if(_cmp_date($t,$date)>=0){
# Yes: comment and change the title, but don't
# actually close.
my $from=$log->{'title'};
my $to=$log->{'params'}{'target_title'};
my $user=$log->{'user'};
my $ts=strftime("%H:%M, %d %B %Y (UTC)", gmtime $ts);
$ts=~s/, 0/, /;
$img{$img}{'title'}="[[:$target]]";
$img{$img}{'body'}=~s/\s*$//;
$img{$img}{'body'}.="\n* '''Note:''' The file was moved from [[:$from]] to [[:$to]] by {{user|1=$user}} at $ts";
$img{$img}{'body'}.="; it now redirects to [[:$target]]" if $target ne $to;
$img{$img}{'body'}.=". ~~~~\n";
push @moved, "[[:$from]]→[[:$target]]";
next PAGE;
}
}
# Redirect, but not because of a move. Close it.
my $iter=$api->iterator(
titles => $_->{'title'},
prop => 'revisions',
rvprop => 'timestamp|content',
rvslots => 'main',
rvsection => 0,
rvlimit => 5,
);
my $ts=0;
my $redir_re=$api->redirect_regex;
while(my $res=$iter->next){
if(!$res->{'_ok_'}){
$api->warn("Could not retrieve revisions from iterator: ".$res->{'error'}."\n");
return 60;
}
$res=$res->{'revisions'}[0];
last unless $res->{'slots'}{'main'}{'*'}=~$redir_re;
$ts=$api->ISO2timestamp($res->{'timestamp'});
}
# WTF?
if($ts==0){
my $t=$_->{'title'};
$api->warn("$t claims it's a redirect, but no #REDIRECT found?\n");
$api->whine("[[:$t]] confuses me", "When checking [[:$t]], the API prop=info reported it's a redirect, but the top revision does not match $redir_re. Which probably means my code needs fixing.");
next PAGE;
}
# Skip if it was redirectified too recently
next PAGE if(time()-$ts<3600);
if($target=~/^(?:File|Image):/i){
$msg="'''Redirect'''. The file nominated is a redirect. If you are trying to nominate the redirect for deletion, list it at [[WP:RFD]]. If you are trying to nominate [[:$target]] for deletion, please nominate it by that name.";
} else {
$msg="'''Bad Redirect'''. The file nominated is a redirect to [[:$target]], which is outside the File namespace. Please fix it, or tag it with {{tl|db-imagepage}} if it cannot be fixed.";
}
}
if(!defined($msg)){
# If we get here, there was no (valid) deletion log for
# this file, but it still doesn't exist locally.
if($_->{'imagerepository'} eq 'shared'){
$msg="'''Wrong forum'''. The file is on [[commons:|Commons]]. Please [[commons:Commons:Deletion requests|nominate it for deletion there]] if you feel it is non-free.";
} else {
$msg="'''File does not exist'''. If the file name in the header contains a typo, feel free to correct the typo and un-close this discussion.";
}
}
next if !defined($msg);
$img{$img}{'body'}=~s/^\s+|\s+$//g;
$img{$img}{'body'}="{{subst:puf top|1=$msg ~~~~}}\n".$img{$img}{'body'}."\n{{subst:puf bottom}}\n";
$ct--;
push @closed, "[[:".$_->{'title'}."]]";
}
}
# Mark for closing if applicable
push @closedpages, $tt if $ct==0;
# Need to edit?
next unless(@closed || @moved || $fixed || $fixedhead || @commentednonfree);
# Processed, now reconstruct the page
$outtxt=$api->join_sections(@sections);
# Subst templates, if necessary
my $subst=0;
$outtxt=$api->process_templates($outtxt, sub {
my $name=shift;
shift; #$params
my $wikitext=shift;
return undef unless exists($tosubst{"Template:$name"});
$subst++;
$wikitext=~s/^\{\{\s*/\{\{subst:/;
return $wikitext;
});
# Create summary
my @summary=();
if($fixedhead){
if(exists($tok->{'missing'})){
push @summary, "new discussion page: ".$date->[2].' '.$months[$date->[1]].' '.$date->[0];
} else {
push @summary, "fix page header";
}
}
push @summary, "subst {{puf top}} and/or {{puf bottom}}" if $subst>0;
push @summary, 'move closing box'.(($fixed>1)?'es':'').' per [[WP:DPR#PUF]]' if $fixed;
push @summary, 'close discussions for deleted/nonexistent files: '.join(', ', @closed) if @closed;
push @summary, 'rename discussions for moved files: '.join(', ', @moved) if @moved;
push @summary, 'commented on non-free files: '.join(', ', @commentednonfree) if @commentednonfree;
my $summary='(BOT) '.ucfirst(join('; ', @summary)).$screwup;
$api->log("$summary in $title");
if(length($summary)>500){
@summary=();
if($fixedhead){
if(exists($tok->{'missing'})){
push @summary, "new discussion page: ".$date->[2].' '.$months[$date->[1]].' '.$date->[0];
} else {
push @summary, "fix page header";
}
}
push @summary, "subst {{puf top}} and/or {{puf bottom}}" if $subst>0;
push @summary, 'move closing box'.(($fixed>1)?'es':'').' per [[WP:DPR#PUF]]' if $fixed;
push @summary, 'close discussions for deleted/nonexistent files: [too many to list]' if @closed;
push @summary, 'rename discussions for moved files: [too many to list]' if @moved;
push @summary, 'commented on non-free files: [too many to list]' if @commentednonfree;
$summary='(BOT) '.ucfirst(join('; ', @summary)).$screwup;
}
my $r=$api->edit($tok, $outtxt, $summary, 0, 1);
if($r->{'code'} ne 'success'){
$api->warn("Write failed on $title: ".$r->{'error'}."\n");
return 60;
}
}
my $tok=$api->edittoken('Wikipedia:Possibly unfree files');
if($tok->{'code'} eq 'shutoff'){
$api->warn("Task disabled: ".$tok->{'content'}."\n");
return 300;
}
if($tok->{'code'} ne 'success'){
$api->warn("Failed to get edit token for Wikipedia:Possibly unfree files: ".$tok->{'error'}."\n");
return 60;
}
my $intxt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'};
my $outtxt=$intxt;
my @s=();
if($intxt=~/(\n==\s*Holding cell\s*==\s*\n.*?\n)==/s){
my ($s1,$s2)=($1,$1);
if(@closedpages){
my $re=join('|',map quotemeta($_), @closedpages);
$s2=~s/\s*\n\*\s*\[\[(?:(?i:Wikipedia|WP)\s*:\s*Possibly unfree files)?(?:$re)\s*\]\]\s*(?=\n)//g;
$s2=~s/\s*$//;
push @s, "Removing completed dates from holding cell";
}
if($addNewDay){
my @t=gmtime(time-8*86400);
my $t=strftime("/%Y %B %e", @t);
unless($s2 =~ /(^|\n)\*\s*\[\[\Q$t\E\]\]/){
$s2 =~ s/\s*$/\n*[[$t]]\n/;
push @s, "Adding new date to holding cell";
}
}
$outtxt=~s/\Q$s1\E/$s2\n\n/;
}
my $ot=$outtxt; $ot=~s/\s//g;
my $it=$intxt; $it=~s/\s//g;
if($ot ne $it){
my $s=join('; ', @s);
$api->log($s);
my $r=$api->edit($tok, $outtxt, "(BOT) $s.$screwup", 0, 1);
if($r->{'code'} ne 'success'){
$api->warn("Write failed on Wikipedia:Possibly unfree files: ".$r->{'error'}."\n");
return 60;
}
}
# Save checked revision
$self->{'lasttime'}=$starttime;
$api->store->{'lasttime'}=$starttime;
$t=82800-($starttime%86400);
return $starttime+$t-time() if($t>0 && $t<3600);
$t=86400-($starttime%86400);
return $starttime+$t-time() if($t>0 && $t<3600);
return $starttime+3600-time();
}
sub _make_date {
my $t=shift || time;
if(ref($t) eq 'ARRAY'){
return _fix_date([@$t]);
} else {
my @t=gmtime($t);
@t=@t[3..5];
$t[1]+=1;
$t[2]+=1900;
return [@t];
}
}
sub _date_add {
my @t=@{$_[0]};
$t[0]+=$_[1];
$t[1]+=$_[2];
$t[2]+=$_[3];
return _fix_date([@t]);
}
sub _fix_date {
my $t=shift;
my @t=gmtime(timegm(0,0,0,$t->[0],$t->[1]-1,$t->[2]-1900));
@t=@t[3..5];
$t[1]+=1;
$t[2]+=1900;
return [@t];
}
sub _cmp_date {
my $a=shift;
my $b=shift;
my $x;
$x=$a->[2]-$b->[2];
$x=$a->[1]-$b->[1] if $x==0;
$x=$a->[0]-$b->[0] if $x==0;
return $x;
}
sub _makepagehead {
my $date=shift;
my $prevdt=_date_add($date,-1,0,0);
my $nextdt=_date_add($date,1,0,0);
return '<noinclude><div class="boilerplate metadata vfd" style="background-color: #F3F9FF; margin: 0 auto; padding: 0 1px 0 0; border: 1px solid #AAAAAA; font-size:10px">
{| width = "100%"
|-
! width="50%" align="left" | <span style="color:gray"><</span> [[Wikipedia:Possibly unfree files/'.$prevdt->[2].' '.$months[$prevdt->[1]].' '.$prevdt->[0].'|'.$months[$prevdt->[1]].' '.$prevdt->[0].']]
! width="50%" align="right" | [[Wikipedia:Possibly unfree files/'.$nextdt->[2].' '.$months[$nextdt->[1]].' '.$nextdt->[0].'|'.$months[$nextdt->[1]].' '.$nextdt->[0].']] <span style="color:gray">></span>
|}</div></noinclude>
'.'==='.$months[$date->[1]].' '.$date->[0].'===';
}
1;