User:AnomieBOT/source/tasks/MedcabBot.pm

package tasks::MedcabBot;

=pod

=for warning
Due to breaking changes in AnomieBOT::API, this task will probably not run
anymore. If you really must run it, try getting a version from before
2018-08-12.

=begin metadata

Bot:     MedcabBot
Task:    MedcabBot
BRFA:    Wikipedia:Bots/Requests for approval/MedcabBot 2
Status:  Inactive 2012-07-27
Created: 2011-10-04

Perform basic clerking tasks for [[Wikipedia:Mediation Cabal]]:
* Update [[Wikipedia:Mediation Cabal/Cases]]
* Mark cases active, inactive, closing, or closed based on activity.
* Notify users about case status.

=end metadata

=cut

use utf8;
use strict;

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

my $version=3;

my %cat2status=(
    'Category:Wikipedia Medcab new cases' => 'New',
    'Category:Wikipedia Medcab active cases' => 'Active',
    'Category:Wikipedia Medcab cases on hold' => 'On hold',
    'Category:Wikipedia Medcab inactive cases' => 'Inactive',
    'Category:Wikipedia Medcab cases pending closure' => 'Closing',
);
my %statusmap=(
    'NEW' => 'New',
    'New' => 'New',
    'new' => 'New',
    
    'ACTIVE' => 'Active',
    'Open' => 'Active',
    'Active' => 'Active',
    'active' => 'Active',
    'Opened' => 'Active',
    'opened' => 'Active',
    'open' => 'Active',

    'HOLD' => 'On hold',
    'Onhold' => 'On hold',
    'On hold' => 'On hold',
    'hold' => 'On hold',
    'Hold' => 'On hold',
    
    'INACTIVE' => 'Inactive',
    'Inactive' => 'Inactive',
    'Stale' => 'Inactive',
    'inactive' => 'Inactive',
    
    'PENDINGCLOSE' => 'Closing',
    'Closing' => 'Closing',
    
    'CLOSED' => 'Closed',
    'Closed' => 'Closed',
    'close' => 'Closed',
    'closed' => 'Closed',
);

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

=pod

=for info
Approved 2011-10-29.<br />[[Wikipedia:Bots/Requests for approval/MedcabBot 2]]

=for info
Bot is currently inactive, as MedCab is closed.

=cut

sub approved {
    return -211;
}

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

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

    my $screwup=' Errors? [[User:'.$api->user.'/shutoff/MedcabBot]]';
    my $b0rken=0;
    my $botname=$api->user;

    my $starttime=time();

    my $slow=0;
    my $vv=$api->store->{'version'}//0;
    if($vv < $version){
        $slow=1;
        for my $k (keys %{$api->store}){
            next unless $k=~/^case /;
            my $scase=$api->store->{$k};
            delete $scase->{'opened'} if $vv<2;
            delete $scase->{'lastrevid'};
            $api->store->{$k}=$scase;
        }
    }

    # Database cleanup
    while(my ($k,$v)=each %{$api->store}){
        next unless $k=~/^case /;
        delete $api->store->{$k} if(($v->{'lastedit'}//0) < time-120*86400);
    }

    # Load the templates processed by the bot
    my %templates=$api->redirects_to_resolved('Template:Medcab participant', 'Template:Inactivecase', 'Template:Medcab case update', 'Template:MedcabStatus');
    if(exists($templates{''})){
        $api->warn("Failed to get medcab template redirects: ".$templates{''}{'error'}."\n");
        return 60;
    }

    # Load the list of cases to be processed
    my %cases=();
    my $iter=$api->iterator(
        generator      => 'categorymembers',
        gcmtitle       => ['Category:Wikipedia Medcab new cases', 'Category:Wikipedia Medcab active cases', 'Category:Wikipedia Medcab cases on hold', 'Category:Wikipedia Medcab inactive cases', 'Category:Wikipedia Medcab cases pending closure'],
        gcmnamespace   => 4,
        gcmtype        => 'page',
        gcmlimit       => 'max',
        prop           => 'info|categories',
        cllimit        => 'max',
        clcategories   => 'Category:Wikipedia Medcab closed cases',
    );
    while(my $p=$iter->next){
        return 0 if $api->halting;
        if(!$p->{'_ok_'}){
            $api->warn("Failed to retrieve members for ".$iter->iterval.": ".$p->{'error'}."\n");
            return 60;
        }
        next unless $p->{'title'}=~m{^Wikipedia:Mediation Cabal/Cases/(\d+ \S+ \d+/.+)$};
        next if grep $_ eq 'Category:Wikipedia Medcab closed cases', @{$p->{'categories'}//[]};

        my $case=$1;
        $cases{$case}={
            case      => $case,
            status    => $cat2status{$iter->iterval},
            lastrevid => $p->{'lastrevid'},
        };
    }

    # Update data for edited cases
    while(my ($k,$case)=each %cases){
        return 0 if $api->halting;
        my $scase=($api->store->{"case $k"}//{});
        my $edited=$case->{'lastrevid'} != ($scase->{'lastrevid'}//0);

        $scase->{'case'}=$case->{'case'};
        $scase->{'status'}//=$case->{'status'};
        $scase->{'newstatus'}=$case->{'status'};
        $scase->{'needcheck'}=1 if $edited;
        $scase->{'lastrevid'}=$case->{'lastrevid'};
        $scase->{'externaldiscussion'}//='';

        if(!exists($scase->{'created'})){
            my $res=$api->query(
                titles        => "Wikipedia:Mediation Cabal/Cases/$k",
                prop          => 'revisions',
                rvprop        => 'timestamp',
                rvlimit       => 1,
                rvdir         => 'newer',
            );
            if($res->{'code'} ne 'success'){
                $api->warn("Failed to fetch first revision for $k: ".$res->{'error'}."\n");
                return 60;
            }
            $res=(values %{$res->{'query'}{'pages'}})[0];
            $scase->{'created'}=ISO2timestamp($res->{'revisions'}[0]{'timestamp'}) if @{$res->{'revisions'}};
            $scase->{'created'}//=time;
        }

        if($edited){
            my $res=$api->query(
                titles        => "Wikipedia:Mediation Cabal/Cases/$k",
                prop          => 'revisions',
                rvprop        => 'timestamp|content',
                rvlimit       => 1,
                rvexcludeuser => $api->user,
            );
            if($res->{'code'} ne 'success'){
                $api->warn("Failed to fetch most recent non-bot revision for $k: ".$res->{'error'}."\n");
                return 60;
            }
            $res=(values %{$res->{'query'}{'pages'}})[0];
            if(@{$res->{'revisions'}}){
                $scase->{'lastedit'}=ISO2timestamp($res->{'revisions'}[0]{'timestamp'});

                # Get list of mediators
                {
                    my $mediators='';
                    my $comment='';
                    my $externaldiscussion='';
                    $api->process_templates($res->{'revisions'}[0]{'*'}, sub {
                        my $name=shift;
                        my $params=shift;

                        return undef unless ($templates{"Template:$name"}//'') eq 'Template:MedcabStatus';
                        foreach ($api->process_paramlist(@$params)){
                            if($_->{'name'} eq 'mediators'){
                                $mediators=$_->{'value'}
                            }
                            if($_->{'name'} eq 'external discussion'){
                                $externaldiscussion=$_->{'value'}
                            }
                            if($_->{'name'} eq 'comment'){
                                $comment=$_->{'value'};
                                $comment=~s/^\s*|\s*$//g;
                            }
                        }
                        return undef;
                    });
                    my %seen=();
                    my @m=grep { !$seen{$_}++ } $mediators=~/\[\[\s*(?i:User)\s*:\s*(.*?)(?:\|.*?)?\s*\]\]/g;
                    $scase->{'mediators'}=\@m;
                    $scase->{'externaldiscussion'}=$externaldiscussion;
                    $scase->{'curcomment'}=$comment;
                }

                # Get list of parties
                for my $s ($api->split_sections($res->{'revisions'}[0]{'*'})){
                    next unless $s->{'title'} eq 'Who is involved?';
                    my $x=join("\n", grep /^[*#][^*#:]/, split(/\n/, $s->{'body'}));
                    if(($scase->{'rawparties'}//'') ne $x) {
                        my $res2=$api->query(
                            action => 'parse',
                            title  => "Wikipedia:Mediation Cabal/Cases/$k",
                            text   => $x,
                            prop   => 'links',
                        );
                        if($res2->{'code'} ne 'success'){
                            $api->warn("Failed to parse list of parties in $k: ".$res2->{'error'}."\n");
                            return 60;
                        }
                        $scase->{'rawparties'}=$x;
                        my %x=map { $_=$_->{'*'}; s#^[^:]*:([^/]+)(?:/.*)?#$1#; $_ => 1; } grep(($_->{'ns'}&~1)==2, @{$res2->{'parse'}{'links'}});
                        $scase->{'parties'}=[sort keys %x];
                    }
                    last;
                }
            }
        }

        if($scase->{'externaldiscussion'} ne ''){
            my $ed=$scase->{'externaldiscussion'};
            $ed=~s/#.*//;
            my $res2=$api->query(
                titles        => $ed,
                prop          => 'revisions',
                rvprop        => 'timestamp',
                rvlimit       => 1,
                rvexcludeuser => $api->user,
            );
            if($res2->{'code'} ne 'success'){
                $api->warn("Failed to fetch most recent non-bot revision for $k external discussion at $ed: ".$res2->{'error'}."\n");
                return 60;
            }
            $res2=(values %{$res2->{'query'}{'pages'}})[0];
            if(@{$res2->{'revisions'}}){
                my $le=ISO2timestamp($res2->{'revisions'}[0]{'timestamp'});
                $scase->{'lastedit'}=$le if $le>$scase->{'lastedit'};
            }
        }

        $scase->{'newstatus'}='Active' if($scase->{'newstatus'} ne 'New' && $scase->{'newstatus'} ne 'On hold' && $scase->{'lastedit'}>=time-7*86400);
        if($scase->{'newstatus'} eq 'Active' && $scase->{'lastedit'}<time-7*86400){
            $scase->{'newstatus'}='Inactive';
            $scase->{'spaminactive'}=[@{$scase->{'parties'}}, @{$scase->{'mediators'}}];
        }
        ($scase->{'spamclosing'},$scase->{'newstatus'})=(1,'Closing') if($scase->{'newstatus'} eq 'Inactive' && $scase->{'lastedit'}<time-21*86400);
        $scase->{'newstatus'}='Closed' if($scase->{'newstatus'} eq 'Closing' && $scase->{'lastedit'}<time-28*86400);

        my $c=$scase->{'comment'}//'';
        $scase->{'comment'}='';
        $scase->{'comment'}="Inactive since ".strftime('%e %B %Y', gmtime $scase->{'lastedit'})."<!-- MedcabBot -->" if($scase->{'newstatus'} eq 'Inactive' || $scase->{'newstatus'} eq 'Closing');

        $scase->{'needcheck'}=1 if $scase->{'status'} ne $scase->{'newstatus'};
        $scase->{'needcheck'}=1 if $c ne $scase->{'comment'};

        if($scase->{'newstatus'} ne 'New' && !exists($scase->{'opened'})){
            $scase->{'opened'}=time;
            if($slow){
                # Ugh.
                my $res2=$api->query(
                    titles        => "Wikipedia:Mediation Cabal/Cases/$k",
                    prop          => 'revisions',
                    rvprop        => 'ids|timestamp',
                    rvlimit       => 'max',
                );
                if($res2->{'code'} ne 'success'){
                    $api->warn("Failed to fetch revision list for $k: ".$res2->{'error'}."\n");
                    return 60;
                }
                $res2=(values %{$res2->{'query'}{'pages'}})[0]{'revisions'};
                for my $r (@$res2){
                    my $res3=$api->query(
                        action => 'parse',
                        oldid  => $r->{'revid'},
                        prop   => 'categories',
                    );
                    if($res3->{'code'} ne 'success'){
                        $api->warn("Failed to parse revision ".$r->{'revid'}." for $k: ".$res3->{'error'}."\n");
                        return 60;
                    }
                    if(grep $_->{'*'} eq 'Wikipedia_Medcab_new_cases', @{$res3->{'parse'}{'categories'}}){
                        last;
                    } else {
                        $scase->{'opened'}=ISO2timestamp($r->{'timestamp'});
                    }
                }
            }
        }

        delete $scase->{'held'} if $scase->{'newstatus'} ne 'On hold';
        if($scase->{'newstatus'} eq 'On hold' && !exists($scase->{'held'})){
            $scase->{'held'}=time;
            if($slow){
                # Ugh.
                my $res2=$api->query(
                    titles        => "Wikipedia:Mediation Cabal/Cases/$k",
                    prop          => 'revisions',
                    rvprop        => 'ids|timestamp',
                    rvlimit       => 'max',
                );
                if($res2->{'code'} ne 'success'){
                    $api->warn("Failed to fetch revision list for $k: ".$res2->{'error'}."\n");
                    return 60;
                }
                $res2=(values %{$res2->{'query'}{'pages'}})[0]{'revisions'};
                for my $r (@$res2){
                    my $res3=$api->query(
                        action => 'parse',
                        oldid  => $r->{'revid'},
                        prop   => 'categories',
                    );
                    if($res3->{'code'} ne 'success'){
                        $api->warn("Failed to parse revision ".$r->{'revid'}." for $k: ".$res3->{'error'}."\n");
                        return 60;
                    }
                    if(grep $_->{'*'} eq 'Wikipedia_Medcab_cases_on_hold', @{$res3->{'parse'}{'categories'}}){
                        last;
                    } else {
                        $scase->{'held'}=ISO2timestamp($r->{'timestamp'});
                    }
                }
            }
        }

        $api->store->{"case $k"}=$scase;
    }

    $api->store->{'version'} = $version;

    # Now, update any pages that need updating
    for my $k (keys %cases){
        return 0 if $api->halting;
        my $scase=$api->store->{"case $k"};
        next unless $scase->{'needcheck'};

        my $tok=$api->edittoken("Wikipedia:Mediation Cabal/Cases/$k", 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 $k: ".$tok->{'error'}."\n");
            return 60;
        }

        my $intxt=$tok->{'revisions'}[0]{'*'};
        my @summary=();
        my $istaggedinactive=0;
        my $curcomment=$scase->{'curcomment'}//'';
        my $outtxt=$api->process_templates($intxt, sub {
            my $name=shift;
            my $params=shift;
            my $wikitext=shift;
            shift; # $data
            my $oname=shift;

            $istaggedinactive=1 if ($templates{"Template:$name"}//'') eq 'Template:Inactivecase';
            return undef unless ($templates{"Template:$name"}//'') eq 'Template:MedcabStatus';

            my $status='';
            my $comment='';
            foreach ($api->process_paramlist(@$params)){
                if($_->{'name'} eq 'status'){
                    my $v=$_->{'value'};
                    $status=$statusmap{$v}//$v;
                }
                $comment=$_->{'value'} if $_->{'name'} eq 'comment';
            }
            if($status eq 'Closing' && $comment!~/<!-- MedcabBot -->/){
                $scase->{'newstatus'}='Closing';
                $scase->{'comment'}='';
            }

            my @ch=();
            my $ret="{{$oname";
            my $didstatus=0;
            my $didcomment=0;
            foreach ($api->process_paramlist(@$params)){
                if($_->{'name'} eq 'status'){
                    my $v=$_->{'value'};
                    $v=$statusmap{$v}//$v;
                    my $nl=($_->{'text'}=~/[\r\n]\s*$/)?"\n":"";
                    if($v ne $scase->{'newstatus'}){
                        $_->{'text'}=$_->{'oname'}.'='.$scase->{'newstatus'}.$nl;
                        push @ch, "status=".$scase->{'newstatus'};
                    }
                    $didstatus=1;
                }
                if($_->{'name'} eq 'comment'){
                    $didcomment=1;
                    my $nl=($_->{'text'}=~/[\r\n]\s*$/)?"\n":"";
                    if($scase->{'comment'}){
                        if($scase->{'comment'} ne $_->{'value'}){
                            $_->{'text'}=$_->{'oname'}.'='.$scase->{'comment'}.$nl;
                            push @ch, "update comment";
                            $curcomment=$scase->{'comment'};
                        }
                    } elsif($_->{'text'}=~/<!-- MedcabBot -->/) {
                        $_->{'text'}=$_->{'oname'}.'='.$nl;
                        push @ch, "remove bot comment";
                        $curcomment='';
                    }
                }
                $ret.='|'.$_->{'text'};
            }
            if(!$didstatus){
                my $nl=($ret=~/[\r\n]\s*$/)?"\n":"";
                $ret.="|status=".$scase->{'newstatus'}.$nl;
                push @ch, "status=".$scase->{'newstatus'};
            }
            if(!$didcomment && $scase->{'comment'}){
                my $nl=($ret=~/[\r\n]\s*$/)?"\n":"";
                $ret.="|comment=".$scase->{'comment'}.$nl;
                push @ch, "add comment";
                $curcomment=$scase->{'comment'};
            }
            $ret.="}}";
            
            $wikitext=~s/\s+/ /g;
            my $x=$ret; $x=~s/\s+/ /g;
            return undef if $x eq $wikitext;
            push @summary, "update {{MedcabStatus}} (".join(', ', @ch).")";
            return $ret;
        });

        # Add or remove {{inactivecase}}
        if($scase->{'lastedit'} < time-7*86400){
            $outtxt="{{inactivecase}}\n".$outtxt if !$istaggedinactive;
            push @summary, "tag {{inactivecase}}" if !$istaggedinactive;
        } elsif($istaggedinactive) {
            $outtxt=$api->process_templates($outtxt, sub {
                my $name=shift;
                return undef if ($templates{"Template:$name"}//'') ne 'Template:Inactivecase';
                push @summary, "remove {{inactivecase}}";
                return '';
            });
        }

        if(@summary){
            $summary[$#summary]='and '.$summary[$#summary] if @summary>1;
            my $summary=join((@summary>2)?', ':' ', @summary);
            $api->log("$summary in $k");
            $res=$api->edit($tok, $outtxt, $summary, 0, 1);
            if($res->{'code'} ne 'success'){
                $api->warn("Failed to edit $k: ".$res->{'error'}."\n");
                return 60;
            }
        }
        $scase->{'status'}=$scase->{'newstatus'};
        $scase->{'curcomment'}=$curcomment;
        $scase->{'needcheck'}=0;
        $api->store->{"case $k"}=$scase;
    }

    # Spam anyone that needs spamming
    for my $k (keys %cases){
        return 0 if $api->halting;
        my $scase=$api->store->{"case $k"};
        my $mediator=$scase->{'mediators'}[0]//'';

        if($scase->{'status'} ne 'New' && $scase->{'status'} ne 'On hold' && $scase->{'status'} ne 'Closed' && $mediator ne ''){
            $scase->{'spammednew'}//=[];
            for my $user (@{$scase->{'parties'}}) {
                if($api->halting){
                    $api->store->{"case $k"}=$scase;
                    return 0;
                }

                next if grep $_ eq $user, @{$scase->{'spammednew'}};
                my $tok=$api->edittoken("User talk:$user", links=>{ namespace=>4 });
                if($tok->{'code'} eq 'shutoff'){
                    $api->warn("Task disabled: ".$tok->{'content'}."\n");
                    $api->store->{"case $k"}=$scase;
                    return 300;
                }
                if($tok->{'code'} eq 'pageprotected' || $tok->{'code'} eq 'botexcluded'){
                    # Cannot notify, don't worry about it
                    push @{$scase->{'spammednew'}}, $user;
                    next;
                }
                if($tok->{'code'} ne 'success'){
                    $api->warn("Failed to get edit token for User talk:$user: ".$tok->{'error'}."\n");
                    next;
                }

                if(grep $_->{'title'} eq "Wikipedia:Mediation Cabal/Cases/$k", @{$tok->{'links'}}){
                    $api->log("It seems $user is already notified of $k, skipping");
                    push @{$scase->{'spammednew'}}, $user;
                } else {
                    my $txt=$tok->{'revisions'}[0]{'*'};
                    $txt=~s/\s*$/\n\n{{subst:Medcab participant|2=$k|3=$mediator}} ~~~~/;
                    $api->log("Notifying $user of $k");
                    $res=$api->edit($tok, $txt, "/* Mediation Cabal: Request for participation */ You have been mentioned in [[Wikipedia:Mediation Cabal/Cases/$k]]", 0, 0);
                    if($res->{'code'} ne 'success'){
                        $api->warn("Failed to edit User talk:$user: ".$res->{'error'}."\n");
                        next;
                    }
                    push @{$scase->{'spammednew'}}, $user;
                }
            }
            $api->store->{"case $k"}=$scase;
        }

        my @users=@{$scase->{'spaminactive'}//[]};
        if(@users && $mediator ne ''){
            $scase->{'spaminactive'}=[];
            while(@users){
                if($api->halting){
                    push @{$scase->{'spaminactive'}}, @users;
                    $api->store->{"case $k"}=$scase;
                    return 0;
                }

                my $user=shift @users;
                my $tok=$api->edittoken("User talk:$user");
                if($tok->{'code'} eq 'shutoff'){
                    $api->warn("Task disabled: ".$tok->{'content'}."\n");
                    push @{$scase->{'spaminactive'}}, $user, @users;
                    $api->store->{"case $k"}=$scase;
                    return 300;
                }
                if($tok->{'code'} eq 'pageprotected' || $tok->{'code'} eq 'botexcluded'){
                    # Cannot notify, don't worry about it
                    next;
                }
                if($tok->{'code'} ne 'success'){
                    $api->warn("Failed to get edit token for User talk:$user: ".$tok->{'error'}."\n");
                    push @{$scase->{'spaminactive'}}, $user;
                    next;
                }

                my $ed='';
                $ed="|external discussion=".$scase->{'externaldiscussion'} if $scase->{'externaldiscussion'} ne '';
                my $txt=$tok->{'revisions'}[0]{'*'};
                $txt=~s/\s*$/\n\n{{subst:Medcab case update|2=$k|3=$mediator$ed}}/;
                $api->log("Notifying $user of $k inactivity");
                $res=$api->edit($tok, $txt, "/* Mediation Cabal: Case update */ The case [[Wikipedia:Mediation Cabal/Cases/$k]] you are involved with is inactive", 0, 0);
                if($res->{'code'} ne 'success'){
                    $api->warn("Failed to edit User talk:$user: ".$res->{'error'}."\n");
                    push @{$scase->{'spaminactive'}}, $user;
                    next;
                }
            }
            $api->store->{"case $k"}=$scase;
        }

        if($scase->{'spamclosing'}){{
            return 0 if $api->halting;
            my $tok=$api->edittoken("Wikipedia talk:Mediation Cabal");
            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 talk:Mediation Cabal: ".$tok->{'error'}."\n");
                last;
            }

            my $txt=$tok->{'revisions'}[0]{'*'};
            $txt=~s/\s*$//;
            $txt.="\n\n== MedcabBot: Case [[Wikipedia:Mediation Cabal/Cases/$k|$k]] pending closure due to inactivity ==\nThe case [[Wikipedia:Mediation Cabal/Cases/$k]]".($scase->{'externaldiscussion'} ne ''?" (with outside discussion at [[:$scase->{externaldiscussion}]])":"")." has been inactive since ".strftime("%e %B %Y", gmtime $scase->{'lastedit'}).", and will be automatically closed at about ".strftime("%H:%M, %e %B %Y", gmtime $scase->{'lastedit'}+28*86400)." (UTC). Note that any non-bot edit to the case page will reset the timer. ~~~~";
            $api->log("Notifying Medcab of $k inactivity");
            $res=$api->edit($tok, $txt, "/* MedcabBot: Case $k pending closure due to inactivity */ new section", 0, 0);
            if($res->{'code'} ne 'success'){
                $api->warn("Failed to edit Wikipedia talk:Mediation Cabal: ".$res->{'error'}."\n");
            } else {
                $scase->{'spamclosing'}=0;
            }
        }}
        $api->store->{"case $k"}=$scase;
    }

    # Update the case listing page
    for my $page ('Wikipedia:Mediation Cabal/Cases', 'Wikipedia:Mediation Cabal/Coordination Desk') {
        return 0 if $api->halting;
        my $tok=$api->edittoken($page);
        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 $page: ".$tok->{'error'}."\n");
            return 60;
        }
        my $intxt=$tok->{'revisions'}[0]{'*'};

        my %scases=(
            'New' => [],
            'Active' => [],
            'On hold' => [],
            'Inactive' => [],
            'Closing' => [],
        );
        for my $k (keys %cases){
            my $scase=$api->store->{"case $k"};
            push @{$scases{$scase->{'status'}}}, $scase;
        }

        my $outtxt=$intxt;
        my @tags=();
        for my $x (['New','created',0],
                   ['Active','opened',0],
                   ['On hold','held',0],
                   ['Inactive','lastedit',1],
                   ['Closing','lastedit',1]){
            my ($tag,$sort,$le)=@$x;
            my @cases=@{$scases{$tag}};
            @cases=sort { $a->{$sort} <=> $b->{$sort} } @cases;
            @cases=map {
                my $ret="* [[Wikipedia:Mediation Cabal/Cases/".$_->{'case'}."|".$_->{'case'}."]] — ";
                my @mediators=@{$_->{'mediators'}};
                if(@mediators){
                    $ret.=(@mediators==1?'Mediator: ':'Mediators: ');
                    $ret.=join(', ', map "[[User:$_|$_]]", @mediators)."; ";
                }
                if($le){
                    $ret.="opened ".strftime("%e %B %Y", gmtime $_->{'opened'});
                    $ret.=", inactive since ".strftime("%e %B %Y", gmtime $_->{'lastedit'});
                } elsif($tag eq 'On hold'){
                    $ret.="opened ".strftime("%e %B %Y", gmtime $_->{'opened'});
                    $ret.=", on hold since ".strftime("%e %B %Y", gmtime $_->{'held'});
                } else {
                    $ret.="$sort ".strftime("%e %B %Y", gmtime $_->{$sort});
                }
                my $cc=$_->{'curcomment'}//'';
                $cc=~s/<!--.*?-->//g;
                $cc=~s/^\s*|\s*$//g;
                $ret.='.';
                $ret.=" '''Comment:''' $cc" if $cc ne '';
                $ret.=" ([[:".$_->{'externaldiscussion'}."|external discussion]])" if $_->{'externaldiscussion'} ne "";
                $ret.=" ($tag)";
            } @cases;
            my $cases=join("\n", @cases);
            $cases="\n$cases\n" if $cases ne '';
            $outtxt=~s/(<!-- BEGIN ${tag}Cases -->).*?(<!-- END ${tag}Cases -->)/$1$cases$2/s;
            push @tags, $tag if $outtxt=~/<!-- BEGIN ${tag}Cases -->/;
        }

        if($outtxt ne $intxt){
            my @s=();
            for my $tag (@tags){
                my $ct=@{$scases{$tag}};
                push @s, lc("$ct $tag") if $ct;
            }
            push @s, 'no cases' unless @s;
            my $summary='Updating cases list: '.join(', ', @s);
            $api->log($summary);
            $res=$api->edit($tok, $outtxt, $summary, 0, 1);
            if($res->{'code'} ne 'success'){
                $api->warn("Failed to edit $page: ".$res->{'error'}."\n");
                return 60;
            }
        }
    }

    return 1800;
}

1;