User:AnomieBOT/source/tasks/TFDTemplateSubster.pm

package tasks::TFDTemplateSubster;

=pod

=begin metadata

Bot:      AnomieBOT
Task:     TFDTemplateSubster
BRFA:     Wikipedia:Bots/Requests for approval/AnomieBOT 78
Status:   Approved 2020-02-02
Created:  2020-01-31

Subst templates to implement the results of a deletion discussion, based on listing at
[[User:AnomieBOT/TFDTemplateSubster]].

=end metadata

=cut

use utf8;
use strict;

use Data::Dumper;
use JSON;
use AnomieBOT::Task qw/:time/;
use tasks::TemplateSubster::Base;
use vars qw/@ISA/;
@ISA=qw/tasks::TemplateSubster::Base/;

my $workPage = 'User:AnomieBOT/TFDTemplateSubster';
my $workPageTalk = 'User talk:AnomieBOT/TFDTemplateSubster';

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

=pod

=for info
Approved 2020-02-02<br />[[Wikipedia:Bots/Requests for approval/AnomieBOT 78]]

=cut

sub approved {
    return 3;
}

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

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

    $res = $self->fetchSig( $api );
    return $res if defined( $res );

    # Spend a max of 5 minutes on this task before restarting
    my $endtime = time() + 300;

    # Load and check work page
    $res = $api->query(
        titles => $workPage,
        prop => 'revisions|info',
        inprop => 'protection',
        rvprop => 'content',
        rvslots => 'main',
        rvlimit => 1,
        formatversion => 2,
    );
    if ( $res->{'code'} ne 'success' ) {
        $api->warn( "Failed to get work list: " . $res->{'error'} . "\n" );
        return 60;
    }
    $res = $res->{'query'}{'pages'}[0];
    if ( exists( $res->{'missing'} ) ) {
        $api->warn( "Work list does not exist, WTF?\n" );
        return 3600;
    }
    my $ok=0;
    foreach ( @{$res->{'protection'}} ) {
        $ok=1 if($_->{'type'} eq 'edit' && ($_->{'level'} eq 'sysop' || $_->{'level'} eq 'templateeditor'));
    }
    if ( !$ok ) {
        $api->whine( "Work page is unprotected", "The work page must be fully protected or template-protected. Please have the page protected..", Pagename => $workPageTalk );
        return 300;
    }

    # Extract work list
    my %workList = ();
    my $fail = 0;
    $api->process_templates( $res->{'revisions'}[0]{'slots'}{'main'}{'content'}, sub {
        return undef if $fail;

        my $name=shift;
        my $params=shift;

        return undef unless $name eq '/row';

        my %data = ();
        foreach ( $api->process_paramlist( @$params ) ) {
            $data{$_->{'name'}} = $_->{'value'};
        }
        return undef unless exists( $data{1} );

        my ( $link, $logpage );
        if ( exists( $data{'link'} ) ) {
            $link = $data{'link'};
            ( $logpage = $link ) =~ s/#.*//;
        } else {
            return undef unless exists( $data{2} );
            $logpage = 'WP:Templates for discussion/Log/' . $data{2};
            $link = $logpage . '#' . ( $data{'section'} // $data{1} );
        }

        my $res = $api->query(
            'titles' => join( '|', $data{1}, $logpage ),
            'formatversion' => 2,
        );
        if ( $res->{'code'} ne 'success' ) {
            $fail = 1;
            return undef;
        }

        $workList{$data{1}} = {
            'link' => $link,
            'reason' => $data{'reason'} // '',
        };
        return undef;
    } );
    return 3600 unless %workList; # Nothing to process

    # Don't resolve in case only a redirect should be substed for some strange reason.
    my %r = $api->redirects_to( keys %workList );
    if ( exists( $r{''} ) ) {
        $api->warn( "Failed to get redirects: " . $r{''}{'error'} . "\n" );
        return 60;
    }

    # Handle normalizations, e.g. underscore to space.
    while ( my ( $r, $t ) = each( %r ) ) {
        if ( defined( $workList{$r} ) && !defined( $workList{$t} ) ) {
            $workList{$t} = $workList{$r};
            delete $workList{$r};
        }
    }

    $self->{'tfdMap'} = {};
    while ( my ( $r, $t ) = each( %r ) ) {
        $self->{'tfdMap'}{$r} = $workList{$t};
    }

    # Load and (if necessary) parse processing status page
    my $tok = $api->edittoken( "$workPage/status", EditRedir => 1, NoExclusion => 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 $workPage/status: " . $tok->{'error'} . "\n" );
        return 300;
    }
    my $revid = $tok->{'lastrevid'} // 0;
    my $status = $api->store->{'status'} // [ 0, {} ];
    if ( $status->[0] < $revid ) {
        eval {
            $status->[1] = JSON->new->decode( $tok->{'revisions'}[0]{'slots'}{'main'}{'*'} // '' );
        };
        $status->[1] = {} unless ref( $status->[1] ) eq 'HASH';
    }

    # Prepare processing list
    my %process = ();
    foreach my $title ( keys %workList ) {
        $process{$title} = 0;
    }
    foreach my $title ( keys %{$status->[1]} ) {
        $process{$title} = $status->[1]{$title} if exists( $process{$title} );
    }

    # Process pages!
    $res = $self->process( $api, \%process, \%r, $endtime );

    # Write processing status, if necessary
    my $writeStatus = $self->{'writeStatus'} // 0;
    foreach my $title ( keys %process ) {
        $writeStatus = 1 if $process{$title} != ( $status->[1]{$title} // 0 );
    }
    if ( $writeStatus ) {
        $api->log( "Updating processing status page" );
        my $outtxt = JSON->new->pretty->canonical->encode( \%process );
        my $res2 = $api->edit( $tok, $outtxt, 'Updating status page', 1, 1, contentmodel => 'json' );
        if ( $res2->{'code'} eq 'success' ) {
            $revid = $res2->{'edit'}{'newrevid'};
            $writeStatus = 0;
        } else {
            $api->warn( "Write failed on $workPage/status: " . $res2->{'error'} . "\n" );
        }
        $api->store->{'status'} = [ $revid, \%process ];
        $api->store->{'writeStatus'} = $writeStatus;
    }

    # Done for now.
    return $res;
}

sub summary {
    my ($self, $api, @remv) = @_;

    my %per = ();
    foreach my $t ( @remv ) {
        my $tfd = $self->{'tfdMap'}{"Template:$t"} // $self->{'tfdMap'}{$t} // undef;
        if ( !defined( $tfd ) ) {
            die "TFD entry for {{$t}} is missing from tfdMap! " . Dumper( $self->{'tfdMap'} );
        }
        my $per = '[[' . $tfd->{'link'} . ( $tfd->{'reason'} ne '' ? '|' . $tfd->{'reason'} : '' ) . ']]';
        push @{$per{$per}}, "{{$t}}";
    }

    my @s = ();
    foreach my $per ( keys %per ) {
        my @t = @{$per{$per}};
        $t[-1] = 'and ' . $t[-1] if @t > 1;
        push @s, join( ( @t > 2 ) ? ', ' : ' ', @t ) . ' per ' . $per;
    }

    return "Substing templates: " . join( '; ', @s ) . ". Report errors at [[$workPageTalk]].";
}

1;