User:AnomieBOT/source/tasks/DeletionSortingCleaner.pm

Source: Wikipedia, the free encyclopedia.
package tasks::DeletionSortingCleaner;

=pod

=begin metadata

Bot:     AnomieBOT
Task:    DeletionSortingCleaner
BRFA:    Wikipedia:Bots/Requests for approval/AnomieBOT 40
Status:  Approved 2010-07-06
+BRFA:   Wikipedia:Bots/Requests for approval/AnomieBOT 79
+Status: Approved 2020-06-06
Created: 2010-06-18

Perform certain tasks for [[WP:WikiProject Deletion sorting]]:
* Subst various AfD templates that should be substed
* Archive discussions for closed XfDs
* Remove duplicate XfD listings

If necessary, the bot may be kept off a deletion sorting subpage by adding
{{[[Template:bots|bots]]|optout=AnomieBOT/DeletionSortingCleaner}} to that page.

=end metadata

=cut

use utf8;
use strict;

use AnomieBOT::Task;
use URI::Escape;
use Data::Dumper;
use POSIX;
use vars qw/@ISA/;
@ISA=qw/AnomieBOT::Task/;

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

=pod

=for info
Approved 2010-07-06<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 40]]

=for info
Supplemental BFRA approved 2020-06-06<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 79]]

=cut

sub approved {
    return 3;
}

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

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

    # Get all redirects to templates we need to subst in XfD pages
    my %xfdtemplates=$api->redirects_to_resolved('Template:At','Template:Afd top','Template:Afd bottom', 'Template:Afd-privacy');
    if(exists($xfdtemplates{''})){
        $api->warn("Failed to get list of XfD templates: ".$xfdtemplates{''}{'error'}."\n");
        return 60;
    }

    # Only check twice per day
    if($self->{'lasttime'}==0){
        if(exists($api->store->{'lasttime'})){
            my $t=$api->store->{'lasttime'};
            $self->{'lasttime'}=$t if($t=~/^\d+$/ && $t<=time());
        }
        $self->{'broken'}=$api->store->{'broken'} if(exists($api->store->{'broken'}));
    }
    my $starttime=time();
    my $t=$self->{'lasttime'}+($self->{'broken'}?3600:43200)-$starttime;
    return $t if $t>0;

    my $screwup=' ([[User:'.$api->user.'/shutoff/DeletionSortingCleaner|errors?]])';
    my $broken=0;

    # Load list of deletion sorting subpages to process
    $res=$api->query(titles=>'Wikipedia:WikiProject Deletion sorting/Compact',prop=>'links',plnamespace=>4,pllimit=>'max');
    if($res->{'code'} ne 'success'){
        $api->warn("Failed to get list of pages to process: ".$res->{'error'}."\n");
        return 60;
    }
    my @pages=sort grep m!^Wikipedia:WikiProject Deletion sorting/!, map $_->{'title'}, @{(values %{$res->{'query'}{'pages'}})[0]{'links'}};
    unless(@pages){
        $api->warn("No pages in list?");
        $self->{'broken'}=1;
        $api->store->{'broken'}=1;
        return 3600;
    }

    for my $page (@pages){
        # First, load the page to archive from. Allow for opting out using
        # {{bots|optout=AnomieBOT/DeletionSortingCleaner}}
        my $tok=$api->edittoken($page, OptOut=>$api->user.'/DeletionSortingCleaner');
        if($tok->{'code'} eq 'shutoff'){
            $api->warn("Task disabled: ".$tok->{'content'}."\n");
            return 300;
        }
        if($tok->{'code'} eq 'botexcluded'){
            $api->warn("Bot excluded from $page: ".$tok->{'error'}."\n") unless $tok->{'type'} eq 'optout';
            next;
        }
        if($tok->{'code'} ne 'success'){
            $api->warn("Failed to get edit token for $page: ".$tok->{'error'}."\n");
            $broken=1;
            next;
        }
        next if exists($tok->{'missing'});

        # Go through all templates in the page looking for transclusions of XfD
        # pages. For each one, subst any substable templates and then see if it
        # looks closed.
        my $intxt=$tok->{'revisions'}[0]{'slots'}{'main'}{'*'};
        my @archive=@{$api->store->{"archive $page"} // []}; # Load saved archivals
        my @summary=();
        my $fail=undef;
        my $dups=0;
        my %dups=();
        my $outtxt=$api->process_templates($intxt, sub {
            return undef if defined($fail);
            my $name=shift;

            return undef unless $name=~m!^(?i:Wikipedia|WP) *: *((?:[Aa]rticles|[Mm]iscellany) for deletion)/(.+)$!;
            my $name2=$2;

            # Normalize. People do weird things sometimes.
            $name = "Wikipedia:\u$1/$2";

            if(exists($dups{$name})){
                $dups=1;
                return '';
            }
            $dups{$name}=1;

            my $cannoteditreason=undef;
            REDO:
            my $xfdtok=$api->edittoken($name);
            if($xfdtok->{'code'} eq 'shutoff'){
                $api->warn("Task disabled: ".$xfdtok->{'content'}."\n");
                $fail=300;
                return undef;
            }
            if($xfdtok->{'code'} eq 'pageprotected'){
                $cannoteditreason=$xfdtok->{'error'};
                my $res=$api->query(
                    prop      => 'revisions',
                    titles    => $name,
                    rvprop    => 'ids|timestamp|content|flags|user|size|comment',
                    rvslots   => 'main',
                );
                $xfdtok=(values %{$res->{'query'}{'pages'}})[0];
                $xfdtok->{'code'}=$res->{'code'};
                $xfdtok->{'error'}=$res->{'error'};
            }
            if($xfdtok->{'code'} ne 'success'){
                # Don't worry about this error, just assume the discussion is
                # not closed and retry next time around.
                $api->warn("Failed to get edit token for $name: ".$xfdtok->{'error'}."\n");
                $broken=1;
                return undef;
            }
            if(exists($xfdtok->{'missing'})){
                # WTF?
                $api->warn("XfD page $name linked from $page does not exist\n");
                return undef;
            }

            my %substlist=();
            my $xfdintxt=$xfdtok->{'revisions'}[0]{'slots'}{'main'}{'*'};
            my $xfdouttxt=$api->process_templates($xfdintxt, sub {
                my $name=shift;
                shift; # $params
                my $wikitext=shift;

                return undef unless exists($xfdtemplates{"Template:$name"});
                $substlist{$name}=1;
                $wikitext=~s/^\{\{/{{subst:/; # }}
                return $wikitext;
            });
            if($xfdintxt ne $xfdouttxt){
                # We found templates to subst, so save the changed page and
                # then reload it.
                if(defined($cannoteditreason)){
                    $api->log("Editprotected to subst templates in $name");
                    my $talk=$name; $talk=~s/:/ talk:/;
                    my @t=map "{{[[Template:$_|$_]]}}", keys %substlist;
                    $t[$#t]='and '.$t[$#t] if @t>1;
                    my $t=join((@t>2)?', ':' ', @t);
                    my $s=(@t==1)?'':'s';
                    my $res=$api->whine("Editprotected request: please subst $t", "{{editprotected}} The template$s $t in [[$name|this page]] should be substed, but I cannot do so because \l$cannoteditreason. Please do so. Thanks.", Summary=>"[BOT] Editprotected request: please subst $t", Pagename=>$talk);
                    if($res->{'code'} ne 'success'){
                        # Don't worry about this error, just assume the
                        # discussion is not closed and retry next time around.
                        $api->warn("Editprotected request failed for $name: ".$res->{'error'}."\n");
                        $broken=1;
                        return undef;
                    }
                } else {
                    $api->log("Substing templates in $name");
                    my $res=$api->edit($xfdtok, $xfdouttxt, "[[Wikipedia:Template substitution|substituting]] closure templates".$screwup, 1, 1);
                    if($res->{'code'} ne 'success'){
                        # Don't worry about this error, just assume the
                        # discussion is not closed and retry next time around.
                        $api->warn("Save failed for $name: ".$res->{'error'}."\n");
                        $broken=1;
                        return undef;
                    }
                    goto REDO;
                }
            }

            # If closed, remove from the main page and note it for archival.
            # If still open, do nothing.
            return undef unless $xfdintxt=~m!<div class="[^"]*(?<=[" ])[xt]fd-closed[ "]!;
            my ($result,$date)=('(unknown)','(unknown)');
            $result=$1 if $xfdintxt=~m!(?:result was|result of the discussion was:(?:'')?)\s*(?:'''|<(?:b|strong)>)(.+?)(?:'''|</(?:b|strong)>)!;
            $date=$1 if $xfdintxt=~m!(\d\d:\d\d, \d+ \w+ \d{4} \(UTC\))!;
            unshift @archive, "* [[$name|$name2]] - (".length($xfdintxt).") - $result - <small>closed $date</small>";
            push @summary, "[[$name]]";
            return '';
        });
        return $fail if defined($fail);

        # Calculate the changes needed to the archive page, if any. We do this
        # before saving the original page to minimize chances of being able to
        # save one but not the other.
        my ($apage,$atok,$atxt)=(undef,undef,undef);
        if(@archive){
            my ($i, $sz) = (1, 0);
            ($apage = "$page/archive") =~ s/^Wikipedia://;
            $res = $api->query(
                generator => 'allpages',
                gapnamespace => 4,
                gapprefix => $apage,
                gaplimit => 'max',
                prop => 'info',
            );
            for my $p (values %{$res->{'query'}{'pages'}}) {
                if ( $p->{'title'} eq "$page/archive" && $i <= 1) {
                    ($i,$sz) = (1,$p->{'length'});
                } elsif ( $p->{'title'} =~ /^\Q$page\/archive\E (\d+)$/ && $i <= $1 ) {
                    ($i,$sz) = ($1,$p->{'length'});
                }
            }
            $i++ if $sz > 1048576;
            $apage = $i < 2 ? "$page/archive" : "$page/archive $i";

            $atok=$api->edittoken($apage);
            if($atok->{'code'} eq 'shutoff'){
                $api->warn("Task disabled: ".$atok->{'content'}."\n");
                return 300;
            }
            if($atok->{'code'} ne 'success'){
                $api->warn("Failed to get edit token for $apage: ".$atok->{'error'}."\n");
                $broken=1;
                next;
            }
            my $n=$page; $n=~s!^[^/]*/!!;
            my $aintxt=$atok->{'revisions'}[0]{'slots'}{'main'}{'*'} // '';
            $atxt=$aintxt;
            if(exists($atok->{'missing'})){
                # Doesn't exist, create boilerplate
                $atxt="<noinclude>{{deletionlistarchive|$n}}</noinclude>\n\n==$n==\n\n===Articles for Deletion===\n<!-- add old AfD discussions at the top -->\n\n<!-- end of old AfD discussions -->";
            }
            my $a=join("\n",@archive);
            $atxt=~s/<!-- add old AfD discussions at the top -->/<!-- add old AfD discussions at the top -->\n$a/;
            if($aintxt eq $atxt){
                $api->whine("Broken deletion sorting archive page for $n", "The deletion sorting archive page [[$apage]] is lacking the marker <code><nowiki><!-- add old AfD discussions at the top --></nowiki></code>, which is needed for me to know where to put the archived AfDs. I can't do anything to that page until someone fixes it.");
                $broken=1;
                next;
            }
        }

        # Now do the saving
        if($outtxt ne $intxt){
            $api->log("Archiving closed XfDs and/or removing duplicates from $page...");
            my $summary;
            if(@summary){
                $summary="[[$apage|Archiving closed XfDs]]" . ( $dups ? ' and removing duplicate XfDs' : '' ) . $screwup . ": " . join(" ", @summary);
                $summary="[[$apage|Archiving closed XfDs]]" . ( $dups ? ' and removing duplicate XfDs' : '' ) . $screwup . ": [" . scalar(@summary) . " discussions]" if length($summary)>500;
            } else {
                $summary = "Removing duplicate XfDs" . $screwup;
            }
            $res=$api->edit($tok, $outtxt, $summary, 0, 1);
            if($res->{'code'} ne 'success'){
                $api->warn("Save failed for $page: ".$res->{'error'}."\n");
                $broken=1;
                next;
            }
            # Now that we saved the original page, we must save the archival
            # records just in case the next edit fails.
            $api->store->{"archive $page"}=[@archive];
        }
        if(defined($atok)){
            $api->log("Archiving closed XfDs to $apage...");
            $res=$api->edit($atok, $atxt, "Archiving closed XfDs from [[$page]]".$screwup, 0, 1);
            if($res->{'code'} ne 'success'){
                $api->warn("Save failed for $apage".$res->{'error'}."\n");
                $broken=1;
                next;
            }
            # Now that we saved the archival page, clear the saved value.
            delete $api->store->{"archive $page"};
        }
    }

    # Save checked revision
    $self->{'lasttime'}=$starttime;
    $self->{'broken'}=$broken;
    $api->store->{'lasttime'}=$starttime;
    $api->store->{'broken'}=$broken;
    return $starttime+($broken?3600:43200)-time();
}

1;