#!/usr/bin/perl
# This script is released under the GFDL license, see
# http://en.wikipedia.org/w/index.php?title=User:HBC_AIV_helperbot/source&action=history
# for a full list of contributors
 
use strict;
use warnings;

### Configuration ###
my $read_rate = 30;
my $write_rate = 15;
 
my (%pages_to_watch) =
 (
  'Wikipedia:Administrator intervention against vandalism'      => $read_rate,
  'Wikipedia:Administrator intervention against vandalism/TB2'  => $read_rate,
  'Wikipedia:Usernames for administrator attention'             => $read_rate,
  'Wikipedia:Usernames for administrator attention/Bot'         => $read_rate,
  'Wikipedia:Usernames for administrator attention/Holding pen' => $read_rate,
 );
 
# Pattern to match examples used in the instructions
my $example_pattern = qr/(?:IP ?address|username)/i;
 
my @desired_parameters = qw(
  RemoveBlocked MergeDuplicates AutoMark FixInstructions AutoBacklog
);
### End Configuration ###
 
use DateTime;
use DateTime::Format::Duration;
use MediaWiki;
use Net::Netmask;
use POSIX qw(strftime);
use Time::Local;
use URI::Escape;
 
my $version_number = '2.0.25';
my $VERSION = "HBC AIV helperbot v$version_number";
 
my %special_ips;
my %notable_cats;
my $instructions = '';
 
local $SIG{'__WARN__'} = \&mywarn;
		 
open(PASS,'password');                  # A file with only the password, no carriage return
sysread(PASS, my $password, -s(PASS));  # No password in source code.
close(PASS);
open(USER,username');                  # A file with only the username, no carriage return
sysread(USER, my $username, -s(USER));  #
close(USER);
 
my $c                  =   MediaWiki->new;
$c->setup
                        ({
                          'bot' => {'user' => $username,'pass' => $password},
                          'wiki' => {'host' => 'en.wikipedia.org','path' => 'w', 'ssl' => 1}
                        }) || die "Failed to log in\n";
my $whoami              =  $c->user();
warn "$whoami v$version_number connected.\n";

# The program runs in this loop which handles a queue of jobs.
my(@job_list);
my $timing = 0;
 
add_job([\&get_ip_list,$c],0);
add_job([\&get_instructions,$c],0);
add_job([\&check_login,$c],600);
 
foreach my $page (keys %pages_to_watch)
  {
  add_job([\&check_page,$c,$page],$timing);
  $timing += 5;
  }
 
while (1)                               # Infinite loop, a serpent biting it's own tail.
  {
  sleep(1);                             # Important in all infinite loops to keep it calm
  my (@kept_jobs);                      # A place to put jobs not ready to run yet
  while (my $job = shift(@job_list))    # Go through each job pending
    {
    my($r_job , $timing) = @{$job};
    if ($timing < time())               # If it is time to run it then run it
      {
      if (ref($r_job) eq 'ARRAY')       # Callback style, reference an array with a sub followed by paramaters
        {
        my $cmd = shift(@{$r_job});
        &{$cmd}(@{$r_job});
        }
      elsif (ref($r_job) eq 'CODE')     # Otherwise just the reference to the sub
        {
        &{$r_job};
        }
      }
    else                                # If it is not time yet, save it for later
      {
      push(@kept_jobs , $job)
      }
    }
  push (@job_list , @kept_jobs);        # Keep jobs that are still pending
  }
 
###################
### SUBROUTINES ###
###################
 
sub add_job     # Command to add a job to the queue
  {
  my ($r_job , $timing) = @_;
  push (@job_list , [$r_job , (time()+$timing)]);
  }
 
sub check_instructions {
  my ($c, $page, $content) = @_;
 
  unless ($content =~ m/\Q$instructions\E/s) {
    add_job([\&fix_instructions,$c,$page],0);
    return 0;
  }
  return 1;
}
 
sub check_login {
  my ($c) = @_;
  my $html = $c->{ua}->get("https://en.wikipedia.org/wiki/User:$whoami")->content();
  warn "Checking login... $whoami v$version_number still connected.\n";
  if ($html =~ m|\"wgUserName\":null|) {
    warn "Login check failed, logging back in!\n";
    delete $c->{'logged_in'};
    $c->login;
  }
  add_job([\&check_login,$c],600);
}
 
sub check_page  # Read the page and gather usernames, give each use a check_user job on the queue
  {             # Then add Check_page to the queue scheduled for $read_rate seconds
  my ($c,$page) = @_;
  # Get page, read only
  my $content =  $c->get($page, 'r')->{'content'};
  unless ($content && $content =~ m|\{\{((?:no)?adminbacklog)\}\}\s*<\!-- (?:HBC AIV helperbot )?v([\d.]+) ((?:\w+=\S+\s+)+)-->|i)
    {
    warn "Could not find parameter string, not doing anything: $page\n";
    add_job([\&check_page,$c,$page],$pages_to_watch{$page});
    return;
    }
  my($ab_current, $active_version, $parameters) = ($1,$2,$3);
  unless (check_version($active_version)) {
    warn "Current version $version_number not allowed by active version $active_version on $page! Will check again in 2 minutes.\n";
    add_job([\&check_page,$c,$page],120);  # Schedule myself 2 minutes later
    return;
  }
  my $params = parse_parameters($parameters);
  add_job([\&check_page,$c,$page],$pages_to_watch{$page});
  ($params->{'AutoBacklog'} = '') if ($params->{'AddLimit'} <= $params->{'RemoveLimit'});
  if ($params->{'FixInstructions'} eq 'on') {
    return unless check_instructions($c,$page,$content);
  }
  my @content = split("\n",$content); # Split into lines
  my $report_count = 0;
  my (%user_count, @IP_comments_needed, $merge_called, $in_comment);
  foreach my $line (@content)
    {
    my $bare_line;
    ($in_comment,$bare_line, undef) = comment_handler($line, $in_comment);
    next if ($in_comment && ($line eq $bare_line));
    ($bare_line =~ m/{{((?:ip)?vandal|userlinks|user-uaa)\|\s*(.+?)\s*}}/i)  || next(); # Go to next line if there is not a vandal template on this one.
    my $user = $2;                              # Extract username from template
    my $user2;
    if ($user =~ m/^((?:1|user)=)/i) {
      $user2 = $user;
      $user =~ s/^$1//i;
    }
    $report_count++;
    $user_count{$user}++;
    if (($user_count{$user} > 1) && !($merge_called) && ($params->{'MergeDuplicates'} eq 'on'))
      {
      warn "Calling merge because of $user on $page\n";
      add_job([\&merge_duplicate_reports,$c,$page],0);
      $merge_called = 1;
      }
    if ($params->{'RemoveBlocked'} eq 'on') {
      add_job([\&check_user,$c,$user,$page],0); # Queue a check_user job for the user to run ASAP
      if ($user2) {
        add_job([\&check_user,$c,$user2,$page],0);
      }
    }
    my(@cats) = check_cats($user);
    if (scalar(@cats))
      {
      $special_ips{$user} = 'User is in the '.((scalar(@cats) > 1) ? ('categories') : ('category')).': ';
      foreach (@cats)
        {
        $_ = '[[:Category:'.$_.'|'.$_.']]'
        }
      $special_ips{$user} .= join(', ',@cats);
      $special_ips{$user} .= '.';
      }
    if ($params->{'AutoMark'} eq 'on' && !$merge_called)
      {
      if ($line !~ m|<\!-- Marked -->|)
        {
        foreach my $mask (keys(%special_ips))
          {
          if ($mask =~ m|^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:/\d{1,2})?$| && $user =~ m|^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$|) {
            if (Net::Netmask->new($mask)->match($user))
              {
              push (@IP_comments_needed, [\&comment_special_IP,$c,$page,$user,$mask]);
              last; # only match one mask
              }
          } else {
            if ($mask eq $user) {
              push (@IP_comments_needed, [\&comment_special_IP,$c,$page,$user,$mask]);
              last; # only match one mask
            }
          }
          }
        }
      }
    }
  foreach my $ra_param (@IP_comments_needed)
    {
    add_job([@{$ra_param},$report_count],0);
    }
  if ($params->{'AutoBacklog'} eq 'on' && !$merge_called)
    {
    add_job([\&set_backlog,$c,$page,$report_count,$params->{'AddLimit'},$params->{'RemoveLimit'}],0)
     if         ((($report_count >= $params->{'AddLimit'})    && ($ab_current eq 'noadminbacklog')) ||
                 (($report_count <= $params->{'RemoveLimit'}) && ($ab_current eq   'adminbacklog')));
    }
  return;
  }
 
sub check_user  # Determine if the user is blocked, if so gather information about the block
  {             # and shedule a remove_name job with all the information passed along
  my ($c,$user,$page) = @_;
  my $url = $c->{index}.'?title=Special:Ipblocklist&ip='.uri_escape($user);
  my $data = $c->{ua}->get($url)->content(); # Get blocklist info for user
  if ($data =~ m|TablePager mw-blocklist">|)       # If the user is currently blocked
    {
    # Get name of blocking admin
    ($data =~ m'TablePager_col_ipb_by"><a href="/wiki/User:(.*?)" (title|class)=') || ($data =~ m'\d{2}, <a href="/w/index\.php\?title=User:(.*?)&amp;'); #"
    my $blocker = uri_unescape($1);
    # Get expiry time of block, starting time of block, and calculate total time
    my $duration;
    if ($data =~ m|TablePager_col_ipb_expiry">(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})|)    # Match expiry time if one exists
      {
      my $expiry = DateTime->new(year=>$1,month=>$2,day=>$3,hour=>$4,minute=>$5,second=>$6,time_zone=>'UTC');
       $data =~ ( m|TablePager_col_ipb_timestamp">(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})|); # Match starting time
      my $block_time = DateTime->new(year=>$1,month=>$2,day=>$3,hour=>$4,minute=>$5,second=>$6,time_zone=>'UTC');
      $duration = timeconv($expiry, $block_time); # Pretty print the difference via timeconv (see below)
      }
    elsif($data =~ m/never|infinite|no expiry set/) # If there is no expiry and the word 'infinite' is found
      {
      $duration = 'indef'; # Set to indef
      }
    # Get block type flags
    my(@flags);
    (push(@flags,'AO')) if ($data =~ m|anon\. only|);                   # Match anon only
    (push(@flags,'ACB')) if ($data =~ m|account creation blocked|);     # Match account creation blocked
    (push(@flags,'ABD')) if ($data =~ m|autoblock disabled|);           # Match autoblock disabled
    my $block_type = ''; # Build empty string
    # If any flag exists build a flag string.
    $block_type = '[[User:HBC AIV helperbot/Legend|('.join(' ',@flags).')]]' if (scalar(@flags));
    add_job([\&remove_name,$c,$user,$blocker,$duration,$block_type,$page],0); # Queue a remove_name job to run ASAP
    }
  }
 
sub check_version {
  my ($active_version) = @_;
 
  my @active_parts = split(/\./, $active_version);
  my @my_parts = split(/\./, $version_number);
 
  return 0 if scalar(@active_parts) > scalar(@my_parts); # should never happen
 
  foreach (@active_parts) {
    my $check_part = shift(@my_parts);
    last if $check_part > $_;
    next if $_ <= $check_part;
    return 0;
  }
 
  return 1;
}
 
sub comment_handler {
  my ($line, $in_comment) = @_;
  my ($comment_starts, $comment_ends, $remainder) = (0,0,'');
 
  if ($in_comment) {
    # check if an opened comment ends in this line
    if ($line =~ m|-->|) {
      $line =~ s|(.*?-->)||;
      $in_comment = 0;
      $comment_ends = 1;
      $remainder = $1;
    }
  }
 
  # remove any self-contained comments
  $line =~ s|<!--.*?-->||g;
 
  if ($line =~ s|<!--.*||) {
    $in_comment = 1;
    $comment_starts = 1;
  }
 
  return (wantarray) ? ($in_comment, $line, $remainder) :
    $in_comment;
}
 
sub comment_special_IP
  {
  my($c,$page_name,$user,$mask,$report_count) = @_;
  my $page = $c->get($page_name, 'rw'); # Get page read/write
  return unless $page->{'content'};
  my(@content) = split("\n",$page->{'content'}); # Split into lines
  my (@new_content, $in_comment); # Place to put replacement content
  foreach my $line (@content) {
    $in_comment = comment_handler($line, $in_comment);
    if (($line =~ m|\Q$user\E|) && ($line =~ m/{{((?:ip)?vandal|userlinks|user-uaa)/i))
      {
      return if ($line =~ m|<\!-- Marked -->|);
      $line .= ' -->' if $in_comment;
      $line .= ' <!-- Marked -->'."\n:*'''Note''': $special_ips{$mask} ~~~~";
      $line .= ' <!-- ' if $in_comment;
      }
    push(@new_content,$line);
  }
  my $tally;
  $tally = 'Empty.' if ($report_count == 0);
  $tally ||= ($report_count.' report'.(($report_count > 1) ? ('s remaining.') : (' remaining.')));
  $page->{'content'} = join("\n",@new_content);
  $page->{'summary'} = $tally." Commenting on $user: $special_ips{$mask}";
  $page->save();
  warn "$user matched $mask, marked as: $special_ips{$mask}\n";
  return 1;
  }
 
sub fix_instructions {
  my ($c, $page_name) = @_;
  my $page = $c->get($page_name, 'rw');
  my $content = $page->{'content'};
  return unless $content;
  if ($content =~ m|===\s*User-reported\s*===\n|s) {
    $content =~ s|<!-- HagermanBot Auto-Unsigned -->|RE-ADD-HAGERMAN|;
    my @content = split("\n", $content);
    my (@reports_to_move, $in_comment, $report_count, $msg);
    foreach my $line (@content) {
      my ($bare_line,$remainder);
      ($in_comment,$bare_line,$remainder) = comment_handler($line, $in_comment);
      if ($line =~ m/{{((?:ip)?vandal|userlinks|user-uaa)\|\s*(?!$example_pattern)/i) {
        push(@reports_to_move, $line) if $in_comment;
        $report_count++;
      } elsif ($remainder =~ m/{{((?:ip)?vandal|userlinks|user-uaa)\|\s*(?!$example_pattern)/i) {
        $remainder =~ s/-->//;
        push(@reports_to_move, $remainder);
      }
    }
    if ($content =~ m|===\s*User-reported\s*===\s+<!--|s) {
      $content =~ s:(===\s*User-reported\s*===\s+)<!--.*?(-->|$):$1$instructions:s;
      $msg = '';
    } else {
      $content =~ s|(===\s*User-reported\s*===\n)|$1$instructions\n|s;
      $msg = ' Old instructions not found, please check page for problems.';
    }
    my $remaining_text;
    if ($report_count) {
      $remaining_text = ($report_count > 1) ? "$report_count reports remaining." : "$report_count report remaining.";
    } else {
      $remaining_text = "Empty.";
    }
    if (@reports_to_move) {
      my $reports_moved = scalar(@reports_to_move);
      if ($reports_moved > 50) {
        $page->{'summary'} = "$remaining_text Reset [[WP:AIV/I|instruction block]], WARNING: tried to move more than 50 reports, aborting - check history for lost reports.$msg";
      } else {
        foreach my $report (@reports_to_move) {
          if ($report =~ m|RE-ADD-HAGERMAN|) {
            $report =~ s|RE-ADD-HAGERMAN|<!-- HagermanBot Auto-Unsigned -->|;
            $report =~ s|~~~~||;
          } else {
            $report =~ s|~~~~|~~~~ <small><sup>(Original signature lost - report made inside comment)</sup></small>|;
          }
          $content .= "$report\n";
        }
        $page->{'summary'} = "$remaining_text Reset [[WP:AIV/I|instruction block]], $reports_moved report(s) moved to end of page.$msg";
      }
    } else {
      $page->{'summary'} = "$remaining_text Reset [[WP:AIV/I|instruction block]].$msg";
    }
    $content =~ s|RE-ADD-HAGERMAN|<!-- HagermanBot Auto-Unsigned -->|;
    $page->{'content'} = $content;
    $page->save();
    warn "Reset instruction block: $page_name\n";
  } else {
    warn "FATAL ERROR: User-reported header not found on $page_name!  Sleeping 2 minutes.\n";
    unless ($content =~ m|<!-- HBC AIV helperbot WARNING -->|) {
      $content .= "<!-- HBC AIV helperbot WARNING -->\n";
      $page->{'summary'} = 'WARNING: User-reported header not found!';
      $page->{'content'} = $content;
      $page->save();
    }
    sleep(120);
    return;
  }
}
 
sub get_instructions {
  my ($c) = @_;
  warn "Fetching instructions...\n";
  my $content = $c->get('Wikipedia:Administrator intervention against vandalism/instructions', 'r')->content();
  unless ($content) {
    warn "failed to load page - will try again in 2 minutes.\n";
    add_job([\&get_instructions,$c],120);
    return;
  }
  $instructions = ''; # start with a clean slate
  my $keep = 0;
  foreach my $line (split("\n",$content)) {
    if (!$keep && $line =~ m/^<!-- HBC AIV helperbot BEGIN INSTRUCTIONS -->$/) {
      $keep = 1;
      next;
    } elsif ($keep && $line =~ m/^<!-- HBC AIV helperbot END INSTRUCTIONS -->$/) {
      $keep = 0;
    }
    next unless $keep;
    $instructions .= "$line\n";
  }
  chomp($instructions);
  warn "Done, will check again in 30 minutes.\n";
  add_job([\&get_instructions,$c],1800);
}
 
sub get_ip_list
  {
  my($c) = @_;
  warn "Fetching special IP list...\n";
  my $ip_table = $c->get('User:HBC AIV helperbot/Special IPs','r')->content();
  unless ($ip_table) {
    warn "Failed to load page - will try again in 2 minutes.\n";
    add_job([\&get_ip_list,$c],120);
    return;
  }
  %special_ips = (); # Clear any old list
  foreach my $line (split("\n",$ip_table))
    {
    if ($line =~ m|^\* \[\[:Category:(.*?)\]\]$|)
      {
      $notable_cats{$1} = 1;
      next;
      }
    next unless ($line =~ m|^;(.*?):(.*)$|);
    my ($ip, $comment) = ($1, $2);
    next unless ($ip =~ m|^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:/\d{1,2})?$|);
    $special_ips{$ip} = "This IP matches the mask ($ip) in my [[User:HBC AIV helperbot/Special IPs|special IP list]] which is marked as: \"$comment\"";
    }
  warn "Done, will check again in 10 minutes.\n";
  add_job([\&get_ip_list,$c],600); # Run myself in 10 minutes
  }
 
sub merge_duplicate_reports
  {
  my ($c, $page_name) = @_;
  my $page = $c->get($page_name, 'rw'); # Get page read/write
  return unless $page->{'content'};
  my(@content) = split("\n",$page->{'content'}); # Split into lines
  my (@new_content, %user_table, $report_count, $in_comment);
  while (scalar(@content)) {
    my $line = shift(@content);
    my $bare_line;
    ($in_comment,$bare_line,undef) = comment_handler($line, $in_comment);
    next if $line eq "\n";
    if (($in_comment && ($line eq $bare_line)) || $bare_line !~ m/{{((?:ip)?vandal|userlinks|user-uaa)\|\s*(.*?)\s*}}/i)
      {
      push(@new_content,$line); next;
      }
    my $user = $2;
    if ($user =~ m/^((?:1|user)=)/i) {
      $user =~ s/^$1//i;
    }
    if ($user)
      {
      unless ($user_table{$user})
        {
        push(@new_content,$line);
        $user_table{$user} = \$new_content[scalar(@new_content)-1];
        while ((scalar(@content)) && !($content[0] =~ m/{{((?:ip)?vandal|userlinks|user-uaa)\|/i) && !($content[0] =~ m|<\!--|))
          {
          my $comment = shift(@content);
          $in_comment = comment_handler($comment, $in_comment);
          ${$user_table{$user}} .= "\n$comment"
          }
        $report_count++;
        }
      else
        {
        $line =~ s|^\*||;
        $line =~ s/{{((?:ip)?vandal|userlinks|user-uaa)\|\s*(.*?)\s*}}//i;
        ${$user_table{$user}} .= "\n:*$line <small><sup>(Moved by bot)</sup></small>";
        }
      }
  }
  my $tally;
  $tally = 'Empty.' if ($report_count == 0);
  $tally ||= ($report_count.' report'.(($report_count > 1) ? ('s remaining.') : (' remaining.')));
  $page->{'content'} = join("\n",@new_content);
  $page->{'summary'} = "$tally Duplicate entries merged";
  $page->save();
  warn "Duplicates merged: $page_name\n";
  }
 
sub parse_parameters {
  my ($parameters) = @_;
  my %result;
  foreach my $item (split(/\s+/, $parameters)) {
    my ($key, $value) = split(/=/, $item);
    $result{$key} = lc($value);
  }
 
  foreach (@desired_parameters) {
    $result{$_} ||= 'off';
  }
 
  if ($result{'AutoBacklog'} eq 'on') {
    $result{'AddLimit'} ||= 0;
    $result{'RemoveLimit'} ||= 0;
  }
 
  return \%result;
}
 
sub remove_name
  {
  my ($c,$user,$blocker,$duration,$block_type,$page_name) = @_;
  my $page = $c->get($page_name, 'rw'); # Get page read/write
  return unless $page->{'content'};
  my($ips_left,$users_left) =  ('0','0'); # Start these with 0 instead of undef
  my(@content) = split("\n",$page->{'content'}); # Split into lines
  my (@new_content, $found, $lines_skipped, $in_comment);
  while (scalar(@content)) {
    my $line = shift(@content);
    my ($bare_line,$remainder);
    ($in_comment,$bare_line,$remainder) = comment_handler($line, $in_comment);
    unless (!$in_comment && $line =~ m/{{((?:ip)?vandal|userlinks|user-uaa)\|\s*(?:1=|user=)?\Q$user\E\s*}}/i)
      {
      push(@new_content,$line);
      next if ($in_comment && ($line eq $bare_line));
      if($bare_line =~ m/{{IPvandal\|/i)
        {
        $ips_left++;
        }
      if($bare_line =~ m/{{(vandal|userlinks|user-uaa)\|/i)
        {
        $users_left++;
        }
      }
    else
      {
      $found = 1;
      push(@new_content,$remainder) if $remainder;
      while ((scalar(@content)) && !($content[0] =~ m/{{((?:ip)?vandal|userlinks|user-uaa)\|/i) && !($content[0] =~ m|^<\!--|) && !($content[0] =~ m/^=/))
        {
        my $removed = shift(@content);
        if (length($removed) > 0) {
          $lines_skipped++;
          $in_comment = comment_handler($removed, $in_comment);
        }
        }
      }
  }
  $page->{'content'} = join("\n",@new_content);
  return unless($found);                # Cancel if could not find the entry attempting to be removed.
  return unless($page->{'content'});    # Cancel if result would blank the page.
  my $length = ((defined($duration)) ? (' '.$duration) : (' '));
  $length = ' indef ' if (defined($duration) && $duration eq 'indef');
  my $tally;
  if ($ips_left || $users_left)
    {
    $tally = join(' & ',
    (
     (($ips_left) ? ($ips_left.' IP'.(($ips_left > 1) ? ('s') : (''))) : ()),
     (($users_left) ? ($users_left.' user'.(($users_left > 1) ? ('s') : (''))) : ()),
    )).' left.';
    }
  else
    {
    $tally = 'Empty.'
    }
  my $skipped = (($lines_skipped) ? (" $lines_skipped comment(s) removed.") : (''));
  $page->{'summary'} = $tally.' rm [[Special:Contributions/'.$user.'|'.$user.']] (blocked'.$length.'by [[User:'.$blocker.'|'.$blocker.']] '.$block_type.'). '.$skipped;
  $page->save();
  warn "rm '$user': $page_name\n";
  sleep($write_rate);
  }
 
sub set_backlog
  {
  my ($c, $page_name, $report_count,$ab_add,$ab_remove) = @_;
  $report_count ||= '0';
  my $page = $c->get($page_name, 'rw'); # Get page read/write
  return unless $page->{'content'};
  my(@content) = split("\n",$page->{'content'}); # Split into lines
  my(@new_content); # Place to put replacement content
  foreach my $line (@content)
    {
    if ($line =~ m|^\{\{(?:no)?adminbacklog\}\}|i)
      {
      my $tally;
      $tally = 'Empty.' if ($report_count == 0);
      $tally ||= ($report_count.' report'.(($report_count > 1) ? ('s remaining.') : (' remaining.')));
      if        ($report_count >= $ab_add)
        {
        warn "Backlog added to: $page_name\n";
        $page->{'summary'} = ($tally.' Noticeboard is backlogged.');
        $line =~ s|^\{\{noadminbacklog|\{\{adminbacklog|i;
        push (@new_content,$line);
        }
      elsif     ($report_count <= $ab_remove)
        {
        warn "Backlog removed from: $page_name\n";
        $page->{'summary'} = ($tally.' Noticeboard is no longer backlogged.');
        $line =~ s|^\{\{adminbacklog|\{\{noadminbacklog|i;
        push (@new_content,$line);
        }
      }
    else
      {
      push(@new_content,$line);
      }
    }
  $page->{'content'} = join("\n",@new_content);
  return unless($page->{'content'});
  $page->save();
  }
 
sub check_cats
  {
  my ($user) = @_;
  my (@response);
  my $url = "http://en.wikipedia.org/w/api.php?action=query&prop=categories&titles=User%20talk:".uri_escape($user)."&format=json";
  my $data = $c->{ua}->get($url)->content();
  while ($data =~ m|{"ns":14,"[^"]*":"Category:(.*?)"\}|g) # " A comment with a quote to fix a bug in syntax highlighting
    {
    if ($notable_cats{$1})
      {
      push(@response, $1);
      }
    }
  return @response;
  }
 
sub timeconv {
  my($expiry, $block_time)  = @_;
  my $duration = $expiry - $block_time;
  my $formatter = DateTime::Format::Duration->new(
    pattern => '%Y years, %m months, %e days, %H hours, %M minutes, %S seconds',
    normalize => 1,
    base => $block_time,
  );
  my %normalized = $formatter->normalize($duration);
  my @periods = ('years','months','days','hours','minutes','seconds');
  my $output;
  if ($normalized{'minutes'} || $normalized{'seconds'}) {
    $output = sprintf('until %s %s ', $expiry->ymd, $expiry->hms);
  } else {
    foreach (@periods) {
      $output .= sprintf('%s %s, ', $normalized{$_}, $_) if $normalized{$_};
      if ($normalized{$_} == 1) {
        my $singular = $_;
        $singular =~ s/s$//;
        $output =~ s/$_/$singular/;
      }
    }
    $output =~ s/, $/ /;
    # special cases
    if ($output eq '1 day, 7 hours ') {
      $output = '31 hours ';
    } elsif ($output eq '4 days, 3 hours ') {
      $output = '99 hours ';
    } elsif ($output eq '4 days, 4 hours ') {
      $output = '100 hours ';
    }
  }
  return $output;
}
 
sub mywarn {
  my ($msg) = @_;
  if ($^O eq 'MSWin32')
    {
    CORE::warn($msg);
    }
  else
    {
    CORE::warn('['.strftime('%F %T UTC',gmtime()).'] '.$msg);
    }
}