User:AnomieBOT/source/tasks/DeletionSortingCleaner.pm

This is an old revision of this page, as edited by AnomieBOT (talk | contribs) at 20:23, 18 June 2010 (Updating published sources: AnomieBOT::API: * When an edit token request fails for botexcluded, return an additional property indicating why. * Load the list of available props from the server (for the "additional data" bit of edittoken), instead of). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
(diff) ← Previous revision | Latest revision (diff) | Newer revision → (diff)
package tasks::DeletionSortingCleaner;

=pod

=begin metadata

Bot:     AnomieBOT
Task:    DeletionSortingCleaner
BRFA:    Wikipedia:Bots/Requests for approval/AnomieBOT 40
Status:  BRFA
Rate:    Max 6 edits/minute
Created: 2010-06-18

Perform certain tasks for [[WP:WikiProject Deletion sorting]]:
* Subst various AfD templates that should be substed
* Archive discussions for closed AfDs

If necessary, the bot may be kept off a deletion sorting subpage by adding
{{tl|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
Approval requested<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 40]]

=cut

sub approved {
    return 0;
}

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('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=' Errors? [[User:'.$api->user.'/shutoff/DeletionSortingCleaner]]';
    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.
        $api->log("Processing $page...");
        my $intxt=$tok->{'revisions'}[0]{'*'};
        my @archive=@{$api->store->{"archive $page"} // []}; # Load saved archivals
        my $fail=undef;
        my $outtxt=$api->process_templates($intxt, sub {
            return undef if defined($fail);
            my $name=shift;

            return undef unless $name=~m!^Wikipedia:Articles for deletion/(.+)$!;
            my $name2=$1;

            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'} 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;
            }

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

                return undef unless exists($xfdtemplates{"Template:$name"});
                $wikitext=~s/^{{/{{subst:/; # }} }}
                return $wikitext;
            });
            if($xfdintxt ne $xfdouttxt){
                # We found templates to subst, so save the changed page and
                # then reload it.
                $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="boilerplate metadata!;
            my ($result,$date)=('(unknown)','(unknown)');
            $result=$1 if $xfdintxt=~m!result was\s*'''(.+?)'''!;
            $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>";
            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 ($atok,$atxt)=(undef,undef);
        if(@archive){
            $atok=$api->edittoken("$page/archive");
            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 $page/archive: ".$atok->{'error'}."\n");
                $broken=1;
                next;
            }
            my $n=$page; $n=~s!^[^/]*/!!;
            my $aintxt=$atok->{'revisions'}[0]{'*'} // '';
            $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 [[$page/archive]] 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){
            $res=$api->edit($tok, $outtxt, "Archiving closed XfDs to [[$page/archive]]".$screwup, 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)){
            $res=$api->edit($atok, $atxt, "Archiving closed XfDs from [[$page]]".$screwup, 0, 1);
            if($res->{'code'} ne 'success'){
                $api->warn("Save failed for $page/archive: ".$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;