User:AnomieBOT/source/tasks/CHUUClerk.pm

package tasks::CHUUClerk;

=pod

=begin metadata

Bot:     AnomieBOT
Task:    CHUUClerk
BRFA:    Wikipedia:Bots/Requests for approval/AnomieBOT 47
Status:  Approved 2010-10-26
+BRFA:   Wikipedia:Bots/Requests for approval/AnomieBOT 70
+Status: Withdrawn
Created: 2010-10-12

Perform basic clerking tasks at [[WP:CHU/U]].

=end metadata

=cut

use utf8;
use strict;

use AnomieBOT::Task qw/:time/;
use Data::Dumper;
use vars qw/@ISA/;
@ISA=qw/AnomieBOT::Task/;

my @months=('','January','February','March','April','May','June','July','August','September','October','November','December');
my $monthsre=join('|',@months[1..12]); $monthsre=qr/$monthsre/;

my @skip_log_types=qw/block protect rights delete patrol suppress review stable gblblock renameuser globalauth gblrights abusefilter newusers/;
my $skip_log_types_re=join('|', @skip_log_types); $skip_log_types_re=qr/$skip_log_types_re/;

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

=pod

=for info
Approved 2010-10-26.<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 47]]

=for info
First supplemental BFRA withdrawn after the other bot was fixed<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 70]]

=cut

sub approved {
    return 2;
}

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

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

    my $screwup=' Errors? [[User:'.$api->user.'/shutoff/CHUUClerk]]';
    my $b0rken=0;
    my $re='^\s*('.join('|',@months[1..12]).')\s+(\d+)\s*$'; $re=qr/$re/;
    my $botname=$api->user;

    my $starttime=time();
    my $lastrun=$api->store->{'lastrun'} // (time()-86400*30);

    if(!exists($self->{'lastarchive'})){
        my $iter=$api->iterator(
            generator    => 'allpages',
            gapnamespace => 4,
            gapprefix    => 'Changing username/Usurpations/',
            gaplimit     => 'max',
        );
        while(my $p=$iter->next){
            if(!$p->{'_ok_'}){
                $api->warn("Could not retrieve pages from iterator: ".$p->{'error'}."\n");
                return 60;
            }
            next unless $p->{'title'}=~m!Wikipedia:Changing username/Usurpations/(.+)/(\d+)$!;
            $self->{'lastarchive'}{$1}=$2 if $2>($self->{'lastarchive'}{$1}//0);
        }
    }

    # Load list of bureaucrats
    my %renamers = ();
    my $iter = $api->iterator(
        list    => 'allusers',
        augroup => 'bureaucrat',
        aulimit => 'max',
    );
    while ( $_ = $iter->next ) {
        return 0 if $api->halting;
        if ( !$_->{'_ok_'} ) {
            $api->warn( "Failed to retrieve list of bureaucrats: " . $res->{'error'} . "\n" );
            return 60;
        }
        $renamers{$_->{'name'}} = 1;
    }
    # And global renamers on Meta
    $iter = $api->copy( wikibase => 'https://meta.wikimedia.org/w/', assert => 'user' )->iterator(
        list    => 'allusers',
        augroup => 'global-renamer',
        aulimit => 'max',
    );
    while ( $_ = $iter->next ) {
        return 0 if $api->halting;
        if ( !$_->{'_ok_'} ) {
            $api->warn( "Failed to retrieve list of global-renamers: " . $res->{'error'} . "\n" );
            return 60;
        }
        $renamers{$_->{'name'}} = 2;
    }
    # And stewards
    $iter = $api->iterator(
        list    => 'globalallusers',
        agugroup => 'steward',
        agulimit => 'max',
    );
    while ( $_ = $iter->next ) {
        return 0 if $api->halting;
        if ( !$_->{'_ok_'} ) {
            $api->warn( "Failed to retrieve list of stewards: " . $res->{'error'} . "\n" );
            return 60;
        }
        $renamers{$_->{'name'}} = 3;
    }
    my $renamerre = join( '|', map "(?i:" . quotemeta( substr( $_, 0, 1 ) ) . ")" . quotemeta( substr( $_, 1 ) ), sort keys %renamers );
    $renamerre=qr/$renamerre/;

    # Load list of recent renames by opted-in renamers
    $res=$api->rawpage("User:$botname/CHUUClerk closer opt-in");
    if($res->{'code'} ne 'success'){
        $api->warn("Failed to retrieve User:$botname/CHUUClerk closer opt-in: ".$res->{'error'}."\n");
        return 60;
    }
    $res->{'content'}=~s/\d{2}:\d{2}, \d+ $monthsre \d{4} \(UTC\)//g;
    my @lines=($res->{'content'}=~/\n\*\s*(.*?\[\[[ :]*(?i:User|User[_ ]+talk) *: *($renamerre) *[]|].*?)\s*?(?=\n|$)/g);
    my @renames=();
    for(my $i=0; $i<@lines; $i+=2){
        my $sig=$lines[$i];
        my $u=$lines[$i+1];
        my $rn=$api->query([],
            list    => 'logevents',
            letype  => 'renameuser',
            leuser  => $u,
            lestart => timestamp2ISO($lastrun-600),
            ledir   => 'newer',
            lelimit => 'max',
        );
        if($rn->{'code'} ne 'success'){
            $api->warn("Failed to retrieve rename logs for User:$u: ".$rn->{'error'}."\n");
            return 60;
        }
        foreach my $e (@{$rn->{'query'}{'logevents'}}) {
            next unless $e->{'action'} eq 'renameuser';
            my $ts=ISO2timestamp($e->{'timestamp'});
            my $wait=$ts+600-$starttime;
            if($wait>0){
                $api->log("Renames in progress! Waiting ${wait}s in case there are more.\n");
                return $wait;
            }
            my $n=$e->{'title'};
            $n=~s/^User://;
            $ts=strftime('%H:%M, %-d %B %Y (UTC)', gmtime $ts);
            push @renames, [$u, $sig, $ts, $e->{'params'}{'olduser'} // $n, $e->{'params'}{'newuser'}];
        }
    }

    {
        # Load WP:CHU/U
        $api->log("Checking WP:CHU/U");
        my $tok=$api->edittoken('Wikipedia:Changing username/Usurpations');
        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:Changing username/Usurpations: ".$tok->{'error'}."\n");
            return 60;
        }
        if(exists($tok->{'missing'})){
            $api->warn("Wikipedia:Changing username/Usurpations does not exist, WTF?");
            return 300;
        }

        # Split the page into sections, and process them
        my $intxt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'};
        $intxt=~s/\s*$//;
        my %archive=();
        my @sections=$api->split_sections($intxt, '34');
        my $header=undef;
        my %outsections=();
        my %allsections=();
        my ($archive,$dt);
        my $archivesoon=0;
        my ($notif,$esr,$cmt,$carc,$rarc)=(0,0,0,0,0);
        my %closefor=();
        foreach my $s (@sections){
            $archive=0;
            $dt=1e100;

            next unless(($s->{'level'}//0)==4);
            if($s->{'body'}=~/\d{2}:\d{2}, (\d+) ($monthsre) (\d{4}) \(UTC\)/){
                my $m=0;
                for(my $i=1; $i<@months; $i++){ $m=$i if $2 eq $months[$i]; }
                $dt=timegm(0,0,0,$1,$m-1,$3-1900);
            }

            my ($oldname,$newname)=('','');
            my ($ct,$sul,$clerked)=(0,0,0);
            my ($repl,$check_notified,$check_edited)=(undef,0,1);
            $api->process_templates($s->{'body'}, sub {
                my $name=shift;
                my $params=shift;
                my $wikitext=shift;

                if($name eq 'CUU' || $name eq 'Cuu'){
                    $clerked=1;
                    $repl=$wikitext;
                    foreach (@$params){
                        $check_notified=1 if /^\s*notified\s*=\s*no\s*$/;
                        $check_edited=0 if /^\s*edited since request\s*=\s*yes\s*$/;
                    }
                }
                $clerked=1 if $name eq 'Clerk note';

                if($name eq 'Usurp2'){
                    $ct++;
                    foreach ($api->process_paramlist(@$params)){
                        $_->{'name'}=~s/^\s+|\s+$//g;
                        $_->{'value'}=~s/[_\s]+/ /g;
                        $_->{'value'}=~s/^\s+|\s+$//g;
                        $oldname=ucfirst($_->{'value'}) if $_->{'name'} eq '1';
                        $newname=ucfirst($_->{'value'}) if $_->{'name'} eq '2';
                    }
                }

                if($name eq 'Usurp4'){
                    $ct++; $sul=1;
                    foreach ($api->process_paramlist(@$params)){
                        $_->{'name'}=~s/^\s+|\s+$//g;
                        $_->{'value'}=~s/[_\s]+/ /g;
                        $_->{'value'}=~s/^\s+|\s+$//g;
                        $newname=ucfirst($_->{'value'}) if $_->{'name'} eq '1';
                    }
                }

                return undef;
            });

            # Closed?
            if($s->{'body'}=~/\{\{(?i:Done)\}\}.*\[\[[ :]*(?i:User|User[_ ]+talk) *: *($renamerre) *[]|].*(\d{2}):(\d{2}), (\d+) ($monthsre) (\d{4}) \(UTC\)/){
                my $m=0;
                for(my $i=1; $i<@months; $i++){ $m=$i if $5 eq $months[$i]; }
                my $t=timegm(0,$3,$2,$4,$m-1,$6-1900);
                ($archive,$carc)=('Completed',$carc+1) if $t+3*86400 < time();
                $archivesoon=1 if(!$archive && $t+3*86400 < time()+3600);
                next;
            }
            if($s->{'body'}=~/\{\{(?i:Not[ _]?done)\}\}.*\[\[[ :]*(?i:User|User[ _]+talk) *: *($renamerre) *[]|].*(\d{2}):(\d{2}), (\d+) ($monthsre) (\d{4}) \(UTC\)/){
                my $m=0;
                for(my $i=1; $i<@months; $i++){ $m=$i if $5 eq $months[$i]; }
                my $t=timegm(0,$3,$2,$4,$m-1,$6-1900);
                ($archive,$rarc)=('Rejected',$rarc+1) if $t+2*86400 < time();
                $archivesoon=1 if(!$archive && $t+2*86400 < time()+3600);
                next;
            }

            # Auto-close?
            if($ct == 1){
                my @rn=();
                if($sul){
                    @rn=grep { $_->[3] eq $newname } @renames;
                } else {
                    @rn=grep { $_->[3] eq $oldname && $_->[4] eq $newname } @renames;
                }
                if(@rn){
                    my ($renamer,$sig,$ts,$on,$nn)=@{$rn[0]};
                    $s->{'body'}=~s/\{\{status(?:\s*\|.*?)?\s*\}\}/{{status|done}}/;
                    $s->{'body'}=~s/\s*$/\n: {{done}} $sig <small>([[User:$botname\/CHUUClerk closer opt-in|autosigned by $botname]])<\/small> $ts\n/;
                    $closefor{$renamer}++;
                    next;
                }
            }

            # Check notified, if necessary
            if($repl && $check_notified && $newname ne ''){
                $res=$api->query(titles=>"User talk:$newname",prop=>'revisions',rvprop=>'content',rvslots=>'main',rvlimit=>1,redirects=>1);
                if($res->{'code'} ne 'success'){
                    $api->warn("Failed to get page content for User talk:$newname: ".$res->{'error'}."\n");
                    return 60;
                }
                $res=(values %{$res->{'query'}{'pages'}})[0]{'revisions'}[0]{'slots'}{'main'}{'*'}//'';
                my $notified=0;
                if($oldname){
                    my $re='\[\[[ :]*(?i:User) *: *(?i:'.quotemeta(substr($oldname,0,1)).')'.quotemeta(substr($oldname,1)).' *[]|]'; $re=qr/$re/;
                    $notified=1 if($res=~/$re/ && $res=~/usurp/i);
                }
                $notified=1 if($res=~/Usurpation requested/i);
                if($notified){
                    my $x=$repl;
                    $x=~s/(\|\s*notified\s*=\s*)no(\s*(?=\||\x7d\x7d$))/${1}yes$2/g;
                    $s->{'body'}=~s/\Q$repl\E/$x/g;
                    $repl=$x;
                    $notif++;
                }
                $clerked=1;
            }

            # Check edited, if necessary
            if($repl && $check_edited && $newname ne ''){
                my $res=$api->query(list=>'usercontribs', ucend=>timestamp2ISO($dt), ucprop=>"title", ucuser=>$newname, uclimit=>1);
                if($res->{'code'} ne 'success'){
                    $api->warn("Failed to get contribs for $newname: ".$res->{'error'}."\n");
                    return 60;
                }
                if(@{$res->{'query'}{'usercontribs'}//[]}){
                    my $x=$repl;
                    unless($x=~s/(\|\s*edited since request\s*=\s*).*?(\s*(?=\||\x7d\x7d$))/${1}yes$2/gs) {
                        $x=~s/\x7d\x7d$/|edited since request=yes\x7d\x7d/;
                    }
                    $s->{'body'}=~s/\Q$repl\E/$x/g;
                    $repl=$x;
                    $esr++;
                }
                $clerked=1;
            }

            # Already clerked?
            next if $clerked;
            next if $s->{'body'}=~/ClueBot VI/; # Old clerk bot
            next if $s->{'body'}=~/\Q$botname\E/;

            # B0rken?
            next if $newname eq 'TARGET NAME';

            # Sanely parsable?
            if($ct<1){
                $s->{'body'}=~s/\s*$/\n:{{Clerk note}} This request does not contain {{tl|Usurp2}} or {{tl|Usurp4}}, and so cannot be automatically checked. ~~~~ <!-- Remove this whole line and $botname will check again. Leave this comment to prevent $botname from posting it again -->\n/;
                $cmt++;
                next;
            }
            if($ct>1){
                $s->{'body'}=~s/\s*$/\n:{{Clerk note}} This request has multiple instances of {{tl|Usurp2}} or {{tl|Usurp4}}, and so cannot be automatically checked. ~~~~ <!-- Remove this whole line and $botname will check again. Leave this comment to prevent $botname from posting it again -->\n/;
                $cmt++;
                next;
            }
            if($newname eq '' || (!$sul && $oldname eq '')){
                $s->{'body'}=~s/\s*$/\n:{{Clerk note}} This request does not have a valid {{tl|Usurp2}} or {{tl|Usurp4}} template, and so cannot be automatically checked. ~~~~ <!-- Remove this whole line and $botname will check again. Leave this comment to prevent $botname from posting it again -->\n/;
                $cmt++;
                next;
            }
            if(!$sul && $newname eq $oldname){
                $newname=~s/(no)(wiki)/$1<\/nowiki><nowiki>$2/ig;
                $oldname=~s/(no)(wiki)/$1<\/nowiki><nowiki>$2/ig;
                $s->{'body'}=~s/\s*$/\n:{{Clerk note}} The target username "<nowiki>$newname<\/nowiki>" is equivalent to the original username "<nowiki>$oldname<\/nowiki>". Please follow the instructions at the top of the page to correctly file your request. ~~~~ <!-- If removing the bot's message, leave this comment to prevent $botname from posting it again -->\n/;
                $cmt++;
                next;
            }

            # Gather user statistics
            my $uu=$sul?$newname:"$newname|$oldname";
            $res=$api->query(
                list    => 'users',
                ususers => $uu,
                usprop  => 'editcount|registration|emailable|blockinfo',
                meta    => 'globaluserinfo',
                guiuser => $newname,
            );
            if($res->{'code'} ne 'success'){
                $api->warn("Could not load user info for $uu: ".$res->{'error'}."\n");
                next;
            }
            my %oldinfo=(ok=>0);
            my %newinfo=(ok=>0);
            foreach my $u (@{$res->{'query'}{'users'}}){
                my $x=undef;
                $x=\%oldinfo if $u->{'name'} eq ($oldname//'/');
                $x=\%newinfo if $u->{'name'} eq ($newname//'/');
                next unless $x;
                $x->{'invalid'}=exists($u->{'invalid'});
                $x->{'missing'}=exists($u->{'missing'});
                $x->{'ok'}=!($x->{'missing'} || $x->{'invalid'});
                if($x->{'ok'}){
                    $x->{'emailable'}=exists($u->{'emailable'});
                    $x->{'editcount'}=$u->{'editcount'} // 0;
                    $x->{'registration'}=ISO2timestamp($u->{'registration'}) // 0;
                    $x->{'blocked'}='';
                    $x->{'blocked'}.=" by [[User:$u->{blockedby}|]]" if exists($u->{'blockedby'});
                    $x->{'blocked'}.=" for <nowiki>$u->{blockreason}</nowiki>" if exists($u->{'blockreason'});
                }
            }
            if($newinfo{'invalid'}){
                $newname=~s/(no)(wiki)/$1<\/nowiki><nowiki>$2/ig;
                $s->{'body'}=~s/\s*$/\n:{{Clerk note}} The requested username "<nowiki>$newname<\/nowiki>" is not a valid username. ~~~~ <!-- If removing the bot's message, leave this comment to prevent $botname from posting it again -->\n/;
                $cmt++;
                next;
            }
            if($newinfo{'missing'}){
                if(exists($res->{'query'}{'globaluserinfo'}) && !exists($res->{'query'}{'globaluserinfo'}{'missing'})){
                    $s->{'body'}=~s/\s*$/\n:{{Clerk note}} The requested username "$newname" is not registered locally, but is reserved for a global account. If you own the global account, please use that account to comment here explicitly consenting to the usurpation, or link to such a comment made elsewhere. If you do not own the global account, this may preclude renaming. Requests for usurpation of global usernames with significant attachments on other projects must be made at [[meta:Steward requests\/Username changes]]. ~~~~ <!-- If removing the bot's message, leave this comment to prevent $botname from posting it again -->\n/;
                } else {
                    $s->{'body'}=~s/\s*$/\n:{{Clerk note}} The requested username "$newname" is not registered. Please see [[WP:CHU\/S]] instead. ~~~~ <!-- If removing the bot's message, leave this comment to prevent $botname from posting it again -->\n/;
                }
                $cmt++;
                next;
            }
            if(!$newinfo{'ok'}){
                my $t=$s->{'title'};
                $t=~s/\[\[([^\x5d|]*)\]\]/$1/g;
                $t=~s/\[\[[^\x5d|]*\|([^\x5d]*?)\]\]/$1/g;
                $api->whine("Cannot find info for username in  [[WP:CHUU#$t]]", "In [[WP:CHUU#$t]], I cannot load information for the target username. Please fix me.");
                $s->{'body'}=~s/\s*$/\n:{{Clerk note}} I cannot find the target username, so this request cannot be automatically checked. I have asked my operator to fix me. ~~~~ <!-- Remove this whole line and $botname will check again. Leave this comment to prevent $botname from posting it again -->\n/;
                $cmt++;
                next;
            }

            if(!$sul && !$oldinfo{'ok'}){
                if($oldinfo{'invalid'}){
                    $oldname=~s/(no)(wiki)/$1<\/nowiki><nowiki>$2/ig;
                    $oldname="<nowiki>$oldname</nowiki>";
                    $s->{'body'}=~s/\s*$/\n:{{Clerk note}} The listed current username "<nowiki>$oldname<\/nowiki>" is not a valid username. ~~~~ <!-- If removing the bot's message, leave this comment to prevent $botname from posting it again -->\n/;
                } elsif($oldinfo{'missing'}){
                    $s->{'body'}=~s/\s*$/\n:{{Clerk note}} The listed current username "$oldname" is not registered. ~~~~ <!-- If removing the bot's message, leave this comment to prevent $botname from posting it again -->\n/;
                } else {
                    $api->whine("Cannot find info for username in  [[WP:CHUU#".$s->{'title'}."]]", "In [[WP:CHUU#".$s->{'title'}."]], I cannot load information for the requesting username. Please fix me.");
                    $s->{'body'}=~s/\s*$/\n:{{Clerk note}} I cannot find the listed current username, so this request cannot be automatically checked. I have asked my operator to fix me. ~~~~ <!-- Remove this whole line and $botname will check again. Leave this comment to prevent $botname from posting it again -->\n/;
                }
                $cmt++;
                next;
            }

            next unless load_info($api, $newname, \%newinfo, $dt);
            if($oldinfo{'ok'}){
                next unless load_info($api, $oldname, \%oldinfo, $dt);
            }

            my $notified=0;
            my $notify=1;

            my $tok2=$api->edittoken("User talk:$newname");
            if($tok2->{'code'} eq 'shutoff'){
                $api->warn("Task disabled: ".$tok2->{'content'}."\n");
                return 300;
            }
            if($tok2->{'code'} eq 'pageprotected' || $tok2->{'code'} eq 'botexcluded'){
                # Cannot notify, don't worry about it
                $notify=0;
                $res=$api->query(titles=>"User talk:$newname",prop=>'revisions',rvprop=>'content',rvslots=>'main',rvlimit=>1,redirects=>1);
                if($res->{'code'} ne 'success'){
                    $api->warn("Failed to get page content for User talk:$newname: ".$res->{'error'}."\n");
                    return 60;
                }
                $tok2=(values %{$res->{'query'}{'pages'}})[0];
            } elsif($tok2->{'code'} ne 'success'){
                $api->warn("Failed to get edit token for User talk:$newname: ".$tok2->{'error'}."\n");
                return 60;
            }
            if($oldinfo{'ok'}){
                my $re='\[\[[ :]*(?i:User) *: *(?i:'.quotemeta(substr($oldname,0,1)).')'.quotemeta(substr($oldname,1)).' *[]|]'; $re=qr/$re/;
                $notified=1 if(($tok2->{'revisions'}[0]{'slots'}{'main'}{'*'}//'')=~/$re/);
            }
            $notified=1 if(($tok2->{'revisions'}[0]{'slots'}{'main'}{'*'}//'')=~/Usurpation requested/i);

            # Should the bot notify the target account? Be conservative to avoid
            # auto-notifying on requests that may not succeed anyway.
            $notify=0 if $notified;
            $notify=0 if $newinfo{'visiblecontribs'}!=0;
            $notify=0 if $newinfo{'logs'}!=0;
            if($sul){
                $notify=0 if(!$newinfo{'sul-exists'} || $newinfo{'sul-owned'});
            } else {
                $notify=0 if($oldinfo{'new'} && ($oldinfo{'sul-new'}//1));
                $notify=0 if(($oldinfo{'sul-other-edits'}//0)+$oldinfo{'editcount'}<200);
                $notify=0 if $newinfo{'sul-exists'};
            }
            $notify=0 if $newinfo{'blocked'} ne '';
            if($notify){
                my $txt=$tok2->{'revisions'}[0]{'slots'}{'main'}{'*'}//'';
                $txt=~s/\s*$//;
                $txt.="\n\n{{subst:Usurpation requested}} <small>This notification has been automatically delivered by a robot. Please comment at the Request for Usurpation page.</small> ~~~~";
                $txt=~s/^\s*//;
                $api->log("Notifying $newname of request for usurpation");
                $res=$api->edit($tok2, $txt, "Notification of request for usurpation", 0, 0);
                if($res->{'code'} ne 'success'){
                    $api->warn("Failed to edit User talk:$newname: ".$res->{'error'}."\n");
                    return 60;
                }
                $notified=1;
            }

            my $t=":\x7b\x7bCUU";
            $t.='|contribs='.($newinfo{'visiblecontribs'}>0?'yes':'no');
            $t.='|dcontribs='.($newinfo{'deletedcontribs'}?'yes':'no');
            $t.='|logs='.($newinfo{'logs'}?'yes':'no');
            $t.='|new='.(($newinfo{'registration'}>time()-6*30*86400 && $newinfo{'registration'}<time()-3600)?'yes':'no');
            $t.='|notified='.($notified?'yes':'no');
            $t.='|email='.($newinfo{'emailable'}?'yes':'no');
            $t.='|interwiki='.$newinfo{'sul-most-other-edits'} if($newinfo{'sul-other-edits'}//0);
            $t.='|sul-primary='.$newinfo{'sul-primary'} if exists( $newinfo{'sul-primary'} );
            $t.='|sul-other-edits='.($newinfo{'sul-other-edits'} // 0);
            $t.="|blockinfo=$newinfo{blocked}" if $newinfo{'blocked'} ne '';
            $t.="|edited since request=yes" if $newinfo{'editedsincedt'};
            $t.='|note=';
            $t.="Target user has {{subst:plural|".$newinfo{'visiblecontribs'}."|undeleted edit}} and {{subst:plural|".$newinfo{'deletedcontribs'}."|deleted edit}}";
            $t.=", for a total of {{subst:plural|".$newinfo{'editcount'}."|edit}}"
                    if($newinfo{'visiblecontribs'}!=0 && $newinfo{'deletedcontribs'}!=0);
            $t.=".";
            if($newinfo{'sul-exists'}){
                $t.=" An SUL account (primary ".$newinfo{'sul-primary'}.") exists for the target user, with {{subst:plural|".$newinfo{'sul-edits'}."|total edit}} on {{subst:plural|".$newinfo{'sul-count'}."|wiki}}.";
                $t.=" The local account is NOT attached." unless $newinfo{'sul-owned'};
            } else {
                $t.=" No SUL account exists for the target user.";
            }
            if($newinfo{'sul-unattached'}){
                $t.=" ".$newinfo{'sul-unattached'}.(($newinfo{'sul-owned'}//0)?"":" non-enwiki")." unattached {{subst:plural:".$newinfo{'sul-unattached'}."|account exists|accounts exist}} with this name, with {{subst:plural|".$newinfo{'sul-unattached-edits'}."|total edit}}.";
            }

            if($oldinfo{'ok'}){
                if($oldinfo{'deletedcontribs'}!=0){
                    if($oldinfo{'visiblecontribs'}){
                        $t.=" Requesting user has {{subst:plural|".$oldinfo{'visiblecontribs'}."|undeleted edit}} and {{subst:plural|".$oldinfo{'deletedcontribs'}."|deleted edit}}, for a total of {{subst:plural|".$oldinfo{'editcount'}."|edit}}.";
                    } else {
                        $t.=" Requesting user has {{subst:plural|".$oldinfo{'editcount'}."|deleted edit}}.";
                    }
                } else {
                    $t.=" Requesting user has {{subst:plural|".$oldinfo{'editcount'}."|edit}}.";
                }
                my $show_unattached=1;
                if($oldinfo{'sul-exists'}){
                    if($oldinfo{'sul-owned'}){
                        $t.=" An SUL account (primary ".$oldinfo{'sul-primary'}.") exists for the requesting user, with {{subst:plural|".$oldinfo{'sul-edits'}."|total edit}} on {{subst:plural|".$oldinfo{'sul-count'}."|wiki}}.";
                        $show_unattached=0;
                    } else {
                        $t.=" An SUL account (primary ".$oldinfo{'sul-primary'}.") exists for the requesting user's name, but the local account is NOT attached.";
                    }
                }
                if($show_unattached && $oldinfo{'sul-unattached'}){
                    $t.=" ".$oldinfo{'sul-unattached'}.(($oldinfo{'sul-owned'}//0)?"":" non-enwiki")." unattached {{subst:plural:".$oldinfo{'sul-unattached'}."|account exists|accounts exist}} with the requesting user's name, with {{subst:plural|".$oldinfo{'sul-unattached-edits'}."|total edit}}.";
                }
            }
            $t.="\x7d\x7d ~~~~ <!-- If removing the bot's message, leave this comment to prevent $botname from posting it again -->\n";
            $s->{'body'}=~s/\s*$/\n$t/;
            $cmt++;
        } continue {
            push @{$allsections{$dt}}, $s if(($s->{'level'}//0)==4);
            if($archive){
                push @{$archive{$archive}}, $api->join_sections($s);
            } else {
                # Copy section to output
                $header=$s unless defined($s->{'level'});
                push @{$outsections{$dt}}, $s if(($s->{'level'}//0)==4);
            }
        }

        # Reconstruct the page, and save if necessary
        my $outtxt=merge_page($api, $header, %outsections);

        my ($intmp,$outtmp)=($intxt,$outtxt);
        $intmp=~s/\n+/\n/g;
        $outtmp=~s/\n+/\n/g;
        my $alltmp=merge_page($api, $header, %allsections);
        $alltmp=~s/\n+/\n/g;

        if($alltmp eq $intmp && $archivesoon){
            # Only archiving and more will be archived soon, so wait
            $api->log("More requests will be archived soon, waiting");
            $outtmp = $intmp;
        }

        if($intmp ne $outtmp){
            my @summary=();
            push @summary, 'Clerking: Format page';
            push @summary, "add $cmt clerk notice".($cmt==1?'':'s') if $cmt;
            push @summary, "mark $notif request".($notif==1?'':'s')." notified" if $notif;
            push @summary, "mark $esr request".($esr==1?'':'s')." 'user edited'" if $esr;
            push @summary, "archive $carc completed request".($carc==1?'':'s') if $carc;
            push @summary, "archive $rarc rejected request".($rarc==1?'':'s') if $rarc;
            while(my ($renamer,$ct)=each %closefor){
                push @summary, "mark $ct request".($ct==1?'':'s')." completed by $renamer";
            }
            $summary[$#summary]='and '.$summary[$#summary] if @summary>1;
            my $summary=join(@summary>2?', ':' ', @summary);
            $api->log("$summary in $tok->{title}");

            my $r=$api->edit($tok, $outtxt, $summary.$screwup, 0, 1);
            if($r->{'code'} ne 'success'){
                $api->warn("Write failed on $tok->{title}: ".$r->{'error'}."\n");
                return 60;
            }
            if(%archive){
                # Save succeeded, so save the sections-to-archive to the
                # bot's store.
                my $a=$api->store->{'archive'} // {};
                for my $k (keys %archive){
                    push @{$a->{$k}}, @{$archive{$k}};
                }
                $api->store->{'archive'}=$a;
            }
        }

        # Pull sections-to-archive from the bot's store, and write them to the
        # appropriate archive pages.
        my $a=$api->store->{'archive'} // {};
        foreach my $k (keys %$a){
            my @a=@{$a->{$k}};
            while(1){
                my $t="Wikipedia:Changing username/Usurpations/$k/".$self->{'lastarchive'}{$k};
                $tok=$api->edittoken($t);
                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 $t: ".$tok->{'error'}."\n");
                    return 60;
                }
                my $ct=0;
                my $txt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'} // "{{archivemainpage|WP:CHU/U}}\n\n";
                while(@a && length($txt)<=100000){
                    $ct++;
                    $txt=~s/\s*$//;
                    $txt.="\n\n".shift @a;
                }
                if($ct){
                    my $summary="Archiving $ct request".($ct==1?'':'s')." from [[WP:CHU/U]]";
                    $api->log("$summary to $t");
                    $res=$api->edit($tok, $txt, $summary.$screwup, 0, 1);
                    if($res->{'code'} ne 'success'){
                        $api->warn("Write failed on $t: ".$res->{'error'}."\n");
                        return 60;
                    }
                }

                $a->{$k}=\@a;
                $api->store->{'archive'}=$a;
                last unless @a;
                $self->{'lastarchive'}{$k}++;
            }
        }
    }

    $api->store->{'lastrun'}=$starttime;

    return 600;
}

sub load_info {
    my ($api, $name, $x, $dt)=@_;


    my %q=(
        list    => 'logevents',
        leuser  => $name,
        leprop  => 'ids|type|timestamp',
        lelimit => 'max',
        meta    => 'globaluserinfo',
        guiuser => $name,
        guiprop => 'merged|unattached',
    );
    my $res=$api->query([],%q);
    if($res->{'code'} ne 'success'){
        $api->warn("Could not load global info and logs for user $name: ".$res->{'error'}."\n");
        return 0;
    }
    $x->{'logs'}=scalar grep $_->{'type'}!~/^$skip_log_types_re$/, @{$res->{'query'}{'logevents'}};

    $x->{'sul-exists'}=!exists($res->{'query'}{'globaluserinfo'}{'missing'});
    $x->{'sul-other-edits'}=0;
    my $oe=0;
    $x->{'sul-most-other-edits'}='';
    if($x->{'sul-exists'}){
        $x->{'sul-registration'}=ISO2timestamp($res->{'query'}{'globaluserinfo'}{'registration'})//0;
        $x->{'sul-count'}=scalar @{$res->{'query'}{'globaluserinfo'}{'merged'}};
        $x->{'sul-owned'}=grep $_->{'wiki'} eq 'enwiki', @{$res->{'query'}{'globaluserinfo'}{'merged'}};
        my @p=grep(($_->{'method'} eq 'new' || $_->{'method'} eq 'primary'), @{$res->{'query'}{'globaluserinfo'}{'merged'}});
        $x->{'sul-primary'}=@p?join('/',map $_->{'wiki'}, @p):'unknown';
        $x->{'sul-edits'}=0;
        foreach (@{$res->{'query'}{'globaluserinfo'}{'merged'}}){
            $x->{'sul-edits'}+=$_->{'editcount'};
            $x->{'sul-other-edits'}+=$_->{'editcount'} unless $_->{'wiki'} eq 'enwiki';
            ($x->{'sul-most-other-edits'},$oe)=($_->{'wiki'},$_->{'editcount'}) if($_->{'wiki'} ne 'enwiki' && $_->{'editcount'}>$oe);
        }
        $x->{'sul-new'}=($x->{'sul-registration'}>time()-6*30*86400 && $x->{'sul-registration'}<time()-3600);
    }
    $x->{'sul-unattached'}=0;
    $x->{'sul-unattached-edits'}=0;
    foreach (@{$res->{'query'}{'globaluserinfo'}{'unattached'}}) {
        next if $_->{'wiki'} eq 'enwiki';
        $x->{'sul-unattached'}++;
        $x->{'sul-unattached-edits'}+=$_->{'editcount'};
        $x->{'sul-other-edits'}+=$_->{'editcount'};
        ($x->{'sul-most-other-edits'},$oe)=($_->{'wiki'},$_->{'editcount'}) if $_->{'editcount'}>$oe;
    }

    # The "editcount" field in list=users gives the total of edits (including
    # deleted contribs) but not including the "fake" revisions created by
    # various log actions. So we can theoretically calculate the number of
    # deleted contribs by counting the visible contribs that don't correspond
    # to one of these "fake" revisions, and subtracting.
    my %le=map { $_->{'timestamp'} => 1 } grep "$_->{type}--$_->{action}"!~/^(?:review--.*|patrol--.*|upload--upload)$/, @{$res->{'query'}{'logevents'}};
    $res=$api->query([],
        list    => 'usercontribs',
        ucuser  => $name,
        ucprop  => 'timestamp',
        uclimit => 'max',
    );
    if($res->{'code'} ne 'success'){
        $api->warn("Could not load contribs for user $name: ".$res->{'error'}."\n");
        return 0;
    }
    $x->{'visiblecontribs'}=scalar grep !exists($le{$_->{'timestamp'}}), @{$res->{'query'}{'usercontribs'}};
    $x->{'deletedcontribs'}=$x->{'editcount'}-$x->{'visiblecontribs'};
    $x->{'editedsincedt'}=scalar grep ISO2timestamp($_->{'timestamp'})>=$dt, @{$res->{'query'}{'usercontribs'}};

    $x->{'new'}=($x->{'registration'}>time()-6*30*86400 && $x->{'registration'}<time()-3600);

    return 1;
}

sub merge_page {
    my ($api, $header, %sections)=@_;

    my @s=($header);
    foreach my $t (sort { $a<=>$b } keys %sections) {
        if($t==0){
            # CHU/S, no subheader
        } elsif($t==1e100){
            push @s, {
                level => 3,
                title => 'Unknown',
                titlespaced => 1,
                body => '',
            };
        } else {
            push @s, {
                level => 3,
                title => strftime('%B %-d, %Y', gmtime $t),
                titlespaced => 1,
                body => "''Requests left here should be addressed on or after ".strftime('%B %-d, %Y', gmtime $t+8*86400).".''\n\n",
            };
        }
        push @s, @{$sections{$t}};
    }
    my $outtxt=$api->join_sections(@s);
    $outtxt=~s/\s*$//;
    return $outtxt;
}

1;