package tasks::SourceUploader;
=pod
=begin metadata
Task: SourceUploader
BRFA: N/A
Status: Begun 2008-08-15
Rate: On bot startup only, max 6 edits/minute
Created: 2008-08-16
Updates the pages under [[User:AnomieBOT/source]] to reflect the current source
of the bot.
=end metadata
=cut
use utf8;
use strict;
use AnomieBOT::Task;
use vars qw/@ISA/;
@ISA=qw/AnomieBOT::Task/;
use POSIX qw/strftime/;
use Pod::Simple::Wiki;
use tasks::SourceUploader::Pod;
use Data::Dumper;
use Fcntl ':mode';
my %extensions=(
'pl' => 'perl',
'pm' => 'perl',
'ini' => 'ini',
'sh' => 'bash',
);
my @sizes=('', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB');
=pod
=for info
Per [[WP:BOT#Approval]], any bot or automated editing process that only
affects only the operators' user and talk pages (or subpages thereof),
and which are not otherwise disruptive, may be run without prior
approval.
=cut
sub approved {
return 999;
}
sub new {
my $class=shift;
my $self=$class->SUPER::new();
$self->{'pages'}={};
$self->{'loadexisting'}=1;
$self->{'order'}=-1000;
my $basedir=$AnomieBOT::API::basedir;
$basedir=~s{/$}{};
if(!-d $basedir){
$self->warn("Cannot find source directory\n");
return $self;
}
$self->{'summary'}='Updating published sources: ';
if(!open(X, "<", 'ChangeLog')){
$self->warn("Cannot load changelog\n");
return $self;
}
local $_;
my $intro=1;
while(<X>){
if(/^==.*==$/){
last if !$intro;
$intro=0;
next;
}
$self->{'summary'}.=$_ if !$intro;
}
close(X);
$self->{'summary'}=~s/\s+/ /g;
$self->{'summary'}=~s/\s+$//;
$self->{'summary'}=$self->{'summary'};
my %pages=();
my %tasks=(
'01 Current' => [],
'04 Awaiting approval' => [],
'05 Past' => [],
);
my @dirs=($basedir);
while(my $dir=shift @dirs){
if(!opendir(D, $dir)){
$self->warn("Cannot open directory $dir: $!\n");
return $self;
}
my $dirpage=substr($dir,length($basedir));
$dirpage='/'.$dirpage if(substr($dirpage,0,1) ne '/');
$dirpage ="==Index of $dirpage==\n";
$dirpage.="{| class=\"wikitable sortable\" style=\"width:100%\"\n";
$dirpage.="! Filename !! Size !! Modified\n";
while(my $page=readdir(D)){
next if substr($page,0,1) eq '.';
my $p="$dir/$page";
my $pp=substr($p,length($basedir));
my @stat=stat($p);
my $img='Gnome-fs-executable.svg';
if(-d $p){
next if ($stat[2]&(S_IROTH|S_IXOTH))!=(S_IROTH|S_IXOTH);
push @dirs, $p;
$img='Gnome-fs-directory-visiting.svg';
} elsif(-f $p){
next if ($stat[2]&(S_IROTH))!=(S_IROTH);
if(!open(X, '<:utf8', $p)){
$self->warn("Cannot open file $p: $!\n");
return $self;
}
do {
local $/=undef;
$pages{$pp}=<X>;
};
close(X);
my $top='';
if($page eq 'ChangeLog'){
# Pull out most recent 64K of changelog entries
my @x=split(/\n/, $pages{$pp});
$pages{$pp}='';
while(defined(my $x=shift @x)){
last if(length($pages{$pp})>65536 && $x=~/^==.*==$/);
$pages{$pp}.=$x."\n";
}
$pages{$pp}.="\x7b\x7bombox|type=notice|text=See the \x7b\x7bsubst:history|\x7b\x7bsubst:FULLPAGENAME\x7d\x7d|page history|subst=subst:\x7d\x7d for further ChangeLog entries\x7d\x7d\n" if @x;
} elsif($page=~/\.([^.]+)$/ && exists($extensions{$1})){
if($extensions{$1} eq 'perl'){
# Try to construct POD documentation
my $parser=Pod::Simple::Wiki->new('mediawiki');
my $x='';
$parser->output_string(\$x);
$parser->parse_string_document($pages{$pp});
$pages{"$pp/doc"}=$x if($parser->content_seen);
# Handle embedded notices and metadata
$x='';
$parser=tasks::SourceUploader::Pod->new;
$parser->output_string(\$x);
$parser->parse_string_document($pages{$pp});
$top.=$x if($parser->content_seen);
my %metadata=$parser->metadata;
if(%metadata){
$x ="<noinclude>\n";
$x.="{| class=\"wikitable\"\n";
$x.="! Task !! Disable !! {{tlx|bots}} !! Status !! Description\n";
$x.="</noinclude>\n";
$x.="|- valign=\"top\"\n";
my $task=$metadata{'task'};
my $bot="\x02BOT\x03";
$x.="|align=\"center\"| [[User:$bot/source$pp|$task]]\n";
if(exists($metadata{'shutoff'}) && $metadata{'shutoff'} eq 'false'){
$x.="|align=\"center\"| No\n";
} else {
$x.="|align=\"center\"| <span class=\"plainlinks\">[{{fullurl:User:$bot/shutoff/$task|action=edit}} Here]</span>\n";
}
if(exists($metadata{'exclusion'}) && $metadata{'exclusion'} eq 'false'){
$x.="|align=\"center\"| {{N}}\n";
} else {
$x.="|align=\"center\"| {{Y}}\n";
}
my $brfa=$metadata{'brfa'};
my $status=$metadata{'status'};
if($brfa eq 'N/A'){
$x.="| \x7b\x7bsort|$status|\x7d\x7d\x5b\x5bWikipedia:Bot policy#Approval|N/A\x5d\x5d, only edits bot's/owner's userspace. $status\n";
} elsif($brfa eq 'None'){
$x.="| $status\n";
} else {
$x.="| \x5b\x5b$brfa|$status\x5d\x5d\n";
}
if(exists($metadata{'+brfa'})){
$x.="<p style=\"margin:0;padding:0;font-size:smaller\">Supplemental:<br />\n";
for(my $i=0; $i<@{$metadata{'+brfa'}}; $i++){
$x.="+ \x5b\x5b".$metadata{'+brfa'}[$i].'|'.$metadata{'+status'}[$i]."\x5d\x5d<br />\n";
}
$x.="</p>\n";
}
$x.="|\n".$metadata{'*'}."\n";
$x.="<noinclude>\n";
$x.="|}\n";
$x.="</noinclude>";
$pages{"$pp/metadata"}=$x;
my $section=determine_task_section(".$pp", %metadata);
$tasks{$section}=[] unless exists($tasks{$section});
push @{$tasks{$section}}, $metadata{'created'}." {{User:$bot/source$pp/metadata}}";
}
}
$pages{$pp}="<source lang=\"".$extensions{$1}."\">\n".$pages{$pp}."\n</so"."urce>";
} else {
$pages{$pp}="<pre>\n".$pages{$pp}."\n</pre>";
}
$top.="\x7b\x7bombox|text=See \x5b\x5b/doc\x5d\x5d for formatted documentation\x7d\x7d\n" if(exists($pages{"$pp/doc"}));
$pages{$pp}=$top.$pages{$pp};
$img='Gnome-fs-regular.svg';
} else {
$self->warn("Unusual filetype on $p\n");
}
my $sz=$stat[7];
my $i=0;
while($sz>=1024 && $i<@sizes){ $sz/=1024; $i++; }
($sz="$sz")=~s/(\.\d\d)\d+/$1/;
$sz.=' '.$sizes[$i] if $i>0;
$dirpage.="|-\n";
$dirpage.="| \x5b\x5bImage:$img|32x32px\x5d\x5d";
$dirpage.=" \x5b\x5b/$page|$page\x5d\x5d\n";
$dirpage.="|align=\"right\"| \x7b\x7bsort|".sprintf("%020d",$stat[7])."|$sz\x7d\x7d\n";
$dirpage.="|align=\"center\"| \x7b\x7bsort|".sprintf("%020d",$stat[9]).strftime("|%F %T (UTC)", gmtime($stat[9]))."\x7d\x7d\n";
}
closedir(D);
$dirpage.="|}";
$pages{substr($dir,length($basedir))}=$dirpage;
}
my $tasklist='';
foreach my $section (sort keys %tasks){
my @links=sort @{$tasks{$section}};
$section=~s/^(\d+) //;
my $n=$1;
$tasklist.="\n" if $tasklist ne '';
$tasklist.="=== $section ===\n";
if(@links){
$tasklist.="{| class=\"wikitable sortable\"\n";
$tasklist.="! Task !! Disable !! {{tlx|bots}} !! ".(($n==1 || $n==2)?'Approval':'Status')." !! Description\n";
$tasklist.=join("\n", map { substr($_,11) } @links)."\n";
$tasklist.="|}\n";
} else {
$tasklist.="None at this time.\n";
}
}
$self->{'pages'}={%pages};
$self->{'tasklist'}=$tasklist;
return $self;
}
sub run {
my ($self, $api)=@_;
my @keys=keys(%{$self->{'pages'}});
if(!@keys && !exists($self->{'tasklist'})){
$self->warn("Terminating task\n");
return undef;
}
$api->task('SourceUploader');
$api->read_throttle(0);
$api->edit_throttle(10);
my $src='User:'.$api->user.'/source';
if($self->{'loadexisting'}){
my %q=(
list => 'allpages',
apprefix => $api->user.'/source',
apnamespace => '2',
aplimit => 'max'
);
my ($k,$v,$k2,$v2,@x);
my $res;
do {
$res=$api->query(%q);
if($res->{'code'} ne 'success'){
$self->warn("Failed to retrieve source tree: ".$res->{'error'}."\n");
return 300;
}
if(exists($res->{'query-continue'})){
$q{'apfrom'}=$res->{'query-continue'}{'allpages'}{'apfrom'};
}
foreach (@{$res->{'query'}{'allpages'}}){
$v2=substr($_->{'title'}, length($src));
if(!exists($self->{'pages'}{$v2})){
$self->{'pages'}{$v2}='';
push @keys, $v2;
}
}
} while(exists($res->{'query-continue'}));
$self->{'loadexisting'}=0;
}
while(@keys){
my $page=shift @keys;
my $text=$self->{'pages'}{$page};
my $ret=$self->upload_page($api, $src.$page, $text);
return $ret if $ret>60;
next if $ret>0;
delete($self->{'pages'}{$page});
}
if(exists($self->{'tasklist'})){
my $ret=$self->upload_page($api, 'User:'.$api->user.'/TaskList', $self->{'tasklist'});
return $ret if $ret>60;
next if $ret>0;
delete($self->{'tasklist'});
}
return 0;
}
sub upload_page {
my $self=shift;
my $api=shift;
my $page=shift;
my $text=shift;
my $tok=$api->edittoken($page);
if($tok->{'code'} eq 'shutoff'){
$self->warn("Task disabled: ".$tok->{'content'}."\n");
return 300;
}
return 60 if($tok->{'code'} ne 'success');
my $bot=$api->user;
$text=~s/\x02BOT\x03/$bot/go;
$text=~s/\s+$//o;
$tok->{'revisions'}[0]{'*'}=~s/\s+$//o if !exists($tok->{'missing'});
if(exists($tok->{'missing'}) ||
$tok->{'revisions'}[0]{'*'} ne $text){
my $r=$api->edit($tok, $text, $self->{'summary'}, 0, 1);
if($r->{'code'} ne 'success'){
$self->warn("Write error for $page: ".$r->{'error'}."\n");
return 60;
} else {
$self->warn("Updated $page\n");
}
} else {
$self->warn("No update needed for $page\n");
}
return 0;
}
sub determine_task_section {
my $file = shift;
my %metadata = @_;
my $t=$file; $t=~s{^.*/}{}; $t=~s/\.pm$//;
return '99 Invalid metadata' unless(exists($metadata{'task'}) && $metadata{'task'} eq $t);
return '99 Invalid metadata' unless exists($metadata{'brfa'});
return '99 Invalid metadata' unless exists($metadata{'status'});
return '99 Invalid metadata' unless exists($metadata{'rate'});
return '99 Invalid metadata' unless(exists($metadata{'created'}) && $metadata{'created'}=~/^\d{4}-\d\d-\d\d$/);
if(exists($metadata{'+brfa'})){
return '99 Invalid metadata' unless exists($metadata{'+status'});
return '99 Invalid metadata' if scalar(@{$metadata{'+status'}}) != scalar(@{$metadata{'+brfa'}});
}
if(exists($metadata{'ondemand'})){
return '99 Invalid metadata' unless($metadata{'ondemand'} eq 'true' || $metadata{'ondemand'} eq 'false');
}
if(exists($metadata{'shutoff'})){
return '99 Invalid metadata' unless($metadata{'shutoff'} eq 'true' || $metadata{'shutoff'} eq 'false');
}
if(exists($metadata{'exclusion'})){
return '99 Invalid metadata' unless($metadata{'exclusion'} eq 'true' || $metadata{'exclusion'} eq 'false');
}
my $status=$metadata{'status'};
AnomieBOT::API::load($file);
my $task='tasks::'.$metadata{'task'};
return '99 Invalid metadata' unless $task->can('approved');
my $botnum=$task->approved;
if($metadata{'brfa'} eq 'N/A'){
return "01 Current" if($botnum>0 && $status=~/^Begun \d{4}-\d{2}-\d{2}$/);
return "02 On demand" if(exists($metadata{'ondemand'}) && $metadata{'ondemand'} eq 'true');
}
if($status=~/^Approved \d{4}-\d{2}-\d{2}$/){
return "01 Current" if $botnum>0;
return "02 On demand" if(exists($metadata{'ondemand'}) && $metadata{'ondemand'} eq 'true');
return '99 Invalid metadata';
}
return '99 Invalid metadata' if $botnum>0;
return "03 In development" if($status eq 'Coding' || $status eq 'On hold');
return "05 Past" if $status=~/^Completed \d{4}-\d{2}-\d{2}$/;
return '99 Invalid metadata' if($metadata{'brfa'} eq 'N/A');
return "04 Awaiting approval" if($status eq 'BRFA');
return "06 Withdrawn" if $status eq 'Withdrawn';
return "07 Rejected" if $status=~/^Rejected \d{4}-\d{2}-\d{2}$/;
return '99 Invalid metadata';
}
1;