User:AnomieBOT/source/tasks/BAGBot.pm

From Wikipedia, the free encyclopedia
package tasks::BAGBot;

=pod

=begin metadata

Bot:      AnomieBOT
Task:     BAGBot
BRFA:     Wikipedia:Bots/Requests for approval/AnomieBOT 34
Status:   Approved 2009-11-17
+BRFA:    Wikipedia:Bots/Requests for approval/AnomieBOT 48
+Status:  Approved 2010-12-01
+BRFA:    Wikipedia:Bots/Requests for approval/AnomieBOT 53
+Status:  Approved 2011-09-05
+BRFA:    Wikipedia:Bots/Requests for approval/AnomieBOT 54
+Status:  Approved 2011-09-09
+BRFA:    Wikipedia:Bots/Requests for approval/AnomieBOT 56
+Status:  Approved 2011-09-26
Created:  2009-10-27

Various BAG-related maintenance tasks:
* Update [[Wikipedia:BAG/Status]]
* Notify users when {{tl|Operator assistance needed}} is used.
* Move BRFAs from Open to Trial to Trial Complete to Approved/Denied/Withdrawn/Expired as necessary.
* Remove [[:Category:Open Wikipedia bot requests for approval]] from closed BRFAs.

=end metadata

=cut

use utf8;
use strict;

use Data::Dumper;
use Date::Parse;
use URI::Escape;
use AnomieBOT::Task qw/:time/;
use vars qw/@ISA/;
@ISA=qw/AnomieBOT::Task/;

my $version=6;

sub new {
    my $class=shift;
    my $self=$class->SUPER::new();
    bless $self, $class;
    return $self;
}

=pod

=for info
Approved 2009-11-17<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 34]]

=for info
Supplemental BFRA approved 2010-12-01<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 48]]

=for info
Supplemental BFRA approved 2011-09-05<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 53]]

=for info
Supplemental BFRA approved 2011-09-09<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 54]]

=for info
Supplemental BFRA approved 2011-09-26<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 56]]

=cut

sub approved {
    return 2;
}

sub run {
    my ($self, $api)=@_;
    my $res;
    my $needmoving=0;

    $api->task('BAGBot',0,10,qw/d::Sections d::Redirects d::Templates d::Talk d::Timestamp/);

    if($api->store->{'version'}//0 < $version){
        $api->store->{'BAGrevid'}=0;
        $api->store->{'BRFArevid'}=0;
        my %BRFA=%{$api->store->{'BRFA'} // {}};
        $_->{'realrevid'}=0 foreach (values %BRFA);

        if($api->store->{'version'}//0 < 4){
            foreach (values %BRFA){
                $_->{'notify_botedited'}=0x8001 if $_->{'notify_botedited'}==3;
            }
        }
        if($api->store->{'version'}//0 < 5){
            foreach (values %BRFA){
                $_->{'notified_botedited'}=3 if $_->{'notify_botedited'}>=0x8000;
                $_->{'notify_botedited'}=$_->{'notify_botedited'}&0x7fff;
            }
        }
        if($api->store->{'version'}//0 < 6){
            foreach (values %BRFA){
                $_->{'notified_botedited'}|=4;
            }
        }

        $api->store->{'BRFA'}=\%BRFA;
        $api->store->{'version'} = $version;
    }

    # Get the last rev ID for the pages we care about, to skip downloading the
    # entire pages if not necessary.
    $res=$api->query(titles=>'Wikipedia:Bot Approvals Group|Wikipedia:Bots/Requests for approval', prop=>'revisions', rvprop=>'ids');
    if($res->{'code'} ne 'success'){
        $api->warn("Failed to get revids: ".$res->{'error'}."\n");
        return 60;
    }
    my ($BAGrevid,$BRFArevid)=(undef,undef);
    foreach (values %{$res->{'query'}{'pages'}}){
        $BAGrevid=$_->{'revisions'}[0]{'revid'} if $_->{'title'} eq 'Wikipedia:Bot Approvals Group';
        $BRFArevid=$_->{'revisions'}[0]{'revid'} if $_->{'title'} eq 'Wikipedia:Bots/Requests for approval';
    }
    if(!defined($BAGrevid) || !defined($BRFArevid)){
        $api->warn("Response was missing requested revids: ".$res->{'error'}."\n");
        return 60;
    }

    # Get a list of BAG members
    my @BAG=();
    if($BAGrevid==($api->store->{'BAGrevid'} // 0)){
        @BAG=@{$api->store->{'BAG'}};
    } else {
        $res=$api->query(titles=>'Wikipedia:Bot Approvals Group', prop=>'revisions', rvprop=>'content', rvslots=>'main');
        if($res->{'code'} ne 'success'){
            $api->warn("Failed to get WP:BAG: ".$res->{'error'}."\n");
            return 60;
        }
        my $txt=(values %{$res->{'query'}{'pages'}})[0]{'revisions'}[0]{'slots'}{'main'}{'*'};
        if($txt!~/\n==+\s*Member [Ll]ist\s*==+\s*\n\{\|[^\n]*\n(.*?)\n\|\}/s){
            $api->warn("Failed to find member table in WP:BAG\n");
            $api->whine("[[WP:BAG]] cannot be processed", "I could not find the Member List table in [[WP:BAG]], which means either someone changed the wikitext or someone vandalized the page. Either fix it back to the old layout, or update me to find the new version. Thanks.");
            return 60;
        }
        $txt=$1;
        my $rownum=0;
        foreach (split /\n\|-[^\n]*(?:\n|$)/, $txt){
            ++$rownum;
            next if /^\s*\|\+/; # Skip caption row.
            next if /^\s*!/; # Skip header row.
            if(/^\s*\|\s*\{\{[uU]ser\|\s*(.*?)\s*\}\}\s*\|/){
                push @BAG, $1;
            } else {
                $api->warn("Invalid row in WP:BAG member table\n$_\n");
                $api->whine("[[WP:BAG]] cannot be processed", "Row $rownum in the BAG Member List table was not recognized, which means either someone changed the wikitext or someone vandalized the page. Either fix it back to the old layout, or update me to recognize the new version. Thanks.");
                return 60;
            }
        }
        if(!@BAG){
            $api->warn("Invalid WP:BAG member table\n$_\n");
            $api->whine("[[WP:BAG]] cannot be processed", "The BAG Member List table didn't seem to contain any BAG members.");
            return 60;
        }
        $api->store->{'BAGrevid'}=$BAGrevid;
        $api->store->{'BAG'}=\@BAG;
        $api->store->{'BAGupdated'}=1;
    }

    # Get a list of redirects to templates we care about.
    my %tr = $api->redirects_to_resolved(
        'Template:BotTrial', 'Template:BotExtendedTrial', 'Template:BotTrialComplete', 'Template:BotOnHold',
        'Template:BotApproved', 'Template:BotSpeedy', 'Template:BotDenied', 'Template:BotExpired', 'Template:BotWithdrawn', 'Template:BotRevoked',
        'Template:OperatorAssistanceNeeded', 'Template:BAGAssistanceNeeded'
    );
    if(exists($tr{''})){
        $api->warn("Could not load list of redirects to status templates: ".$tr{''}{'error'}."\n");
        return 60;
    }

    # Get the list of active BRFAs
    my %BRFA=();
    if($BRFArevid==($api->store->{'BRFArevid'} // 0)){
        %BRFA=%{$api->store->{'BRFA'}};
        if($api->store->{'BAGupdated'}){
            $_->{'realrevid'}=0 foreach (values %BRFA);
            $api->store->{'BRFA'}=\%BRFA;
            $api->store->{'BAGupdated'}=0;
        }
    } else {
        my %B=();
        my %oldB=();
        foreach my $b (values %{$api->store->{'old BRFA'}}, values %{$api->store->{'BRFA'}}){
            $B{$b->{'page'}}=$b;

            # Keep record of old BRFAs for a short time after completion, just
            # in case they come back.
            $oldB{$b->{'page'}}=$b if($b->{'targettimestamp'}>time()-7*86400);
        }

        $res=$api->query(titles=>'Wikipedia:Bots/Requests for approval', prop=>'revisions', rvprop=>'content', rvslots=>'main');
        if($res->{'code'} ne 'success'){
            $api->warn("Failed to get WP:BRFA: ".$res->{'error'}."\n");
            return 60;
        }
        my $txt=(values %{$res->{'query'}{'pages'}})[0]{'revisions'}[0]{'slots'}{'main'}{'*'};
        my @sec=$api->split_sections($txt, "1");
        if(@sec!=6 ||
           $sec[1]{'title'} ne 'Current requests for approval' ||
           $sec[2]{'title'} ne 'Bots in a trial period' ||
           $sec[3]{'title'} ne 'Bots that have completed the trial period' ||
           $sec[4]{'title'} ne 'Denied requests' ||
           $sec[5]{'title'} ne 'Expired/withdrawn requests'){
            $api->warn("Failed to parse WP:BRFA\n");
            $api->whine("[[WP:BRFA]] cannot be processed", "The BRFA list cannot be processed. Most likely, someone has screwed around with the section headers. Either fix it back to the old layout, or update me to handle the new version. Thanks.");
            return 60;
        }
        my @s=(
            0,'Open',$sec[1]{'body'},
            1,'In trial',$sec[2]{'body'},
            2,'Trial complete',$sec[3]{'body'},
        );

        my $sort=0;
        while(@s){
            my $secnum=shift @s;
            my $section=shift @s;
            my $fail=0;
            $api->process_templates(shift @s, sub {
                return undef if $fail;
                my $name=shift;
                my $params=shift;

                return undef unless $name eq 'BRFA';

                my %p=();
                foreach ($api->process_paramlist(@$params)){
                    $p{$_->{'name'}}=$_->{'value'};
                }

                $p{1}=~s/[\s_]/ /g; $p{1}=~s/^ | $//g;
                $p{2}=~s/[\s_]/ /g; $p{2}=~s/^ | $//g;

                my $nm=$p{1}.(($p{2} ne '')?' '.$p{2}:'');
                my $pg="Wikipedia:Bots/Requests for approval/$nm";
                my $b=$B{$pg} // {
                    page=>$pg,
                    bot=>'',
                    reqnum=>0,
                    name=>$nm,
                    operator=>undef,
                    revid=>0,
                    realrevid=>0,
                    editby=>'Never edited',
                    edittime=>'N/A',
                    oprevid=>0,
                    bagrevid=>0,
                    bageditby=>'Never edited by BAG',
                    bagedittime=>'N/A',
                    'optout-operatorassistanceneeded'=>0,
                    targettimestamp=>time(),
                    targetsection=>-1,
                    check_newbot=>1,
                    notify_botedited=>0,
                    notified_botedited=>0,
                };
                if($b->{'bot'} ne $p{1} || $b->{'reqnum'} ne $p{2}){
                    if($p{2} eq ''){
                        my ($u,$rn)=($p{1},$p{2});
                        while(1){
                            my $res=$api->query(list=>'users', ususers=>$u);
                            if($res->{'code'} ne 'success'){
                                $api->warn("Failed to check user existence for $u: ".$res->{'error'}."\n");
                                $fail=60;
                                return undef;
                            }
                            $res=$res->{'query'}{'users'}[0];
                            unless(exists($res->{'missing'}) || exists($res->{'invalid'})){
                                ($p{1},$p{2})=($u,$rn);
                                last;
                            }
                            last unless $u=~s/ ([^ ]*)$//;
                            $rn=($rn eq '')?$1:"$1 $rn";
                        }
                    }
                    $b->{'bot'}=$p{1};
                    $b->{'reqnum'}=$p{2};
                    $b->{'check_newbot'}=1;
                    $needmoving++;
                }
                $b->{'page'}=$pg;
                $b->{'name'}=$nm;
                $b->{'section'}=$section;
                $b->{'secnum'}=$secnum;
                $b->{'sort'}=++$sort;
                $b->{'realrevid'}=0 if $api->store->{'BAGupdated'};
                $b->{'redirected'}=0;
                $b->{'notify_botedited'}//=0;
                $b->{'notified_botedited'}//=0;

                if(!defined($b->{'created'})){
                    $res=$api->query(titles=>$pg, prop=>'revisions', rvlimit=>1, rvdir=>'newer', rvprop=>'timestamp');
                    if($res->{'code'} ne 'success'){
                        $api->warn("Cannot find creation date for $pg\n");
                    } else {
                        $b->{'created'}=ISO2timestamp((values %{$res->{'query'}{'pages'}})[0]{'revisions'}[0]{'timestamp'});
                    }
                }

                $BRFA{$pg}=$b;
                $oldB{$pg}=$b;

                return undef;
            });
            return $fail if $fail;
        }
        $api->store->{'BRFArevid'}=$BRFArevid;
        $api->store->{'BRFA'}=\%BRFA;
        $api->store->{'old BRFA'}=\%oldB;
        $api->store->{'BAGupdated'}=0;
    }

    # Get a list of unflagged bots being requested
    my %check_unflagged=();
    my @u=keys %{{map { ucfirst($_->{'bot'})=>1 } values %BRFA}};
    while(@u){
        my @uu=splice(@u,0,500);
        $res=$api->query(list=>'users', usprop=>'groups', ususers=>join('|', @uu), titles=>'Wikipedia:Bots/Requests for approval/Approved', prop=>'links', pllimit=>'max', pltitles=>'User:'.join('|User:', @uu));
        if($res->{'code'} ne 'success'){
            $api->warn("Failed to get bot flags: ".$res->{'error'}."\n");
            return 60;
        }
        foreach my $u (@{$res->{'query'}{'users'}}) {
            next if(exists($res->{'missing'}) || exists($res->{'invalid'}));
            $check_unflagged{$u->{'name'}} = 1 unless grep $_ eq 'bot', @{$u->{'groups'}//[]};
        }
        foreach my $u (map $_->{'title'}, @{(values %{$res->{'query'}{'pages'}})[0]{'links'}}) {
            $u=~s/^User://;
            delete $check_unflagged{$u};
        }
    }

    # Load the data for all the BRFAs
    my @brfas=keys %BRFA;
    my @need_edit_brfa=();
    my $active=0;
    my $needbag=0;
    while(@brfas){
        $res=$api->query(titles=>join('|', splice(@brfas,0,500)),prop=>'revisions|templates|categories', cllimit=>'max', tllimit=>'max', rvprop=>'ids|flags|user|timestamp', redirects=>1);
        if($res->{'code'} ne 'success'){
            $api->warn("Failed to get BRFA info: ".$res->{'error'}."\n");
            return 60;
        }
        my %redir=map { $_->{to} => $_->{from} } @{$res->{'query'}{'redirects'}};
        foreach my $r (values %{$res->{'query'}{'pages'}}){
            my $pg=$r->{'title'};
            if(exists($r->{'missing'})){
                $api->warn("BRFA page $pg does not exist\n");
                next;
            }
            my $key=$pg;
            unless(defined($BRFA{$key})){
                next unless exists($redir{$key});
                next unless exists($BRFA{$redir{$key}});
                $api->warn($redir{$key}." redirected to $key");
                $key=$redir{$key};
            }
            my $b={ %{$BRFA{$key}} };

            # Get the operator, if we need it
            unless(defined($b->{'operator'} // undef)){
                my $c=$api->rawpage($pg);
                if($c->{'code'} ne 'success'){
                    $api->warn("Failed to get BRFA page content: ".$c->{'error'}."\n");
                    return 60;
                }
                $c=$c->{'content'};
                unless($c=~/'''Operator:'''\s*.*?(?:\[\[\s*(?::\s*)*(?i:User|User[ _]talk)\s*:|\{\{(?:[uU]ser|[bB]otop)\s*\|)\s*([^\]}|]+?)\s*[\]}|]/){
                    my $n=$pg; $n=~s!^.*/!!;
                    $api->warn("Failed to find operator name in $pg\n");
                    $api->whine("Cannot find operator of [[$pg|$n]]", "I could not find the operator of the bot in [[$pg]]. I look for <code><nowiki>'''Operator:'''</nowiki></code> with a wikilink to the User or User talk namespace on the same line. Please fix it! Thanks.");
                }
                $b->{'operator'}=$1;
                $b->{'notify_botedited'}|=4 if ($b->{'operator'}//'?') eq $b->{'bot'};
            }

            # Update the last edit info
            if($b->{'realrevid'}!=$r->{'revisions'}[0]{'revid'}){
                my $rv=$b->{'realrevid'};
                $b->{'realrevid'}=$r->{'revisions'}[0]{'revid'};

                my ($needOp,$needUser,$needBAG)=(1,1,1);

                if($r->{'revisions'}[0]{'user'} ne $api->user){
                    $b->{'revid'}=$r->{'revisions'}[0]{'revid'};
                    $b->{'editby'}=$r->{'revisions'}[0]{'user'};
                    $b->{'edittime'}=strftime('%F, %T', gmtime $api->ISO2timestamp($r->{'revisions'}[0]{'timestamp'}));
                    $needUser=0;
                }
                if(!exists($r->{'revisions'}[0]{'minor'}) && $b->{'editby'} eq ($b->{'operator'}//'?')){
                    $b->{'oprevid'}=$b->{'revid'};
                    $needOp=0;
                }
                if(!exists($r->{'revisions'}[0]{'minor'}) && $b->{'editby'} ne ($b->{'operator'}//'?') && grep($_ eq $b->{'editby'}, @BAG)){
                    $b->{'bagrevid'}=$b->{'revid'};
                    $b->{'bageditby'}=$b->{'editby'};
                    $b->{'bagedittime'}=$b->{'edittime'};
                    $needBAG=0;
                }
                my %q=(
                    prop=>'revisions',
                    titles=>$pg,
                    rvprop=>'user|ids|timestamp|flags',
                    rvlimit=>'10',
                    $rv ? (rvendid=>$rv) : ()
                );
                my @rr=();
                while($needUser || $needBAG || $needOp){
                    if(!@rr){
                        last unless %q;
                        my $res=$api->query(%q);
                        if($res->{'code'} ne 'success'){
                            $api->warn("Failed to retrieve revisions for $pg: ".$res->{'error'}."\n");
                            return 60;
                        }
                        @rr=@{(values %{$res->{'query'}{'pages'}})[0]{'revisions'}};
                        if(exists($res->{'query-continue'}{'revisions'})){
                            while(my ($k,$v)=each(%{$res->{'query-continue'}{'revisions'}})){
                                $q{$k}=$v;
                            }
                        } else {
                            %q=();
                        }
                    }
                    my $r=shift @rr;
                    $r->{'user'} //= ''; # If revdeled
                    if($needUser && $r->{'user'} ne $api->user){
                        $b->{'revid'}=$r->{'revid'};
                        $b->{'editby'}=$r->{'user'};
                        $b->{'edittime'}=strftime('%F, %T', gmtime $api->ISO2timestamp($r->{'timestamp'}));
                        $needUser=0;
                    }
                    if($needBAG && !exists($r->{'minor'}) && $r->{'user'} ne ($b->{'operator'}//'?') && grep($_ eq $r->{'user'}, @BAG)){
                        $b->{'bagrevid'}=$r->{'revid'};
                        $b->{'bageditby'}=$r->{'user'};
                        $b->{'bagedittime'}=strftime('%F, %T', gmtime $api->ISO2timestamp($r->{'timestamp'}));
                        $needBAG=0;
                    }
                    if($needOp && !exists($r->{'minor'}) && $r->{'user'} eq ($b->{'operator'}//'?')){
                        $b->{'oprevid'}=$r->{'revid'};
                        $needOp=0;
                    }
                }
            }

            # To determine the BRFA status...
            my %cats=map { $_->{'title'}, 1 } @{$r->{'categories'}};
            my %tmpl=map { $_->{'title'}, 1 } @{$r->{'templates'}};
            my $c='';
            my $need_user=($tmpl{$tr{'Template:OperatorAssistanceNeeded'}} // 0);
            push @need_edit_brfa, $pg if($need_user && !$b->{'optout-operatorassistanceneeded'} || $b->{'check_newbot'} || ($b->{'notify_botedited'} & ~$b->{'notified_botedited'}));
            my $need_bag=($tmpl{$tr{'Template:BAGAssistanceNeeded'}} // 0);
            if($cats{'Category:Revoked Wikipedia bot requests for approval'} // 0){
                $need_user=$need_bag=0;
                $c='style="background-color:orange"';
                $b->{'status'}='Revoked';
                $b->{'targettimestamp'}=time() if $b->{'targetsection'}!=-1;
                $b->{'targetsection'}=-1;
            } elsif($cats{'Category:Approved Wikipedia bot requests for approval'} // 0){
                $need_user=$need_bag=0;
                $c='style="background-color:lightblue"';
                $b->{'targettimestamp'}=time() if $b->{'targetsection'}!=3;
                $b->{'targetsection'}=3;
                if($tmpl{$tr{'Template:BotSpeedy'}} // 0){
                    $b->{'status'}='Speedy Approved';
                } else {
                    $b->{'status'}='Approved';
                }
            } elsif($cats{'Category:Denied Wikipedia bot requests for approval'} // 0){
                $need_user=$need_bag=0;
                $c='style="background-color:orange"';
                $b->{'status'}='Denied';
                $b->{'targettimestamp'}=time() if $b->{'targetsection'}!=4;
                $b->{'targetsection'}=4;
            } elsif($cats{'Category:Withdrawn Wikipedia bot requests for approval'} // 0){
                $need_user=$need_bag=0;
                $c='style="background-color:gray"';
                $b->{'status'}='Withdrawn';
                $b->{'targettimestamp'}=time() if $b->{'targetsection'}!=5;
                $b->{'targetsection'}=5;
            } elsif($cats{'Category:Expired Wikipedia bot requests for approval'} // 0){
                $need_user=$need_bag=0;
                $c='style="background-color:yellow"';
                $b->{'status'}='Expired';
                $b->{'targettimestamp'}=time() if $b->{'targetsection'}!=5;
                $b->{'targetsection'}=5;
            } elsif($cats{'Category:Open Wikipedia bot requests for approval'} // 0){
                $active++;
                if($tmpl{$tr{'Template:BotTrialComplete'}} // $tmpl{$tr{'Template:BotExtendedTrial'}} // $tmpl{$tr{'Template:BotTrial'}} // $tmpl{$tr{'Template:BotOnHold'}} // 0){
                    # "Trial complete" may have been superseded by a later new
                    # "Trial". Assume the comments are in chronological order,
                    # and use the last found.
                    my $cc=$api->rawpage($pg);
                    if($cc->{'code'} ne 'success'){
                        $api->warn("Failed to retrieve page data for $pg: ".$cc->{'error'}."\n");
                        return 60;
                    }
                    my $ts=0;
                    $api->process_templates($cc->{'content'}, sub {
                        my $name=shift;
                        $name =~ s/^(Template|msg)://i;
                        $name=$tr{"Template:$name"} // "Template:$name";
                        if($name eq $tr{'Template:BotTrialComplete'}){
                            $c='style="background-color:lightblue"';
                            $b->{'status'}='Trial complete';
                            $ts=2;
                        } elsif($name eq $tr{'Template:BotExtendedTrial'}){
                            $c='style="background-color:lightgreen"';
                            $b->{'status'}='Extended trial';
                            $ts=1;
                        } elsif($name eq $tr{'Template:BotTrial'}){
                            $c='style="background-color:lightgreen"';
                            $b->{'status'}='In trial';
                            $ts=1;
                        } elsif($name eq $tr{'Template:BotOnHold'}){
                            $c='style="background-color:lightgray"';
                            $b->{'status'}='On hold';
                            # For now, leave in whichever section other templates want to put it in.
                        }
                    });
                    $b->{'targettimestamp'}=time() if $b->{'targetsection'}!=$ts;
                    $b->{'targetsection'}=$ts;
                } else {
                    $c='';
                    $b->{'status'}='Open';
                    $b->{'targettimestamp'}=time() if $b->{'targetsection'}!=0;
                    $b->{'targetsection'}=0;
                }
            } else {
                $c='style="background-color:#f88"';
                $b->{'status'}='Unknown';
                $b->{'status'}.=': BAG assistance requested!' if $need_bag;
                $b->{'targettimestamp'}=time() if $b->{'targetsection'}!=-1;
                $b->{'targetsection'}=-1;
                $need_user=$need_bag=0;
            }
            if(!($cats{'Category:Revoked Wikipedia bot requests for approval'} // 0)) {
                my $inconsistent=0;
                $inconsistent=1 if ($tmpl{$tr{'Template:BotRevoked'}} // 0);
                $inconsistent=1 if(!($cats{'Category:Approved Wikipedia bot requests for approval'} // 0) && ($tmpl{$tr{'Template:BotApproved'}} // $tmpl{$tr{'Template:BotSpeedy'}} // 0));
                $inconsistent=1 if(!($cats{'Category:Denied Wikipedia bot requests for approval'} // 0) && ($tmpl{$tr{'Template:BotDenied'}} // 0));
                $inconsistent=1 if(!($cats{'Category:Withdrawn Wikipedia bot requests for approval'} // 0) && ($tmpl{$tr{'Template:BotWithdrawn'}} // 0));
                $inconsistent=1 if(!($cats{'Category:Expired Wikipedia bot requests for approval'} // 0) && ($tmpl{$tr{'Template:BotExpired'}} // 0));
                if($inconsistent){
                    $need_user=$need_bag=0;
                    $b->{'status'}.=': Inconsistent categories/tags!';
                    $c='style="background-color:#f88"';
                    $needbag++;
                }
            }
            $b->{'status'}.=': User response needed!' if $need_user;
            $b->{'status'}.=': BAG assistance requested!' if $need_bag;
            $c='style="background-color:lightgreen"' if $need_user;
            $c='style="background-color:#f88"' if $need_bag;
            $needbag++ if $need_bag;
            $b->{'color'}=$c;
            $BRFA{$key}={ %$b, redirected=>1 };
            $BRFA{$pg}=$b;
            $needmoving++ if($b->{'targetsection'}>0 && $b->{'targetsection'}>$b->{'secnum'});

            # If a bot has been approved for a trial, remove from the
            # "check unflagged" list.
            delete $check_unflagged{ucfirst($b->{'bot'})} if $b->{'targetsection'} > 0;
        }
    }
    $api->store->{'BRFA'}=\%BRFA;

    my $iter=$api->iterator(
        generator    => 'categorymembers',
        gcmtitle     => 'Category:Open Wikipedia bot requests for approval',
        gcmnamespace => 4,
        gcmlimit     => 500,
        prop         => 'categories',
        cllimit      => 'max',
        clcategories => join('|',
            'Category:Approved Wikipedia bot requests for approval',
            'Category:Denied Wikipedia bot requests for approval',
            'Category:Expired Wikipedia bot requests for approval',
            'Category:Revoked Wikipedia bot requests for approval',
            'Category:Withdrawn Wikipedia bot requests for approval',
        )
    );
    while(my $p=$iter->next){
        if(!$p->{'_ok_'}){
            $api->warn("Failed to retrieve members of Category:Open Wikipedia bot requests for approval: ".$p->{'error'}."\n");
            return 60;
        }
        next unless $p->{'title'}=~m{^Wikipedia:Bots/Requests for approval/};
        next unless(exists($p->{'categories'}) && @{$p->{'categories'}}>0);
        my $tok=$api->edittoken($p->{'title'}, EditRedir=>1);
        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 $p->{title}: ".$tok->{'error'}."\n");
            next;
        }
        my $intxt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'};
        my $outtxt=$intxt;
        my $re=qr#<noinclude>\s*\[\[\s*(?i:Category)\s*:\s*Open Wikipedia bot requests for approval\s*(?>\|.*?(?=\]\]))?\]\]\s*</noinclude>#;
        $outtxt=~s/\n$re *\n/\n/g;
        $outtxt=~s/$re//g;
        if($intxt eq $outtxt){
            $api->warn("Couldn't find <noinclude>[[Category:Open Wikipedia bot requests for approval]]</noinclude> in $p->{title}, despite it being in the category!");
            next;
        }
        my $summary="Removing [[Category:Open Wikipedia bot requests for approval]] from closed BRFA";
        $api->log("$summary in $p->{title}");
        my $r=$api->edit($tok, $outtxt, $summary, 0, 0);
        if($r->{'code'} ne 'success'){
            $api->warn("Write failed on $p->{title}: ".$r->{'error'}."\n");
            next;
        }
    }

    # Construct the table
    @brfas=sort { $a->{'sort'} <=> $b->{'sort'} } values %BRFA;
    my $txt='{| border="1" class="sortable wikitable plainlinks"'."\n";
    $txt.="!Bot Name !! Status !! Created !! Last editor !! Date/Time !! Last BAG editor !! Date/Time\n";
    my $listed=0;
    foreach (@brfas){
        next if $_->{'redirected'};
        my $bt=uri_escape_utf8(ucfirst($_->{'bot'}));
        $txt.="|-\n";
        $txt.="| [[$_->{page}|$_->{name}]] <small>([[User talk:$_->{bot}|T]]|[[Special:Contributions/$_->{bot}|C]]|[{{SERVER}}/wiki/Special:Log/block?page=User:$bt B]|[{{SERVER}}/wiki/Special:Log/rights?page=User:$bt F])</small>\n";
        $txt.="|$_->{color}|$_->{status}\n";
        $txt.=defined($_->{'created'})?strftime("| %F, %T\n",gmtime $_->{'created'}):"| {{sort|0|unknown}}\n";
        my $cls='';
        $cls = 'class="MostRecentIsOp"' if $_->{'oprevid'} == $_->{'revid'};
        $cls = 'class="MostRecentIsBAG"' if $_->{'bagrevid'} == $_->{'revid'};
        $txt.=$_->{'revid'} ? "|$cls| [[Special:Diff/prev/$_->{revid}|$_->{editby}]] ||$cls| $_->{edittime}\n" : "| Never edited || {{sort|0|n/a}}\n";
        $txt.=$_->{'bagrevid'} ? "| [[Special:Diff/prev/$_->{bagrevid}|$_->{bageditby}]] || $_->{bagedittime}\n" : "| Never edited by BAG || {{sort|0|n/a}}\n";
        $listed++;
    }
    $txt.="|}";

    # Edit the page, if necessary
    my $tok=$api->edittoken('Wikipedia:BAG/Status', EditRedir=>1);
    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 WP:BAG/Status: ".$tok->{'error'}."\n");
        return 60;
    }
    if($txt ne ($tok->{'revisions'}[0]{'slots'}{'main'}{'*'} // '')){
        my $summary="Updating table: $listed BRFAs listed, $active active";
        $summary.=", 1 needs BAG attention!" if $needbag==1;
        $summary.=", $needbag need BAG attention!" if $needbag>1;
        $api->log($summary);
        my $r=$api->edit($tok, $txt, $summary, 0, 0);
        if($r->{'code'} ne 'success'){
            $api->warn("Write failed on WP:BAG/Status: ".$r->{'error'}."\n");
            return 60;
        }
    }

    # Any unflagged bots that haven't been approved for a trial yet should be
    # checked to see if they are editing without approval.
    foreach my $u (keys %check_unflagged){
        # Damn global interwiki bots.
        my $res=$api->query(meta=>'globaluserinfo',guiuser=>$u,guiprop=>'groups|merged');
        if($res->{'code'} ne 'success'){
            $api->warn("Failed to retrieve global user info for $u: ".$res->{'error'}."\n");
            return 60;
        }
        $res=$res->{'query'}{'globaluserinfo'};
        my $globalbot = (grep($_->{'wiki'} eq 'enwiki', @{$res->{'merged'}//[]}) && grep($_ eq 'Global_bot', @{$res->{'groups'}//[]})) ? 1 : 0;

        my $sts=time();
        my $ts=time();
        my %u=(quotemeta(ucfirst($u))=>1);
        my @brfas=();
        foreach my $b (values %BRFA){
            next unless ucfirst($b->{'bot'}) eq $u;
            push @brfas, $b;
            $u{quotemeta(ucfirst($b->{'operator'}//'?'))}=1;
            my $t=$b->{'unflagged edit checked'} // $b->{'created'};
            $ts=$t if $t < $ts;
        }
        my $uu=join('|', keys %u);
        my $re=qr/^User(?: talk)?:(?:$uu)(?:\/|$)/;
        my $iter=$api->iterator(list=>'usercontribs', ucend=>timestamp2ISO($ts), ucprop=>"title", ucuser=>$u);
        while(my $p=$iter->next){
             if(!$p->{'_ok_'}){
                 $api->warn("Failed to retrieve contribs for $u: ".$p->{'error'}."\n");
                 return 60;
             }
             next if $p->{'title'}=~/$re/;

             foreach my $b (@brfas){
                 $b->{'notify_botedited'}|=($b->{'page'} eq $p->{'title'} ? 2 : ($globalbot?0:1));
                 if($b->{'notify_botedited'} & ~$b->{'notified_botedited'}){
                     push @need_edit_brfa, $b->{'page'} unless grep $_ eq $b->{'page'}, @need_edit_brfa;
                 }
             }
             last;
        }
        foreach my $b (@brfas){
            $b->{'unflagged edit checked'}=$sts;
        }
    }
    $api->store->{'BRFA'}=\%BRFA;

    # Notify users and check {{Newbot}} as necessary
    foreach my $pg (@need_edit_brfa){
        my $n=$pg; $n=~s!^.*/!!;

        my $tok=$api->edittoken($pg);
        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 $pg: ".$tok->{'error'}."\n");
            next;
        }
        next if exists($tok->{'missing'});

        my $intxt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'};
        my ($fixed_newbot,$notified)=(0,0);
        my $outtxt=$api->process_templates($intxt, sub {
            my $name=shift;
            my $params=shift;
            shift; # $wikitext
            shift; # $data
            my $oname=shift;

            if($name eq 'Newbot'){
                my $have2=0;
                foreach ($api->process_paramlist(@$params)){
                    $have2=1 if $_->{'name'} eq '2';
                }

                my $diff=0;
                my $ret="{{$oname";
                my $b=$api->store->{'BRFA'}{$pg};
                push @$params, '' if(!$have2 && $b->{'reqnum'} ne '');
                foreach ($api->process_paramlist(@$params)){
                    $_->{'value'}=~s/^\s+|\s+$//g;
                    if($_->{'name'} eq '1'){
                        $diff=1 if $_->{'value'} ne $b->{'bot'};
                        $ret.='|';
                        $ret.=$b->{'bot'};
                    } elsif($_->{'name'} eq '2'){
                        $diff=1 if $_->{'value'} ne $b->{'reqnum'};
                        $ret.='|';
                        $ret.=$b->{'reqnum'};
                    } else {
                        $ret.='|'.$_->{'text'};
                    }
                }
                $ret.="}}";
                return undef unless $diff;
                $fixed_newbot=1;
                return $ret;
            }

            if($name eq 'Operator assistance needed' || $name eq 'OperatorAssistanceNeeded'){
                foreach ($api->process_paramlist(@$params)){
                    return undef if($_->{'name'} eq '1' && $_->{'value'} ne '');
                }
                $notified=1;
                return "{{$oname|D}}";
            }

            return undef;
        });
        my $notified_botedited=0;
        my $nbe = ($api->store->{'BRFA'}{$pg}{'notify_botedited'} & ~$api->store->{'BRFA'}{$pg}{'notified_botedited'});
        my @nbesummary=();
        if($nbe&4){
            if($api->store->{'BRFA'}{$pg}{'bot'}=~/bot(?:\s*\d+|\s+[XVI]+)?$/i){
                $outtxt.="\n* {{TakeNote}} This request specifies the bot account as the operator. A bot may not operate itself; please update the \"Operator\" field to indicate the account of the ''human'' running this bot. ~~~~\n";
                push @nbesummary, 'noted that the bot is listed as its own operator';
                $notified_botedited|=6;
            } else {
                my $u=$api->store->{'BRFA'}{$pg}{'bot'};
                $res=$api->query(list=>'users', ususers=>$u, usprop=>'editcount');
                if($res->{'code'} ne 'success'){
                    $api->warn("Failed to get edit count for $u: ".$res->{'error'}."\n");
                    next;
                }
                if(($res->{'query'}{'users'}[0]{'editcount'}//0) < 500){
                    $outtxt.="\n* {{TakeNote}} The user account this request is for is also listed as the Operator, but the account name does not clearly indicate that the account is a bot and the account has very few edits. Please note that [[WP:Bot policy]] states that a bot account's username should make it immediately clear that the account is in fact a bot, which is normally done by having the account name end with the word \"Bot\". Also note that a bot may not operate itself, so the Operator field should identify the account of the ''human'' running the bot. <!-- If this BRFA is requesting approval for mass page creation or another manual task requring BAG approval, please remove this note. --> ~~~~\n";
                    push @nbesummary, 'noted that the request appears to be for a non-bot account from a relatively new editor';
                }
                $notified_botedited|=7;
            }
            $nbe&=~$notified_botedited;;
        }
        if($nbe&2){
            $outtxt.="\n* {{TakeNote}} This bot has edited its own BRFA page. [[WP:BOTPOL|Bot policy]] states that the bot account is only for edits on approved tasks or trials approved by BAG; the operator must log into their normal account to make any non-bot edits. ~~~~\n";
            push @nbesummary, 'noted that the bot has edited its own BRFA page';
            $notified_botedited|=2;
        }
        if($nbe&1){
            $outtxt.="\n* {{TakeNote}} This bot appears to have edited since this BRFA was filed. Bots may not edit outside their own or their operator's userspace unless approved or approved for trial. ~~~~\n";
            push @nbesummary, 'noted that the bot has edited before being approved';
            $notified_botedited|=1;
        }

        if($outtxt ne $intxt){
            my @summary=();
            if($notified){
                unless($intxt=~/'''Operator:'''\s*.*?(?:\[\[\s*(?::\s*)*(?i:User|User[ _]talk)\s*:|\{\{(?:[uU]ser|[bB]otop)\s*\|)\s*([^\]}|]+?)\s*[\]}|]/){
                    $api->warn("Failed to find operator name in $pg\n");
                    $api->whine("Cannot notify operator of [[$pg|$n]]", "I could not find the operator of the bot in [[$pg]] in order to notify them of the {{tl|Operator assistance needed}} on that BRFA. I look for <code><nowiki>'''Operator:'''</nowiki></code> with a wikilink to the User or User talk namespace on the same line. Please fix it! Thanks.");
                    next;
                }
                my $user=$1;

                push @summary, "notified $user about {{OperatorAssistanceNeeded}}";
                my $r=$api->whine("Your bot request $n", "Someone has marked [[$pg]] as needing your input. Please visit that page to reply to the requests. Thanks! ~~~~ <small style=\"color:gray\">To opt out of these notifications, place <nowiki>{{bots|optout=operatorassistanceneeded}}</nowiki> anywhere on this page.</small>", Summary=>"Your assistance is needed at [[$pg|$n]]", Pagename=>"User talk:$user", NoSmallPrint=>1, OptOut=>'operatorassistanceneeded', NoSig=>1);
                if($r->{'code'} eq 'shutoff'){
                    $api->warn("Task disabled: ".$r->{'content'}."\n");
                    return 300;
                }
                if($r->{'code'} eq 'botexcluded'){
                    $api->warn("Excluded from User talk:$user: ".$r->{'error'}."\n");
                    my $b=$api->store->{'BRFA'};
                    $b->{$pg}{'optout-operatorassistanceneeded'}=1;
                    $api->store->{'BRFA'}=$b;
                    next;
                }
                if($r->{'code'} ne 'success'){
                    $api->warn("Failed to post to User talk:$user: ".$r->{'error'}."\n");
                    next;
                }
                $api->log("Notified $user about Operator assistance needed on $pg");
            }
            push @summary, "corrected {{Newbot}}" if $fixed_newbot;
            push @summary, @nbesummary;
            $summary[0]=ucfirst($summary[0]);
            $summary[$#summary]='and '.$summary[$#summary] if @summary>1;
            my $summary=join(@summary>2?', ':' ', @summary);
            $api->log("$summary in $pg");
            my $r=$api->edit($tok, $outtxt, $summary, 1, 1);
            if($r->{'code'} ne 'success'){
                $api->warn("Failed to write $pg: ".$r->{'error'}."\n");
                next;
            }
            my $b=$api->store->{'BRFA'};
            $b->{$pg}{'check_newbot'}=0;
            $b->{$pg}{'notified_botedited'}|=$notified_botedited;
            $api->store->{'BRFA'}=$b;
        }
        my $b=$api->store->{'BRFA'};
        $api->store->{'BRFA'}=$b;
    }

    if($needmoving){
        my $tok=$api->edittoken('Wikipedia:Bots/Requests for approval', EditRedir=>1);
        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 WP:Bots/Requests for approval: ".$tok->{'error'}."\n");
            return 60;
        }
        my $txt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'} // '';
        my @sec=$api->split_sections($txt, "1");
        if(@sec!=6 ||
           $sec[1]{'title'} ne 'Current requests for approval' ||
           $sec[2]{'title'} ne 'Bots in a trial period' ||
           $sec[3]{'title'} ne 'Bots that have completed the trial period' ||
           $sec[4]{'title'} ne 'Denied requests' ||
           $sec[5]{'title'} ne 'Expired/withdrawn requests'){
            $api->warn("Failed to parse WP:BRFA\n");
            $api->whine("[[WP:BRFA]] cannot be processed", "The BRFA list cannot be processed. Most likely, someone has screwed around with the section headers. Either fix it back to the old layout, or update me to handle the new version. Thanks.");
            return 60;
        }

        my %BRFA=();
        foreach my $b (values %{$api->store->{'BRFA'}}){
            $BRFA{$b->{'page'}}=$b;
        }
        my %tosec=();
        my @s=(
            0,$sec[1],
            1,$sec[2],
            2,$sec[3],
        );
        my @summary=();
        my $archived_denied=0;
        my $archived_expired=0;
        my %cts=();
        while(@s){
            my $secnum=shift @s;
            my $s=shift @s;
            $s->{'body'}=$api->process_templates($s->{'body'}, sub {
                my $name=shift;
                my $params=shift;
                shift; # $wikitext
                shift; # $data
                my $oname=shift;

                return undef unless $name eq 'BRFA';

                my %p=();
                foreach ($api->process_paramlist(@$params)){
                    $p{$_->{'name'}}=$_->{'value'};
                }
                $p{1}=~s/[\s_]/ /g; $p{1}=~s/^ | $//g;
                $p{2}=~s/[\s_]/ /g; $p{2}=~s/^ | $//g;
                my $nm=$p{1}.(($p{2} ne '')?' '.$p{2}:'');
                my $pg="Wikipedia:Bots/Requests for approval/$nm";
                my $b=$BRFA{$pg} // return undef;

                my $ret=undef;
                if($p{1} ne $b->{'bot'} || $p{2} ne $b->{'reqnum'}){
                    $ret="{{$oname";
                    foreach ($api->process_paramlist(@$params)){
                        if($_->{'name'} eq '1'){
                            $ret.='|';
                            $ret.='1=' if $b->{'bot'}=~/=/;
                            $ret.=$b->{'bot'};
                        } elsif($_->{'name'} eq '2'){
                            $ret.='|';
                            $ret.='2=' if $b->{'reqnum'}=~/=/;
                            $ret.=$b->{'reqnum'};
                        } else {
                            $ret.='|'.$_->{'text'};
                        }
                    }
                    $ret.="}}";
                    $cts{'corrected {{BRFA}}s'}++;
                    push @summary, "correct {{BRFA}} for $nm";
                }

                # No need to move if it's already where it belongs
                return $ret if $b->{'targetsection'}==$secnum;
                # Never move to "Open"
                return $ret if $b->{'targetsection'}<=0;
                # Give a human time to move it?
                #XXX return $ret if $b->{'targettimestamp'}>=time()-3600;
                push @{$tosec{$b->{'targetsection'}}},$b;
                return '';
            });
        }

        if(exists($tosec{1})){
            unless($sec[2]{'body'}=~/-->\s*\n/){
                $api->warn("Failed to find trial insertion point in WP:BRFA\n");
                $api->whine("[[WP:BRFA]] cannot be processed", "I could not find the insertion point in [[WP:BRFA#".$sec[2]{'title'}."]]. Please fix it. Thanks.");
                return 60;
            }
            foreach my $b (@{$tosec{1}}) {
                $sec[2]->{'body'}=~s/-->\s*\n/-->\n{{BRFA|$b->{bot}|$b->{reqnum}|Trial}}\n/;
                my $back=($b->{'secnum'}>1)?' back':'';
                push @summary, $b->{'bot'}.' '.$b->{'reqnum'}.$back.' to trial';
                $cts{'to trial'}++;
            }
        }
        if(exists($tosec{2})){
            unless($sec[3]{'body'}=~/-->\s*\n/){
                $api->warn("Failed to find trial-complete insertion point in WP:BRFA\n");
                $api->whine("[[WP:BRFA]] cannot be processed", "I could not find the insertion point in [[WP:BRFA#".$sec[3]{'title'}."]]. Please fix it. Thanks.");
                return 60;
            }
            foreach my $b (@{$tosec{2}}) {
                $sec[3]->{'body'}=~s/-->\s*\n/-->\n{{BRFA|$b->{bot}|$b->{reqnum}|Trial}}\n/;
                push @summary, $b->{'bot'}.' '.$b->{'reqnum'}.' trial complete';
                $cts{'trial complete'}++;
            }
        }
        if(exists($tosec{4})){
            unless($sec[4]{'body'}=~/-->\s*\n/){
                $api->warn("Failed to find denied insertion point in WP:BRFA\n");
                $api->whine("[[WP:BRFA]] cannot be processed", "I could not find the insertion point in [[WP:BRFA#".$sec[4]{'title'}."]]. Please fix it. Thanks.");
                return 60;
            }
            foreach my $b (@{$tosec{4}}) {
                $sec[4]->{'body'}=~s/-->\s*\n/-->\n{{BRFA|$b->{bot}|$b->{reqnum}|Denied|~~~~~}}\n/;
                push @summary, $b->{'bot'}.' '.$b->{'reqnum'}.' denied';
                $cts{'denied'}++;
            }

            my $seen=0;
            $sec[4]->{'body'}=$api->process_templates($sec[4]->{'body'}, sub {
                my $name=shift;
                my $params=shift;

                return undef unless $name eq 'BRFA';
                return undef if $seen++<15;
                my $ts=0;
                foreach ($api->process_paramlist(@$params)){
                    $ts=str2time($_->{'value'})//0 if $_->{'name'} eq 4;
                }
                return undef if $ts >= time()-7*86400;
                $archived_denied++;
                return '';
            });
        }
        if(exists($tosec{5})){
            unless($sec[5]{'body'}=~/-->\s*\n/){
                $api->warn("Failed to find withdrawn/expired insertion point in WP:BRFA\n");
                $api->whine("[[WP:BRFA]] cannot be processed", "I could not find the insertion point in [[WP:BRFA#".$sec[5]{'title'}."]]. Please fix it. Thanks.");
                return 60;
            }
            foreach my $b (@{$tosec{5}}) {
                my $s = $b->{'status'};
                $s =~ s/:.*//; # Avoid [[Special:Diff/1094004967]]
                $sec[5]->{'body'}=~s/-->\s*\n/-->\n{{BRFA|$b->{bot}|$b->{reqnum}|$s|~~~~~}}\n/;
                push @summary, $b->{'bot'}.' '.$b->{'reqnum'}.' '.lc($b->{'status'});
                $cts{lc($b->{'status'})}++;
            }

            my $seen=0;
            $sec[5]->{'body'}=$api->process_templates($sec[5]->{'body'}, sub {
                my $name=shift;
                my $params=shift;

                return undef unless $name eq 'BRFA';
                return undef if $seen++<15;
                my $ts=0;
                foreach ($api->process_paramlist(@$params)){
                    $ts=str2time($_->{'value'})//0 if $_->{'name'} eq 4;
                }
                return undef if $ts >= time()-7*86400;
                $archived_expired++;
                return '';
            });
        }

        if(exists($tosec{3})){
            my $tok2=$api->edittoken('Wikipedia:Bots/Requests for approval/Approved', EditRedir=>1, links=>{namespace=>4});
            if($tok2->{'code'} eq 'shutoff'){
                $api->warn("Task disabled: ".$tok2->{'content'}."\n");
                return 300;
            }
            if($tok2->{'code'} ne 'success'){
                $api->warn("Failed to get edit token for WP:Bots/Requests for approval/Approved: ".$tok2->{'error'}."\n");
                return 60;
            }
            my $txt2=$tok2->{'revisions'}[0]{'slots'}{'main'}{'*'} // '';
            unless($txt2=~/-->\s*\n/){
                $api->warn("Failed to find insertion point in WP:Bots/Requests for approval/Approved\n");
                $api->whine("[[WP:Bots/Requests for approval/Approved]] cannot be processed", "I cannot find the insertion point in [[WP:Bots/Requests for approval/Approved]]. Please fix it. Thanks.");
                return 60;
            }
            my %l=map { $_->{'title'}=>1 } @{$tok2->{'links'}};
            my %u=();
            foreach my $b (@{$tosec{3}}) {
                push @summary, $b->{'bot'}.' '.$b->{'reqnum'}.' '.lc($b->{'status'});
                $cts{lc($b->{'status'})}++;
                $u{ucfirst($b->{'bot'})}=0 unless exists($l{$b->{'page'}});
            }
            $api->process_templates($txt2, sub {
                my $name=shift;
                my $params=shift;
                return undef unless $name eq 'BRFA';
                foreach ($api->process_paramlist(@$params)){
                    $u{ucfirst($_->{'value'})}=0 if $_->{'name'} eq '1';
                }
                return undef;
            });
            my $res=$api->query(list=>'users',ususers=>join('|',keys %u),usprop=>'groups');
            if($res->{'code'} ne 'success'){
                $api->warn("Failed to get user info for users ".join(', ',keys %u).": ".$res->{'error'}."\n");
                return 60;
            }
            foreach my $u (@{$res->{'query'}{'users'}}) {
                $u{$u->{'name'}}=grep $_ eq 'bot', @{$u->{'groups'}//[]};
            }
            my $new_brfas='';
            my @summary2=();
            my ($totalct,$needct,$nowct)=(0,0,0);
            foreach my $b (@{$tosec{3}}) {
                next if exists($l{$b->{'page'}});
                $totalct++;
                if($u{ucfirst($b->{'bot'})} // 0){
                    $new_brfas.="{{subst:BRFAA|$b->{bot}|$b->{reqnum}|Flagged|~~~~~}}\n";
                    push @summary2, $b->{'bot'}.' '.$b->{'reqnum'}.' '.lc($b->{'status'}).', has flag';
                } else {
                    $new_brfas.="{{BRFA|$b->{bot}|$b->{reqnum}|Approved|~~~~~}}\n";
                    push @summary2, $b->{'bot'}.' '.$b->{'reqnum'}.' '.lc($b->{'status'}).', NEEDS FLAG';
                    $needct++;
                }
            }
            $txt2=$api->process_templates($txt2, sub {
                my $name=shift;
                my $params=shift;
                return undef unless $name eq 'BRFA';
                my ($u,$rn)=('','');
                foreach ($api->process_paramlist(@$params)){
                    $u=$_->{'value'} if $_->{'name'} eq '1';
                    $rn=$_->{'value'} if $_->{'name'} eq '2';
                }
                return undef unless($u{$u} // 0);
                $nowct++;
                my $s="$u now flagged";
                push @summary2, $s unless grep $_ eq $s, @summary2;
                return "{{subst:BRFAA|$u|$rn|Flagged|~~~~~}}";
            });

            $txt2=~s/\s*\n\*\s*\n/\n/g;
            $txt2=~s/-->\s*\n/-->\n$new_brfas/;

            # Archive any BRFAs that need archiving
            my %archive=();
            my $archivect=0;
            my $seen=0;
            my $didnoinclude=0;
            my @lines=split(/\n/, $txt2);
            $txt2='';
            foreach my $l (@lines){
                next if $seen && $l=~/^<noinclude>\s*$/;
                my $archive=0;
                if($l=~/^(?:\s*\{\{(?:BRFA|subst:BRFAA)\s*(?:\|[^|]*){3}\|\s*|\*\s*\{\{botlinks\|[^\}]*\}\} ''Approved\s+)(~~~~~|\d{2}:\d{2}, \d{1,2} [A-Z][a-z]+ (\d{4}) \(UTC\))/){
                    my ($t,$y)=($1,$2);
                    $t=($t eq '~~~~~' ? time() : str2time($t));
                    if($seen++>=30 && defined($t) && $t<time()-7*86400){
                        if(!$didnoinclude){
                            $txt2.="<noinclude>\n";
                            $didnoinclude=1;
                        }
                        $archive=$y-2004 if $t<time()-365*86400;
                    }
                } elsif($seen && !$didnoinclude){
                    $txt2.="<noinclude>\n";
                    $didnoinclude=1;
                }
                if($archive){
                    $archive{$archive}=($archive{$archive}//'')."$l\n";
                    $archivect++;
                } else {
                    $txt2.="$l\n";
                }
            }
            push @summary2, "archived $archivect old request".($archivect==1?'':'s') if $archivect;

            if(@summary2){
                my $summary=ucfirst(join('; ', @summary2));
                $summary=~s/\s+/ /g;
                $api->log("$summary in WP:Bots/Requests for approval/Approved");
                if(length($summary)>500){
                    @summary2=();
                    push @summary2, "$totalct approved, ".($needct?"$needct NEED FLAG":"all are flagged") if $totalct;
                    push @summary2, "$nowct now flagged" if $nowct;
                    push @summary2, "$archivect archived" if $archivect;
                    $summary=ucfirst(join('; ', @summary2))." [too many to list]";
                }
                my $r=$api->edit($tok2, $txt2, $summary, 0, 0);
                if($r->{'code'} ne 'success'){
                    $api->warn("Failed to write WP:Bots/Requests for approval/Approved: ".$r->{'error'}."\n");
                    return 60;
                }
            }

            # Save to-be-archived BRFAs for later
            if($archivect){
                my $a=$api->store->{'BRFAA archives'};
                while(my ($k,$v)=each(%archive)){
                    $a->{$k}=$v.($a->{$k}//'');
                }
                $api->store->{'BRFAA archives'}=$a;
            }
        }

        push @summary, "archived $archived_denied denied request".($archived_denied==1?'':'s') if $archived_denied;
        push @summary, "archived $archived_expired expired/withdrawn request".($archived_expired==1?'':'s') if $archived_expired;

        if(@summary){
            $txt=$api->join_sections(@sec);
            my $summary=join('; ', @summary);
            $summary=~s/\s+/ /g;
            if(length($summary)>500){
                @summary=();
                for my $k (sort keys %cts) {
                    push @summary, $cts{$k}.' '.$k;
                }
                push @summary, "archived $archived_denied denied request".($archived_denied==1?'':'s') if $archived_denied;
                push @summary, "archived $archived_expired expired/withdrawn request".($archived_expired==1?'':'s') if $archived_expired;
                $summary=join(', ', @summary)." [too many to list]";
            }
            $api->log("$summary in WP:Bots/Requests for approval");
            my $r=$api->edit($tok, $txt, $summary, 0, 1);
            if($r->{'code'} ne 'success'){
                $api->warn("Failed to write WP:Bots/Requests for approval: ".$r->{'error'}."\n");
                return 60;
            }
        }
        my $b=$api->store->{'BRFA'};
        $api->store->{'BRFA'}=$b;
    }

    # Check for newly-flagged bots in BRFA/A
    {
        my $tok2=$api->edittoken('Wikipedia:Bots/Requests for approval/Approved', EditRedir=>1);
        if($tok2->{'code'} eq 'shutoff'){
            $api->warn("Task disabled: ".$tok2->{'content'}."\n");
            return 300;
        }
        if($tok2->{'code'} ne 'success'){
            $api->warn("Failed to get edit token for WP:Bots/Requests for approval/Approved: ".$tok2->{'error'}."\n");
            return 60;
        }
        my $txt2=$tok2->{'revisions'}[0]{'slots'}{'main'}{'*'} // '';
        my %u=();
        $api->process_templates($txt2, sub {
            my $name=shift;
            my $params=shift;
            return undef unless $name eq 'BRFA';
            foreach ($api->process_paramlist(@$params)){
                $u{ucfirst($_->{'value'})}=0 if $_->{'name'} eq '1';
            }
            return undef;
        });
        last unless %u;
        my $res=$api->query(list=>'users',ususers=>join('|',keys %u),usprop=>'groups');
        if($res->{'code'} ne 'success'){
            $api->warn("Failed to get user info for users ".join(', ',keys %u).": ".$res->{'error'}."\n");
            return 60;
        }
        foreach my $u (@{$res->{'query'}{'users'}}) {
            $u{$u->{'name'}}=grep $_ eq 'bot', @{$u->{'groups'}//[]};
        }
        my @summary2=();
        my $nowct=0;
        $txt2=$api->process_templates($txt2, sub {
            my $name=shift;
            my $params=shift;
            return undef unless $name eq 'BRFA';
            my ($u,$rn,$dt)=('','','~~~~~');
            foreach ($api->process_paramlist(@$params)){
                $u=ucfirst($_->{'value'}) if $_->{'name'} eq '1';
                $rn=$_->{'value'} if $_->{'name'} eq '2';
                $dt=$_->{'value'} if $_->{'name'} eq '4';
            }
            return undef unless($u{$u} // 0);
            $nowct++;
            my $s="$u now flagged";
            push @summary2, $s unless grep $_ eq $s, @summary2;
            return "{{subst:BRFAA|$u|$rn|Flagged|$dt}}";
        });
        if(@summary2){
            my $summary=join('; ', @summary2);
            $summary=~s/\s+/ /g;
            $summary="$nowct now flagged [too many to list]" if length($summary)>500;
            $api->log("$summary in WP:Bots/Requests for approval/Approved");
            my $r=$api->edit($tok2, $txt2, $summary, 0, 0);
            if($r->{'code'} ne 'success'){
                $api->warn("Failed to write WP:Bots/Requests for approval/Approved: ".$r->{'error'}."\n");
                return 60;
            }
        }
    }

    # Archive from WP:BRFA/A
    {
        my $a=$api->store->{'BRFAA archives'};
        foreach my $k (keys %$a) {
            my $tok2=$api->edittoken("Wikipedia:Bots/Requests for approval/Approved/Archive $k", EditRedir=>1);
            if($tok2->{'code'} eq 'shutoff'){
                $api->warn("Task disabled: ".$tok2->{'content'}."\n");
                return 300;
            }
            if($tok2->{'code'} ne 'success'){
                $api->warn("Failed to get edit token for WP:Bots/Requests for approval/Approved/Archive $k: ".$tok2->{'error'}."\n");
                return 60;
            }
            my $txt2=$tok2->{'revisions'}[0]{'slots'}{'main'}{'*'} // "{{archive}}\n";
            my $v=$a->{$k};
            unless($txt2=~s/\{\{archive\}\}\n/{{archive}}\n$v/){
                $api->warn("Failed to find insertion point in WP:Bots/Requests for approval/Approved/Archive $k\n");
                $api->whine("[[WP:Bots/Requests for approval/Approved/Archive $k]] cannot be processed", "I cannot find the insertion point in [[WP:Bots/Requests for approval/Approved/Archive $k]]. Please fix it, or fix me. Thanks.");
                next;
            }
            my $summary='Archiving old approvals';
            $api->log("$summary in WP:Bots/Requests for approval/Approved/Archive $k");
            my $r=$api->edit($tok2, $txt2, $summary, 0, 0);
            if($r->{'code'} eq 'success'){
                delete $a->{$k};
                $api->store->{'BRFAA archives'}=$a;
            } else {
                $api->warn("Failed to write WP:Bots/Requests for approval/Approved/Archive $k: ".$r->{'error'}."\n");
            }
        }
    }

    # Check if [[WP:BRFAAA]] needs updating
    {
        $res=$api->query([],
            list        =>'allpages', 
            apprefix    => 'Bots/Requests for approval/Approved/Archive ',
            apnamespace => 4,
            aplimit     => 'max',
        );
        if($res->{'code'} ne 'success'){
            $api->warn("Failed to get list of WP:BRFA/A archives: ".$res->{'error'}."\n");
            return 60;
        }
        my @pages=();
        foreach my $p (map $_->{'title'}, @{$res->{'query'}{'allpages'}}) {
            push @pages, $1 if $p=~m!^Wikipedia:Bots/Requests for approval/Approved/Archive (\d+)$!;
        }
        @pages=sort { $a <=> $b } @pages;

        $tok=$api->edittoken('Wikipedia:Bots/Requests for approval/Approved/Archives', links => { namespace => 4 });
        if($tok->{'code'} ne 'success'){
            $api->warn("Failed to get edit token and links for WP:BRFAAA: ".$tok->{'error'}."\n");
            return 60;
        }
        my $txt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'};
        my @links=map $_->{'title'}, @{$tok->{'links'}};

        my $any=0;
        $txt=~s/\s*$/\n/;
        foreach my $p (@pages){
            my $pg="Wikipedia:Bots/Requests for approval/Approved/Archive $p";
            next if grep $_ eq $pg, @links;
            my $y=$p+2004;
            $txt =~ s{<!-- BAGBot: New entry goes here -->}{*[[$pg|Archive $p]]: 1 January $y &ndash; 31 December $y\n$&};
            $any=1;
        }

        if($any){
            my $r=$api->edit($tok, $txt, "Updating list of archives", 0, 0);
            if($r->{'code'} ne 'success'){
                $api->warn("Failed to write WP:Bots/Requests for approval/Approved/Archives: ".$r->{'error'}."\n");
            }
        }
    }

    # Check again in 5 minutes.
    return 300;
}

1;