User:AnomieBOT/source/AnomieBOT/API.pm

{{ombox|text=See /doc for formatted documentation}}

package AnomieBOT::API;

use utf8;

use strict;

use JSON;

use Time::HiRes qw/time sleep/;

use LWP::UserAgent;

use Net::OAuth;

use Bytes::Random::Secure ();

use HTTP::Message;

use Date::Parse;

use File::Spec;

use IPC::Run;

use Digest::MD5 qw/md5_hex/;

use POSIX;

use Carp;

use Encode qw/encode/;

use Data::Dumper;

use DBI;

use IO::File;

use IO::Zlib;

use Cwd qw/realpath/;

use AnomieBOT::API::Iterator;

use AnomieBOT::API::TiedDBD;

use AnomieBOT::API::Cache;

use vars qw/$default_maxlag/;

$default_maxlag=$ENV{'AnomieBOT_maxlag'} // 5;

=pod

=head1 NAME

AnomieBOT::API - AnomieBOT API access class

=head1 SYNOPSIS

use AnomieBOT::API;

my $api = AnomieBOT::API->new('/path/to/config_file', 1);

$api->login();

$res=$api->query(list=>'allpages',apnamespace=>0,aplimit=>10);

=head1 DESCRIPTION

C is a class implementing various functions needed by a

MediaWiki bot.

=head1 RETURN VALUES

Unless otherwise noted, each method returns an object with certain standard

properties:

=over

=item code

A short token indicating the result of the API call. In addition to error codes

returnable by the MediaWiki API, the following may be seen:

=over

=item success

The call succeeded.

=item httperror

An HTTP error response was returned. The object will also contain a C

property containing the full HTML returned with the error, and a C

property with the HTTP response code.

=item jsonerror

The response string could not be decoded. The object will also contain a C

property containing the response string;

=item wtferror

A "This can't happen" error occurred.

=item notloggedin

The bot user could not be logged in.

=item botexcluded

Returned by token-fetching functions if the page contains a bot exclusion

template that excludes this bot. The return object will have an extra property

C with the value "bydefault", "byname", or "optout".

=item shutoff

Returned by token-fetching functions if the task's shutoff page is triggered.

=item pageprotected

Returned by token-fetching functions if the page is protected and the bot lacks

the appropriate rights.

=item pagemissing

Returned by token-fetching functions if the page does not exist (when a

non-existent page doesn't make sense).

=item uploadwarnings

Returned by the upload function if warnings were returned.

=item notiterable

Returned by C<< $api->iterator() >> when the result set cannot be iterated.

=item maxlag

If maxlag is set off or the bot is trying to halt, this error may be returned

instead of the bot automatically retrying.

=back

=item error

A textual description of the error code.

=back

=head1 METHODS

Note that this class supports runtime decoratoration, so the complete set of

available methods depends on the most recent call to C<< task() >>.

=over

=item AnomieBOT::API->new( $conffile, $botnum )

Create an instance of the bot.

The config file is simply a list of "key = value"s, one per line; any line

whose first non-whitespace character is '#' is considered a comment and is

ignored.

Sections within the file are delimited with C<< [I] >>. Anything set

before the first section is considered "defaults" for any sections named "bot

I<#>".

In addition, a file may include other files by using a line

C<< @include I I >>. Any options begin with a "!" character;

the remainder of the line is the filename. By default, the file may have its

own defaults section (which is merged with the including file's) and sections;

if the "!section" option is given, the file will be considered part of the

current section and attempting to include other files or define sections will

be considered an error. If the "!ifexists" option is given, it will not be an

error if the file doesn't actually exist.

See A for parameters and defaults. The file permissions must

not include world readability, writability, or executability.

Botnum is the instance number of this bot, which controls which of the

C<[bot #]> sections of the config file is actually used.

=cut

sub _readconf {

my ($file, $def, $CFG, $ssection) = @_;

if ( open( my $fh, '<:utf8', $file ) ) {

croak "Bad file permissions on $file" if (stat $fh)[2]&7;

my $section = $ssection // '';

while ( <$fh> ) {

next if /^\s*#/;

s/^\s+|\s+$//g;

next if $_ eq '';

if ( /^\@include\s+(.+)$/o ) {

my $newfile = $1;

if ( defined( $ssection ) ) {

croak $file.": Cannot include another file at line $.";

} else {

my %opts = ( '!section' => 0, '!ifexists' => 0 );

$opts{$1} = 1 while $newfile=~s/^(!\S+)\s+//;

croak $file.": Invalid line at line $." if $newfile eq '';

my ($volume, $dir) = File::Spec->splitpath( $file );

my $base = File::Spec->catpath( $volume, $dir, '' );

$newfile = File::Spec->rel2abs( $newfile, $base );

next if $opts{'!ifexists'} && !-e $newfile;

_readconf( $newfile, $def, $CFG, $opts{'!section'} ? $section : undef );

}

} elsif ( /^\[([^\x5b\x5d]+)\]\s*$/o ) {

if ( defined( $ssection ) ) {

croak $file.": Cannot begin a section at line $.";

} else {

$section = $1;

$CFG->{$section} //= ($section=~/^bot \d+$/) ? { %$def } : {};

}

} elsif ( /^(\S+?)\s*=\s*(.*)$/o ) {

if ( $section eq '' ) {

$def->{$1} = $2;

} else {

$CFG->{$section}{$1} = $2;

}

} else {

croak $file.": Invalid line at line $.";

}

}

close $fh;

} else {

croak "Could not open ".$file.": $!";

}

}

sub new {

my $class = shift;

my %CFG = ();

croak "USAGE: AnomieBOT::API->new(\$file, \$botnum, [\\\%opt])" unless @_==2 or @_==3;

my $botnum = $_[1];

my $opt = $_[2] // {};

my %def = ();

_readconf( $_[0], \%def, \%CFG );

carp "Bot instance number $botnum is not configured" unless exists($CFG{"bot $botnum"});

my $umask=umask;

# Check data directory

my $datadir;

if(exists($CFG{"bot $botnum"}{'datadir'})){

$datadir=$CFG{"bot $botnum"}{'datadir'};

} elsif(exists($ENV{'HOME'})){

my $d=$ENV{'HOME'};

$d.='/' unless substr($d,-1) eq '/';

$d.='.anomiebot-data/';

$datadir=$d;

} else {

die "HOME not set, please either set it or specify 'datadir' in the config file\n";

}

$datadir.='/' unless substr($datadir,-1) eq '/';

if(!-e $datadir){

umask($umask | 0007);

my $ok = mkdir($datadir);

umask($umask);

die "Data directory ".$datadir." cannot be created: $!\n" unless $ok;

}

die "Data directory ".$datadir." is not a directory\n" unless -d $datadir;

my $t=$datadir.'test'.$botnum;

if(-e $t){

unlink($t);

die "Could not remove test file $t: $!\n" if -e $t;

}

open(X, '>', $t) or die("Could not create test file $t: $!\n");

close(X);

unlink($t);

my $keep_alive = $CFG{"bot $botnum"}{'keep-alive'} // 1;

$keep_alive = undef if $keep_alive < 0;

my $cookiejar = $CFG{"bot $botnum"}{'cookiejar'} // '$DATADIR/AnomieBOT-$BOTNUM.cookies';

$cookiejar=~s/\$BOTNUM/$botnum/g;

$cookiejar=~s/\$DATADIR/$datadir/g;

umask($umask | 0007);

my $ua=LWP::UserAgent->new(

agent=>"AnomieBOT/1.0 (no task; see User:".$CFG{"bot $botnum"}{'lguser'}.")",

from=>$CFG{"bot $botnum"}{'email'} // undef,

cookie_jar=>{ file=>$cookiejar, autosave=>1 },

env_proxy=>1,

keep_alive=>$keep_alive,

);

$ua->cookie_jar->save();

umask($umask);

my $commandfile = $CFG{"bot $botnum"}{'commandfile'} // '$DATADIR/AnomieBOT-$BOTNUM.cmd';

$commandfile=~s/\$BOTNUM/$botnum/g;

$commandfile=~s/\$DATADIR/$datadir/g;

my $logfile = $CFG{"bot $botnum"}{'logfile'} // '';

$logfile=~s/\$BOTNUM/$botnum/g;

$logfile=~s/\$DATADIR/$datadir/g;

my $errfile = $CFG{"bot $botnum"}{'errfile'} // '';

$errfile=~s/\$BOTNUM/$botnum/g;

$errfile=~s/\$DATADIR/$datadir/g;

die "Cannot use other users' home directory in commandfile\n" if $commandfile=~m!^~[^/]!;

die "Cannot use other users' home directory in logfile\n" if $logfile=~m!^~[^/]!;

die "Cannot use other users' home directory in errfile\n" if $errfile=~m!^~[^/]!;

if(exists($ENV{'HOME'})){

my $home = $ENV{'HOME'};

$commandfile=~s!^~/!$home/!;

$logfile=~s!^~/!$home/!;

$errfile=~s!^~/!$home/!;

} else {

die "HOME is not set, cannot use ~/ in commandfile\n" if $commandfile=~m!^~/!;

die "HOME is not set, cannot use ~/ in logfile\n" if $logfile=~m!^~/!;

die "HOME is not set, cannot use ~/ in errfile\n" if $errfile=~m!^~/!;

}

my $memlimit = $CFG{"bot $botnum"}{'memlimit'} // '256Mi';

my $cpulimit = $CFG{"bot $botnum"}{'cpulimit'} // '250m';

my $rand = Bytes::Random::Secure->new( NonBlocking => 1 );

my $self = {

botnum => $botnum,

datadir => $datadir,

ua => $ua,

rand => $rand,

j => JSON->new->utf8(0),

task => 'no task',

store => undef,

wikibase => $CFG{"bot $botnum"}{'basepath'} // 'https://en.wikipedia.org/w/',

lguser => $CFG{"bot $botnum"}{'lguser'} // '',

lgpass => $CFG{"bot $botnum"}{'lgpass'} // '',

oauth_consumer_token => $CFG{"bot $botnum"}{'oauth_consumer_token'} // '',

oauth_consumer_secret => $CFG{"bot $botnum"}{'oauth_consumer_secret'} // '',

oauth_access_token => $CFG{"bot $botnum"}{'oauth_access_token'} // '',

oauth_access_secret => $CFG{"bot $botnum"}{'oauth_access_secret'} // '',

email => $CFG{"bot $botnum"}{'email'} // undef,

operator => $CFG{"bot $botnum"}{'operator'} // undef,

read_throttle => 0,

edit_throttle => 10,

assert => $CFG{"bot $botnum"}{'assert'} // '',

nassert => $CFG{"bot $botnum"}{'nassert'} // '',

assert_edit => $CFG{"bot $botnum"}{'assert_edit'} // 'bot',

nassert_edit => $CFG{"bot $botnum"}{'nassert_edit'} // '',

use_encodings => !($CFG{"bot $botnum"}{'disable-transfer-encodings'} // 0),

lastread => 0,

lastedit => time(),

debug => $CFG{"bot $botnum"}{'DEBUG'} // 0,

automaxlag => 1,

noedit => undef,

nopause => 0,

onpause => undef,

editlimit => undef,

decorators => [],

queryprops => undef,

queryprefix => undef,

queryparams => undef,

paramlimits => {},

edit_watchlist => 'nochange',

memlimit => $memlimit,

cpulimit => $cpulimit,

commandfile => $commandfile,

logfile => $logfile,

errfile => $errfile,

halting => 0,

replica_dsn => $CFG{"bot $botnum"}{'replica_dsn'} // '',

replica_user => $CFG{"bot $botnum"}{'replica_user'} // '',

replica_pass => $CFG{"bot $botnum"}{'replica_pass'} // '',

};

$self->{'replica_dsn'}=~s/\$BOTNUM/$botnum/g;

$self->{'replica_dsn'}=~s/\$DATADIR/$datadir/g;

if(exists($ENV{'HOME'})){

my $home = $ENV{'HOME'};

$self->{'replica_dsn'}=~s/\$HOME/$home/g;

} else {

die "HOME is not set, cannot use \$HOME in replica_dsn\n" if $self->{'replica_dsn'}=~/\$HOME/;

}

$self->{'use_oauth'} = $self->{'oauth_consumer_token'} ne '' &&

$self->{'oauth_consumer_secret'} ne '' &&

$self->{'oauth_access_token'} ne '' &&

$self->{'oauth_access_secret'} ne '';

# Create cache

$self->{'cache'} = AnomieBOT::API::Cache->create(

$CFG{"bot $botnum"}{'cache_handler'}//'Memcached',

$CFG{"bot $botnum"}{'cache_options'}//'servers=127.0.0.1:112211;namespace=ChangeMe:',

);

# Open persistant storage

$CFG{"bot $botnum"}{'store_dsn'}='dbi:SQLite:dbname=$DATADIR/AnomieBOT-$BOTNUM.db' unless exists($CFG{"bot $botnum"}{'store_dsn'});

$CFG{"bot $botnum"}{'store_dsn'}=~s/\$BOTNUM/$botnum/;

$CFG{"bot $botnum"}{'store_dsn'}=~s/\$DATADIR/$datadir/;

if(exists($ENV{'HOME'})){

my $home = $ENV{'HOME'};

$CFG{"bot $botnum"}{'store_dsn'}=~s/\$HOME/$home/g;

} else {

die "HOME is not set, cannot use \$HOME in store_dsn for bot $botnum\n" if $CFG{"bot $botnum"}{'store_dsn'}=~/\$HOME/;

}

$CFG{"bot $botnum"}{'store_user'}='' unless exists($CFG{"bot $botnum"}{'store_user'});

$CFG{"bot $botnum"}{'store_pass'}='' unless exists($CFG{"bot $botnum"}{'store_pass'});

if($opt->{'db'} // 1){

umask($umask | 0007);

$self->{'store'}=DBI->connect($CFG{"bot $botnum"}{'store_dsn'}, $CFG{"bot $botnum"}{'store_user'}, $CFG{"bot $botnum"}{'store_pass'}, { AutoCommit=>1, RaiseError=>1, mysql_auto_reconnect=>1 });

umask($umask);

die "Could not open database\n" unless $self->{'store'};

$self->{'storetask'}=undef;

$self->{'storehash'}=undef;

}

# Copy extra config sections for access by tasks

$self->{'CFG'}={};

while(my ($k,$v)=each %CFG){

next if $k=~/^bot \d+$/;

$self->{'CFG'}{$k}=$v;

}

bless $self, $class;

return $self;

}

sub loadqueryprops {

my $self=shift;

return undef if defined($self->{'queryprops'});

my $res=$self->_query(action=>'paraminfo',modules=>'query',__noassert=>1);

return $res unless $res->{'code'} eq 'success';

my @prop=();

my @all=();

for my $p (@{$res->{'paraminfo'}{'modules'}[0]{'parameters'}}) {

@prop=@{$p->{'type'}} if $p->{'name'} eq 'prop';

@all=(@all,@{$p->{'type'}}) if($p->{'name'} eq 'prop' || $p->{'name'} eq 'list' || $p->{'name'} eq 'generator' || $p->{'name'} eq 'meta');

}

@all=keys %{{ map { $_ => 1 } @all }};

my %props=();

my %prefix=();

my %params=();

while(@all){

$res=$self->_query(action=>'paraminfo',modules=>join('|',map("query+$_",splice(@all,0,50))),__noassert=>1);

return $res unless $res->{'code'} eq 'success';

for my $p (@{$res->{'paraminfo'}{'modules'}}) {

$props{$p->{'prefix'}}=$p->{'name'} if grep $_ eq $p->{'name'}, @prop;

$prefix{$p->{'name'}}=$p->{'prefix'};

$params{$p->{'name'}}={};

for my $pp (@{$p->{'parameters'}}) {

$params{$p->{'name'}}{$p->{'prefix'}.$pp->{'name'}} = $p->{'prefix'}.$pp->{'name'};

}

}

}

$self->{'queryprops'}=\%props;

$self->{'queryprefix'}=\%prefix;

$self->{'queryparams'}=\%params;

return undef;

}

=pod

=item $api->copy( %config )

Get a new AnomieBOT::API with the specified options changed.

=cut

sub copy {

my ($self, %config) = @_;

my $new = {

%$self,

storetask => undef,

storehash => undef,

%config

};

bless $new, ref $self;

return $new;

}

=pod

=item $api->DEBUG

=item $api->DEBUG( $bitmask )

Get/set the debug bitmask. When debugging is enabled, most methods will output

useful text to standard error.

Returns the old value of the bitmask.

=cut

sub DEBUG {

my $self=shift;

my $old=$self->{'debug'};

if(@_){

croak "Invalid DEBUG bitmask: $_[0]" unless $_[0]=~/^[+-]?\d+$/;

$self->{'debug'}=$_[0];

}

return $old;

}

=pod

=item $api->reopen_logs()

Reopen STDOUT and STDERR to the configured log files.

=cut

sub reopen_logs {

my $self=shift;

if($self->{'logfile'} ne ''){

open(STDOUT, '>>', $self->{'logfile'}) or die "Cannot redirect STDOUT: $!\n";

}

if($self->{'errfile'} ne ''){

my $olderr;

open($olderr, ">&STDERR") or die "Cannot dup STDERR: $!\n";

if(!open(STDERR, '>>', $self->{'errfile'})) {

# Output the error message to the old STDERR

print $olderr "Cannot redirect STDERR: $!\n";

exit 1;

}

close($olderr);

}

my $fh = select;

binmode STDOUT, ':utf8';

select STDOUT; $| = 1;

binmode STDERR, ':utf8';

select STDERR; $| = 1;

select $fh;

}

=pod

=item AnomieBOT::API::copy_truncate_file( $from, $to, $fh )

Copy file C<$from> to C<$to> (gzipping on the way), then truncate via C<$fh>.

=cut

sub copy_truncate_file {

my ( $from, $to, $fh ) = @_;

flush $fh;

unlink $to if -e $to;

my $ffh = IO::File->new( $from, 'r' );

die "Cannot rotate $from: Cannot read $from: $!\n" unless defined( $ffh );

my $tfh = IO::Zlib->new( $to, 'wb9' );

die "Cannot rotate $from: Cannot open $to: $!\n" unless defined( $tfh );

my ( $l, $buf );

while ( $l = sysread( $ffh, $buf, 8192 ) ) {

my $o = 0;

while ( $o < $l ) {

my $l2 = syswrite( $tfh, $buf, $l - $o, $o );

die "Cannot rotate $from: Write $to failed: $!\n" unless defined( $l2 );

$o += $l2;

}

}

die "Cannot rotate $from: Read $from failed: $!\n" unless defined( $l );

$tfh->close;

$ffh->close;

truncate $fh, 0 or die "Cannot truncate $from: $!\n";

seek $fh, 0, 0 or die "Cannot rewind $from: $!\n";

}

=pod

=item $api->rotate_logs()

Rotate the log files.

=cut

sub rotate_logs {

my $self=shift;

copy_truncate_file( $self->{'logfile'}, $self->{'logfile'} . '.1.gz', \*STDOUT ) if $self->{'logfile'} ne '';

copy_truncate_file( $self->{'errfile'}, $self->{'errfile'} . '.1.gz', \*STDERR ) if $self->{'errfile'} ne '';

}

=pod

=item $api->warn( $message )

=item AnomieBOT::API->warn( $message )

=item $api->log( $message )

=item AnomieBOT::API->log( $message )

=item $api->debug( $bitmask, $message )

=item AnomieBOT::API->debug( $debuglevel, $bitmask, $message )

Output messages.

=cut

sub warn {

my $self=shift;

$self={ task=>'static', botnum=>0 } unless ref($self);

my $msg=shift;

if(-t STDERR){

my $nl='';

$nl="\n" if $msg=~s/\n$//;

carp "\e[31m".POSIX::strftime('[%F %T] ', localtime).$self->{'task'}.' ('.$self->{'botnum'}."): $msg\e[0m$nl";

} else {

carp POSIX::strftime('W [%F %T] ', localtime).$self->{'task'}.' ('.$self->{'botnum'}.'): '.$msg;

}

}

sub log {

my $self=shift;

$self={ task=>'static', botnum=>0 } unless ref($self);

my $msg=shift;

print POSIX::strftime('[%F %T] ', localtime).$self->{'task'}.' ('.$self->{'botnum'}.'): '.$msg."\n";

}

sub debug {

my $self=shift;

$self={ task=>'static', botnum=>0, debug=>shift } unless ref($self);

my $mask=shift;

my $msg=shift;

if($self->{'debug'} & $mask){

if(-t STDERR){

my $nl='';

$nl="\n" if $msg=~s/\n$//;

carp "\e[33m".POSIX::strftime('[%F %T] ', localtime).$self->{'task'}.' ('.$self->{'botnum'}."): $msg\e[0m$nl";

} else {

carp POSIX::strftime('D [%F %T] ', localtime).$self->{'task'}.' ('.$self->{'botnum'}.'): '.$msg;

}

}

}

=pod

=item $api->check_commands()

=item $api->check_commands( $file )

Check if there are any pending commands for this instance. Returns the command,

if any, unless the command was "KILL".

An optional filename may be passed to override the normal command file.

=item $api->read_command()

=item $api->read_command( $file )

=item $api->read_command( $file, $nodelete )

Read any pending command; to be called on startup. Returns the command, if

any. The pending command is cleared, unless it matches the C<$nodelete>

regexp.

An optional filename may be passed to override the normal command file.

=item $api->send_command( $file, $command )

Sends a command to the specified file, overwriting any pending command (if

any). Returns true on success, undef on failure.

=item $api->onpause()

=item $api->onpause( \&sub )

Get/set a subroutine to be called when the "pause" command is received. The

subroutine will be passed a single argument, 1 when the pause command is

received and 0 when it is cleared.

Returns the old subroutine, if any.

=cut

sub onpause {

my $self = shift;

my $ret = $self->{'onpause'};

if ( @_ ) {

$self->{'onpause'} = shift;

}

return $ret;

}

sub check_commands {

my $self = shift;

my $file = shift // $self->{'commandfile'};

my $cmd = $self->read_command( $file, qr/^pause$|^restart-hash/ );

return undef unless defined( $cmd );

local $self->{'task'} = '[command processor]';

if($cmd =~ '^restart-hash ([0-9a-f]{40})$'){

my $hash = $1;

my $curhash = '';

my $t = IPC::Run::timeout( 5 );

$t->exception( 'git rev-parse timed out' );

eval {

IPC::Run::run( [qw(git rev-parse HEAD)], '>', \$curhash, $t ) or die "Git rev-parse failed with code $?\n";

};

if ( $@ ) {

$self->warn( $@ );

return undef;

}

chomp $curhash;

if ( $curhash eq $hash ) {

$self->warn("Restarting to update to git hash $hash!");

$self->{'halting'}=$cmd;

$self->read_command( $file, qr/^(?!\Q$cmd\E\s*$)/ );

return $cmd;

}

return undef;

} elsif($cmd eq 'restart'){

$self->warn("Restart signal received!");

$self->{'halting'}=$cmd;

} elsif($cmd eq 'term'){

$self->warn("Halt signal received!");

$self->{'halting'}=$cmd;

} elsif($cmd eq 'kill'){

$self->warn("Kill signal received!");

exit(0);

} elsif($cmd eq 'pause'){

if($self->{'nopause'}){

#$self->warn("Ignoring pause signal");

#$self->send_command( $file, undef );

} else {

$self->warn("Pause signal received! Pausing until $file is changed.");

$self->{'onpause'}(1) if $self->{'onpause'};

while($cmd eq 'pause'){

sleep(1);

$cmd = $self->read_command( $file, qr/^/ ) // '';

}

$self->warn("Pause signal cleared!");

$self->{'onpause'}(0) if $self->{'onpause'};

return $self->check_commands( $file );

}

} elsif($cmd eq 'rotatelogs'){

$self->log("Rotating log files");

$self->warn("Rotating log files");

$self->rotate_logs();

$self->log("Log files rotated");

$self->warn("Log files rotated");

} elsif($cmd eq 'reopen'){

$self->log("Reopening log files");

$self->warn("Reopening log files");

$self->reopen_logs();

$self->log("Log files reopened");

$self->warn("Log files reopened");

} elsif($cmd eq 'ping'){

$self->log("Pong!");

$self->warn("Pong!");

} elsif($cmd =~ /^debug\s+([+-]?\d+)$/){

my $old = $self->DEBUG($1);

$self->warn("Set DEBUG = $1, was $old\n");

} elsif($cmd =~ /^debug(?:\s.*)?$/){

$self->warn("Invalid 'debug' command");

} else {

$self->warn("Received unknown command '$cmd'\n");

}

return $cmd;

}

sub read_command {

my $self = shift;

my $file = shift // $self->{'commandfile'};

my $nodelete = shift // qr/^(?!)/;

my $cmd = undef;

if ( $file =~ /^cache:(.*)/ ) {

my $key = $1;

$cmd = $self->cache->get( $key );

$self->cache->delete( $key ) if defined( $cmd ) && $cmd !~ /$nodelete/;

} else {

my $fh;

if(open($fh, "<", $file)) {

$cmd = <$fh>;

close($fh);

$cmd=~s/^\s+|\s+$//g;

unlink( $file ) if $cmd !~ /$nodelete/;

}

}

return $cmd;

}

sub send_command {

my $self = shift;

my $file = shift;

my $cmd = shift;

if ( $file =~ /^cache:(.*)/ ) {

if ( defined( $cmd ) ) {

$self->cache->set( $1, $cmd );

} else {

$self->cache->delete( $1 );

}

} else {

if ( defined( $cmd ) ) {

my $fh;

return undef unless open($fh, ">", $file);

print $fh $cmd;

close($fh);

} else {

unlink( $file );

}

}

return 1;

}

=pod

=item $api->halting

Process commands, then return a true value if the bot is supposed to halt.

=item $api->halting( $value )

Set the return value for future calls to halting().

=cut

sub halting {

my $self = shift;

if(@_){

$self->{'halting'} = shift;

} else {

$self->check_commands();

}

return $self->{'halting'};

}

=pod

=item $api->automaxlag

=item $api->automaxlag( $bool )

Get/set the auto-continue maxlag flag. Note that task() resets this.

Returns the old value of the flag.

=cut

sub automaxlag {

my $self=shift;

my $old=$self->{'automaxlag'};

$self->{'automaxlag'}=$_[0]?1:0 if(@_);

return ($old && !$self->halting);

}

=pod

=item $api->edit_watchlist

=item $api->edit_watchlist( $value )

Get/set the editing C value. Valid values are specified by the API,

currently C, C, C, or C. The default is

C.

Returns the old value.

=cut

sub edit_watchlist {

my $self=shift;

my $old=$self->{'edit_watchlist'};

$self->{'edit_watchlist'}=$_[0] if(@_);

return $old;

}

=pod

=item $api->user

Returns the bot user name.

=item $api->operator

Returns the bot operator's user name.

=cut

sub user {

my $self=shift;

return $self->{'lguser'};

}

sub operator {

my $self=shift;

return $self->{'operator'};

}

=pod

=item $api->task

=item $api->task( $name )

=item $api->task( $name, $read_rate, $write_rate, @decoraters )

Get/set the current task.

When called with no arguments, simply returns the current task name.

With arguments, it sets the task name, read/write rate limits, and the current

list of decorators. If omitted, it defaults to 0 seconds per read, 10 seconds

per write, and no decorators.

=item $api->read_throttle

=item $api->read_throttle( $seconds )

Get/set the current read throttle time. If a read is attempted less than

$seconds seconds after a previous read or edit, the bot will sleep for the

remaining time.

Returns the old throttle.

=item $api->edit_throttle

=item $api->edit_throttle( $seconds )

Get/set the current edit throttle time. If an edit is attempted less than

$seconds seconds after a previous read or edit, the bot will sleep for the

remaining time.

Returns the old throttle.

=item $api->decorators

=item $api->decorators( @decorators )

Get/set the current list of decorators. Note that functions are first searched

for in the current object (thus a decorator cannot override native functions),

then in each decorator I. If you want to set an empty list of

decorators, pass undef as the only argument.

Returns the old list of decorators.

=cut

sub task {

my $self=shift;

my $old=$self->{'task'};

if(@_){

$self->{'task'}=shift;

$self->{'ua'}->agent("AnomieBOT/1.0 (".encode('UTF-8',$self->{'task'})."; see User:".$self->{'lguser'}.")");

$self->debug(1, 'Beginning task');

$self->read_throttle(shift // 0);

$self->edit_throttle(shift // 10);

$self->decorators(@_ ? @_ : undef);

$self->automaxlag(1);

}

return $old;

}

sub read_throttle {

my $self=shift;

my $old=$self->{'read_throttle'};

if(@_){

my $n=shift;

if($n!~/^(?:\d*\.)?\d+$/ || $n<0){

carp "Time value for read_throttle must be a non-negative floating point number.";

} else {

$self->{'read_throttle'}=0.0+$n;

$self->debug(1, "Read throttle set to $n seconds");

}

}

return $old;

}

sub edit_throttle {

my $self=shift;

my $old=$self->{'edit_throttle'};

if(@_){

my $n=shift;

if($n!~/^(?:\d*\.)?\d+$/ || $n<0){

carp "Time value for edit_throttle must be a non-negative floating point number.";

} else {

$self->{'edit_throttle'}=0.0+$n;

$self->debug(1, "Edit throttle set to $n seconds");

}

}

return $old;

}

sub decorators {

my $self=shift;

my $old=$self->{'decorators'};

if(@_){

$self->{'decorators'}=[defined($_[0])?@_:()];

$self->debug(1, "Decorators: ".join(', ', @{$self->{'decorators'}}));

load($_) foreach (@{$self->{'decorators'}});

}

return @$old;

}

sub _throttle {

my $self=shift;

my $which=shift;

my $t;

if($which eq 'read'){

$t=$self->{'read_throttle'}-(time()-$self->{'lastread'});

sleep($t) if $t>0;

$self->{'lastread'}=time();

} elsif($which eq 'edit'){

$t=$self->{'edit_throttle'}-(time()-$self->{'lastedit'});

sleep($t) if $t>0;

$self->{'lastedit'}=time();

$self->{'lastread'}=$self->{'lastedit'};

}

}

=pod

=item $api->is_trial

Returns a true value if a trial is running. This can be used to disable an

unapproved code addition in the live code while running it for trial.

=cut

sub is_trial {

my $self=shift;

return defined($self->{'editlimit'});

}

=pod

=item $api->CFG

Access the configuration settings for the current task. The most common use

will be along the lines of C<< $api->CFG->{$property} >>.

=cut

sub CFG {

my $self=shift;

$self->{'CFG'}{$self->{'task'}}={} unless exists($self->{'CFG'}{$self->{'task'}});

return $self->{'CFG'}{$self->{'task'}};

}

=pod

=item $api->store

Returns a hashref tied to persistant storage corresponding to the current task,

or undef if no task is set.

Since this is tied to persistant storage, only scalars (no scalar refs),

hashrefs, and arrayrefs may be stored in the array. Anything else will cause a

fatal error.

=cut

sub store {

my ($self) = @_;

return undef unless defined($self->{'task'});

if(!defined($self->{'storetask'}) || $self->{'task'} ne $self->{'storetask'}){

untie %{$self->{'storehash'}};

$self->{'storetask'}=$self->{'task'};

tie %{$self->{'storehash'}}, 'AnomieBOT::API::TiedDBD', $self->{'store'}, 'AnomieBOT_Store', 'k', 'v', task => $self->{'storetask'};

}

return $self->{'storehash'};

}

=pod

=item $api->cache

Returns an A object, for non-persistant data storage.

=cut

sub cache {

my ($self) = @_;

return $self->{'cache'};

}

=pod

=item $api->connectToReplica( $wiki )

=item $api->connectToReplica( $wiki, $svc )

Calls C<< DBI->connect >> and returns the database handle.

If C is not set in conf.ini, dies instead.

C<$svc> replaces a C<$SVC> token in C. For Tool Forge, pass 'analytics' or 'web'. Defaults to 'web'.

=cut

sub connectToReplica {

my $self = shift;

my $wiki = shift;

my $svc = shift || 'web';

my $dsn = $self->{'replica_dsn'};

carp "'replica_dsn' is not set\n" if $dsn eq '';

$dsn=~s/\$WIKI/$wiki/g;

$dsn=~s/\$SVC/$svc/g;

my $dbh = DBI->connect($dsn, $self->{'replica_user'}, $self->{'replica_pass'}, { AutoCommit=>1, RaiseError=>1, mysql_auto_reconnect=>1 });

return wantarray ? ($dbh) : $dbh;

}

=pod

=item $api->drop_connections

Drops any connections in the Keep-Alive state. Call this if you will not be

making API calls for the next 300 seconds.

=cut

sub drop_connections {

my $self=shift;

$self->{'ua'}->conn_cache->drop();

}

=pod

=item $api->rawpage( $title )

=item $api->rawpage( $title, $oldid )

Get the raw wikitext of a page, specified by title and (optionally) revision

id. The return object has the following additional properties:

=over

=item content

Content of the page

=back

As with C, this method may pause for read throttling or maxlag errors.

=cut

sub rawpage {

my @args=@_;

my $self = shift @args;

my $title = shift @args;

my %q = @args ? ( 'revids' => $args[0] ) : ( 'titles' => $title, 'rvlimit' => 1 );

my $res = $self->query(

prop => 'revisions',

rvprop => 'content',

rvslots => 'main',

%q,

);

if ( $res->{'code'} eq 'success' ) {

return {

code => 'success',

error => 'Success',

content => (values %{$res->{'query'}{'pages'}})[0]{'revisions'}[0]{'slots'}{'main'}{'*'} // '',

};

} else {

return $res;

}

}

sub _ISO2wptime {

my $t=shift;

return $1.$2.$3.$4.$5.$6

if $t=~/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/;

}

sub _query {

my ($self, %param) = @_;

my $nolog=(exists($param{'__nolog'}) && $param{'__nolog'});

my $errok=(exists($param{'__errok'}) && $param{'__errok'});

my $noassert=(exists($param{'__noassert'}) && $param{'__noassert'});

my $T272319_retries = $param{'__T272319_retries'} // 0;

delete $param{'__nolog'};

delete $param{'__errok'};

delete $param{'__noassert'};

delete $param{'__T272319_retries'};

my $raw=exists($param{'format'});

$param{'format'}='json' unless $raw;

$param{'maxlag'}=$default_maxlag unless exists($param{'maxlag'});

$param{'rawcontinue'}=1 if($param{'action'} eq 'query' && !exists($param{'continue'}));

$param{'assert'}=$self->{'assert'} if(!$noassert && !exists($param{'assert'}) && $self->{'assert'} ne '');

$param{'nassert'}=$self->{'nassert'} if(!$noassert && !exists($param{'nassert'}) && $self->{'nassert'} ne '');

# Convert for logging before the utf8::encode().

my $q;

if($nolog){

$q='[unlogged]';

} else {

$q=Dumper(\%param);

$q=~s/\n\s*/ /g;

}

my %h=(

'Content_Type' => 'form-data',

#'X-Wikimedia-Debug' => 'backend=mwdebug1001.eqiad.wmnet; log',

);

while(my ($k,$v)=each %param){

utf8::encode($param{$k}) if(!ref($v));

}

if($self->{'use_encodings'}) {

my $can_accept=HTTP::Message::decodable;

$h{'Accept-Encoding'}=$can_accept if $can_accept;

}

my $url = $self->{'wikibase'}.'api.php';

if ( $self->{'use_oauth'} ) {

my $nonce = sprintf( '%x.%s.%08x', $$, $self->{'rand'}->bytes_base64( 15 ), (time() * 1e6) & 0xffffffff );

$nonce =~ y!+/\n!-_!d;

# Note: POST data isn't included here because AnomieBOT always uses

# multipart/form-data

my $request = Net::OAuth->request( 'protected resource' )->new(

request_method => 'POST',

request_url => $url,

consumer_key => $self->{'oauth_consumer_token'},

consumer_secret => $self->{'oauth_consumer_secret'},

token => $self->{'oauth_access_token'},

token_secret => $self->{'oauth_access_secret'},

signature_method => 'HMAC-SHA1',

timestamp => int(time()),

nonce => $nonce,

);

$request->sign;

$h{'Authorization'} = $request->to_authorization_header;

}

my $res=$self->{'ua'}->post($url, \%param, %h);

if($res->code!=200){

$self->warn("Query failed: ".$res->status_line);

#$self->warn("Failed query was $q");

return {

code => 'httperror',

httpcode => $res->code,

error => $res->status_line,

page => $res->decoded_content

};

}

if($raw){

$self->debug(1, "Query $q");

return {

code => 'success',

error => 'Success',

content => $res->decoded_content

};

}

my $ret;

eval { $ret=$self->{'j'}->utf8->decode($res->decoded_content // ''); };

if($@){

$self->warn("JSON decoding failed: $@");

#$self->warn("Failed query was $q");

return {

code => 'jsonerror',

error => $@,

page => $res->decoded_content

};

}

$ret={ '*' => $ret } if ref($ret) ne 'HASH';

if($ret->{'error'}){

if($ret->{'error'}{'code'} eq 'maxlag'){

$ret=$self->_handle_maxlag($res, $ret->{'error'}{'info'});

return $ret if defined($ret);

goto \&_query;

} elsif($ret->{'error'}{'code'} =~ /^assert.*failed/){

# Assertion failed. Maybe we're not logged in?

my $ret2=$self->_query(action=>'query', meta=>'userinfo', maxlag => 600, __noassert => 1);

return {

code => $ret->{'error'}{'code'},

error => $ret->{'error'}{'info'}

} if($ret2->{'code'} eq 'success' && $ret2->{'query'}{'userinfo'}{'name'} eq $self->{'lguser'});

$ret2 = $self->login(1);

return $ret2 unless $ret2->{'code'} eq 'success';

goto \&_query;

} elsif($ret->{'error'}{'code'} eq 'mwoauth-invalid-authorization' && $ret->{'error'}{'info'} =~ /Nonce already used:/ && $T272319_retries++ < 10){

# Wikimedia has been having major problems with this lately, see T272319

$self->debug(1, "Retrying (#$T272319_retries) to work around T272319");

push @_, '__T272319_retries', $T272319_retries;

sleep(0.01);

goto \&_query;

} else {

unless(ref($errok) eq 'ARRAY' && grep $_ eq $ret->{'error'}{'code'}, @$errok){

$self->debug(1, "Query $q");

$self->warn("MediaWiki error: ".$ret->{'error'}{'info'});

#$self->warn("Failed query was $q");

}

my %ret2 = %{$ret->{'error'}};

$ret2{'servedby'} = $ret->{'servedby'}//'unknown';

$ret2{'error'} = $ret->{'error'}{'info'};

delete($ret2{'info'});

return \%ret2;

}

}

if($ret->{'warnings'}){

$self->debug(1, "Query $q");

while ( my ($module, $warning) = each (%{$ret->{'warnings'}}) ) {

$self->warn("MediaWiki warning: $module: " . $warning->{'*'});

}

}

$self->debug(1, "Query $q");

$ret->{'code'}='success';

$ret->{'error'}='Success';

return $ret;

}

=pod

=item $api->query( key => value, ... )

=item $api->query( \@continues, key => value, ... )

Perform a general MediaWiki API query.

As you should be aware, if an L API call is going to

return an excessive amount of data, only the first bit will be returned and

various continuation parameters will be returned to get the next bit in a

future call. The array reference C<\@continues> specifies how to handle that:

those multiple calls will be made automatically for all modules I named.

Note that you will get errors if your query uses a generator and you specify

any non-generator modules without also specifying the generator module. To

specify the generator module, prefix the module name with "g|". If for some

reason you need to specify the continuation parameter explicitly, join it to

the module name with a "|" (e.g. "revisions|rvcontinue"); note this is not

recommended.

If C<\@continues> is not provided, the default value consists of the

generator or list modules specified in the query, plus "revisions|rvcontinue"

(we don't want to download all several-thousand revisions automatically when

prop=revisions is in enum mode).

The keys and values are those needed for the API call, with the following

exceptions:

=over

=item action

If omitted, "query" will be assumed. Do not use "login", "edit", "move", or

others that have a method provided.

=item format

If not specified, the json-format response will be decoded as a Perl object,

the standard properties will be added, and the object will be returned. This is

most likely what you want.

If specified (even as "json"), the raw response text will be returned in the

C property of the return object. MediaWiki errors will not be

detected.

=item maxlag

If unspecified, the default value "5" will be used. Maxlag errors are (usually)

automatically retried.

=item __nolog

If specified and true, the query will not be output to the bot log (when

debugging is active). Use this if the query contains passwords or other

sensitive information.

=item __errok

Specify an array of MediaWiki error codes that will not be logged. Use this if

you expect the query might fail and don't want to clutter the log. For example,

you might set C<['editconflict']> when calling L if

you were expecting edit conflicts.

=back

The return value is normally the API response object, but see above for

details.

=cut

sub query {

my $self = shift;

my (@continues, %param);

if(ref($_[0]) eq 'ARRAY'){

@continues = @{shift()};

%param = @_;

} else {

%param = @_;

@continues = ('revisions|rvcontinue');

push @continues, $param{'list'} if exists($param{'list'});

push @continues, 'g|'.$param{'generator'} if exists($param{'generator'});

}

$param{'action'}='query' unless exists($param{'action'});

if(grep $param{'action'} eq $_, qw/edit move login logout upload/){

my $e="Use AnomieBOT::API->".$param{'action'}." instead of passing action=".$param{'action'}." to AnomieBOT::API->query";

carp $e;

return {

code => 'params',

error => $e

};

}

# Split the query prop/list/meta params into several sets

my $qpres=$self->loadqueryprops;

return $qpres if defined($qpres);

my %propparams = ();

my %listparams = ();

my %map = ();

if ($param{'action'} eq 'query') {

%map = (

prop => \%propparams,

list => \%listparams,

meta => \%listparams,

);

while (my ($prop, $list) = each %map) {

if (($param{$prop}//) ne ) {

$list->{$prop} = $param{$prop};

delete($param{$prop});

for my $p (split /\|/, $list->{$prop}) {

my $pp = $self->{'queryparams'}{$p};

while (my ($k,$v) = each(%param)) {

next unless exists($pp->{$k});

$list->{$k} = $v;

delete($param{$k});

}

}

}

}

}

# Is a query, so loop over specified continuations. To do it right, we need

# nested loops: the inner loop runs over all non-generator continuations to

# completion, and the outer loop runs over the generator continuation.

my $ret={};

my %c2=();

my %retc=();

my %savepropparams = %propparams;

do {

# First, do non-generators

my %gc=();

my %c=();

do {

# Perform query: passed params, the "prop=??" value, and continues

$self->_throttle('read');

my $res=$self->_query(%param, %propparams, %listparams, %c, %c2);

return $res if($res->{'code'} ne 'success');

# Process query-continue

%c=();

my %p=();

if(exists($res->{'query-continue'})){

my $qpres=$self->loadqueryprops;

return $qpres if defined($qpres);

my %prefixes=%{$self->{'queryprefix'}};

while(my ($p,$n)=each(%{$res->{'query-continue'}})){

my $prefix=$prefixes{$p}//'**';

while(my ($k,$v)=each(%$n)){

my $g=0;

if(substr($k,0,length($prefix)) eq $prefix){

# Normal parameter

$g=grep(($p eq $_ || "$p|$k" eq $_), @continues);

} elsif(substr($k,0,length($prefix)+1) eq "g$prefix"){

# Generator parameter; at this point, don't continue it.

$g=1;

} else {

# WTF?

$self->warn("Got continuation parameter $k for module $p");

}

if($g){

# Return this one to the caller

$gc{$p}={} if(!exists($gc{$p}));

$gc{$p}{$k}=$v if(!exists($gc{$p}{$k}));

if($gc{$p}{$k} ne $v){

my $e="$p.$k of $v doesn't match previous call's value of ".$gc{$p}{$k}."!";

CORE::warn("\e[31;01m$e\e[0m");

return {

code => 'wtferror',

error => $e,

};

}

} else {

# Continue on this one

$c{$k}=$v;

$p{$p}=1;

}

}

}

delete $res->{'query-continue'};

}

# Update prop/meta/list", only keep the ones we're continuing on

while (my ($prop, $list) = each %map) {

next if ($list->{$prop}//) eq ;

my @keep = ();

for my $pn (split /\|/, $list->{$prop}) {

if (exists($p{$pn})) {

push @keep, $pn;

} else {

for my $k (keys %{$self->{'queryparams'}{$pn}}) {

delete($list->{$k});

}

}

}

if ( @keep ) {

$list->{$prop} = join('|', @keep);

} else {

delete($list->{$prop});

}

}

# Merge queries

_mergequeries($ret, $res);

} while(%c);

# Now, continue any generators that need continuing

%c2=();

if(%gc){

my %prefixes=%{$self->{'queryprefix'}};

while(my ($p,$n)=each(%gc)){

my $prefix=$prefixes{$p}//'**';

while(my ($k,$v)=each(%$n)){

my $g=0;

if(substr($k,0,length($prefix)) eq $prefix){

# Normal parameter

$g=grep(($p eq $_ || "$p|$k" eq $_), @continues);

} elsif(substr($k,0,length($prefix)+1) eq "g$prefix"){

# Generator parameter

$g=grep(("g|$p" eq $_ || "g|$p|$k" eq $_), @continues);

} else {

# WTF?

$self->warn("Got continuation parameter $k for module $p");

}

if($g){

# Return this one to the caller

$retc{$p}={} if(!exists($retc{$p}));

$retc{$p}{$k}=$v if(!exists($retc{$p}{$k}));

if($retc{$p}{$k} ne $v){

my $e="$p.$k of $v doesn't match previous call's value of ".$retc{$p}{$k}."!";

CORE::warn("\e[31;01m$e\e[0m");

return {

code => 'wtferror',

error => $e,

};

}

} else {

# Continue on this one

$c2{$k}=$v;

}

}

}

# Reset the prop parameters for the generator continuation

%propparams = %savepropparams;

}

} while(%c2);

# Add the returnable continue parameters, if any

$ret->{'query-continue'}=\%retc if %retc;

return $ret;

}

sub _mergequeries {

my ($to,$from) = @_;

while(my ($k,$v)=each(%$from)){

my $r=ref($v);

next if($r eq 'ARRAY' && @$v == 0);

if(!exists($to->{$k})){

$to->{$k}=$v;

} elsif(ref($to->{$k}) ne $r){

CORE::warn("\e[31;01mRef mismatch, '".ref($to->{$k})."' ne '$r'!\e[0m");

} elsif(!$r){

$to->{$k}=$v;

} elsif($r eq 'HASH'){

_mergequeries($to->{$k}, $v);

} elsif($r eq 'ARRAY'){

push @{$to->{$k}}, @$v;

} else {

CORE::warn("\e[31;01mUnknown ref type '$r'!\e[0m");

}

}

}

=pod

=item $api->iterator( key => value, ... )

This function returns an iterator object which iterates over the results of a

query. Use it something like this:

my $iter = $api->iterator( %q )

while(my $res = $iter->next()){

# Do stuff

}

Note that, in order to be iterable, the query must return exactly one node

under C<< $res->{'query'} >>, which must be an arrayref or a hashref.

In addition, exactly one value in the query may be an arrayref. In this case,

the query will be iterated to completion in turn for each of the values. For

example,

$api->iterator( titles => ['foo', 'bar', 'baz'], prop => 'info' )

will first query the info for foo, then bar, then baz.

See A for more information.

=cut

sub iterator {

return AnomieBOT::API::Iterator->new(@_);

}

=pod

=item $api->paramLimit( $module, $param )

Return the multi-value limit for a parameter.

Returns the limit as an integer, or 0 if there is no limit defined, or an API

error response on error.

=cut

sub paramLimit {

my ($self, $module, $param) = @_;

unless ( exists($self->{'paramlimits'}{$module}) ) {

my $res=$self->_query(action=>'paraminfo', modules=>$module);

return $res unless $res->{'code'} eq 'success';

for my $p (@{$res->{'paraminfo'}{'modules'}[0]{'parameters'}}) {

$self->{'paramlimits'}{$module}{$p->{'name'}} = +$p->{'limit'} if exists($p->{'limit'}) && $p->{'limit'}=~/^\d+$/;

}

}

return $self->{'paramlimits'}{$module}{$param} // 0;

}

=pod

=item $api->login()

=item $api->login( $force )

Try to log the bot in.

Note that the MediaWiki API doesn't actually return an error when the login

fails, but it does return a C property indicating success or failure.

This is translated into a 'notloggedin' error code.

By default, login will return an empty success response if it determines that

the bot is already logged in. You can override this by specifying a

L value for C<$force>.

=cut

sub login {

my ($self, $force) = @_;

my ($ret);

return {

code => 'notconfigured',

error => 'No password is configured, cannot log in',

} if $self->{'lgpass'} =~ /^\s*$/;

if(!$force){

$self->_throttle('read');

$ret=$self->_query(action=>'query', meta=>'userinfo', maxlag => 600, __noassert => 1);

return {

code => 'success',

error => 'Already logged in',

} if($ret->{'code'} eq 'success' && $ret->{'query'}{'userinfo'}{'name'} eq $self->{'lguser'});

}

$self->_throttle('read');

my $r=$self->_query(

action => 'query',

meta => 'tokens',

type => 'login',

__noassert => 1,

);

return $r if($r->{'code'} ne 'success');

my %q=(

__nolog => 1,

__noassert => 1,

action => 'login',

lgname => $self->{'lguser'},

lgpassword => $self->{'lgpass'},

maxlag => 600,

);

$q{'lgtoken'} = $r->{'query'}{'tokens'}{'logintoken'} if exists( $r->{'query'}{'tokens'}{'logintoken'} );

$self->_throttle('read');

for(my $loops=0; $loops<2; $loops++){

$ret=$self->_query(%q);

$self->{'ua'}->cookie_jar->save();

$self->warn("Login failed ($ret->{code})") if($ret->{'code'} ne 'success');

return $ret if($ret->{'code'} ne 'success');

return $ret if($ret->{'login'}{'result'} eq 'Success');

# Not really logged in. Did we get a login token?

if($ret->{'login'}{'result'} eq 'NeedToken'){

if(!exists($ret->{'login'}{'token'})){

$ret->{'code'}='notloggedin';

$ret->{'error'}='MediaWiki reported NeedToken but did not give us one';

}

$q{'lgtoken'}=$ret->{'login'}{'token'};

redo;

}

# Not really logged in. Did MW say to wait?

my $w=0;

$w=$ret->{'login'}{'wait'} if exists($ret->{'login'}{'wait'});

last if $w<=0;

# Yes they did, do so and try again.

$self->warn("Login failed (".($ret->{'login'}{'result'} // '"success"').") with a wait time, waiting $w seconds");

sleep($w);

}

# Too many retries failed. Just error out now.

$ret->{'code'}='notloggedin';

$ret->{'error'}='MediaWiki reported '.($ret->{'login'}{'result'} // '"success"').', but did not return a login token or a wait time.';

return $ret;

}

=pod

=item $api->logout()

Log the bot out.

=cut

sub logout {

my ($self) = @_;

my ($ret);

$self->_throttle('read');

$ret = $self->_query( action => 'query', meta => 'tokens|userinfo', maxlag => 600, __noassert => 1);

return $ret if $ret->{'code'} ne 'success';

return {

code => 'success',

error => 'Already logged out',

} if exists( $ret->{'query'}{'userinfo'}{'anon'} ) || exists( $ret->{'query'}{'userinfo'}{'temp'} );

$ret = $self->_query( action=>'logout', token => $ret->{'query'}{'tokens'}{'csrftoken'}, __noassert => 1 );

$self->{'ua'}->cookie_jar->save();

return $ret;

}

=pod

=item $api->gettoken( $type )

=item $api->gettoken( $type, %options )

Obtain a token of the specified type (see the C module). Available

options are:

=over

=item Title

In addition to the token, fetch information for the specified title.

=item Redir

Follow redirects, if C is provided.</p> <p>=item NoShutoff</p> <p>Do not check the shutoff page. Don't do this frivolously.</p> <p>=item OptOut</p> <p>If the intention of this edit is to leave a notification on a user's talk page,</p> <p>set C<Title> and set this to the appropriate token as detailed at</p> <p>L<en:Template:bots#Message_notification_opt_out>.</p> <p>=item NoExclusion</p> <p>Do not check for {{tl|bots}} / {{tl|nobots}} in C<Title>. Don't do this frivolously.</p> <p>=item links</p> <p>=item images</p> <p>=item templates</p> <p>=item categories</p> <p>=item [etc]</p> <p>If specified, the corresponding API C<prop> module will be included in the</p> <p>query. If the value is a hash reference, the key-value pairs will be</p> <p>interpreted as the needed parameters for the module.</p> <p>For example, C<< links => 1, categories => { show => 'hidden' } >> will include</p> <p>all the page links and the hidden categories in the returned object.</p> <p>Valid keys are all L<prop modules|mw:API:Properties>.</p> <p>C<revisions> and C<info> are always queried, with</p> <p>C<rvprop=ids|timestamp|content|flags|user|size|comment|tags>, C<rvslots=*>, and</p> <p>C<inprop=protection|talkid|subjectid>.</p> <p>=back</p> <p>The object returned will have properties C<code> and C<error> as usual,</p> <p>C<token> holding the token, C<curtimestamp> holding the timestamp, C<rights></p> <p>holding the current user's rights, and if C<Title> was specified the properties</p> <p>returned for a query of that title.</p> <p>If the bot is not logged in, C<login(1)> will be automatically attempted; if it</p> <p>fails, an error code 'notloggedin' will be returned. If the current user cannot</p> <p>edit the wiki, a 'notallowed' error code will be returned. If the task's</p> <p>shutoff page (User:I<botname>/shutoff/I<task>) is non-empty, an error code</p> <p>'shutoff' will be returned.</p> <p>=cut</p> <p>sub gettoken {</p> <p>my ($self, $toktype, %options) = @_;</p> <p>my ($r);</p> <p>my $shutoff='User:'.$self->{'lguser'}.'/shutoff/'.$self->{'task'};</p> <p>for(my $loops=0; $loops<2; $loops++){</p> <p>my %q=(</p> <p>meta => 'tokens|userinfo',</p> <p>uiprop => 'rights',</p> <p>type => $toktype,</p> <p>curtimestamp => 1,</p> <p>);</p> <p>if(!($options{'NoShutoff'} // 0) || exists($options{'Title'})) {</p> <p>%q = ( %q,</p> <p>prop => 'info|revisions',</p> <p>inprop => 'protection|talkid|subjectid',</p> <p>rvprop => 'ids|timestamp|content|flags|user|size|comment|tags',</p> <p>rvslots => '*',</p> <p>);</p> <p>$q{'redirects'}=1 if $options{'Redir'} // 0;</p> <p>my @titles = ();</p> <p>push @titles, $shutoff unless $options{'NoShutoff'} // 0;</p> <p>if ( exists($options{'Title'}) ) {</p> <p>push @titles, $options{'Title'};</p> <p>my $res=$self->loadqueryprops;</p> <p>return $res if defined($res);</p> <p>my %prop=%{$self->{'queryprops'}};</p> <p>my ($k,$v);</p> <p>while(my ($p,$n)=each %prop){</p> <p>next unless exists($options{$n});</p> <p>my $opts = $options{$n};</p> <p>$opts = {} unless ref($opts) eq 'HASH';</p> <p>if ( $n eq 'info' ) {</p> <p>while (($k,$v)=each %$opts) {</p> <p>if ($k eq 'inprop') {</p> <p>my %tmp;</p> <p>@tmp{split(/\|/, $q{'inprop'}.'|'.$v)} = ();</p> <p>$q{'inprop'}=join('|', keys %tmp);</p> <p>} else {</p> <p>$q{$k}=$v;</p> <p>}</p> <p>}</p> <p>} elsif ( $n eq 'revisions' ) {</p> <p>while (($k,$v)=each %$opts) {</p> <p>if ($k eq 'rvprop') {</p> <p>my %tmp;</p> <p>@tmp{split(/\|/, $q{'rvprop'}.'|'.$v)} = ();</p> <p>$q{'rvprop'}=join('|', keys %tmp);</p> <p>} else {</p> <p>$q{$k}=$v;</p> <p>}</p> <p>}</p> <p>} else {</p> <p>$q{'prop'}.="|$n";</p> <p>$q{$p."limit"}='max' if exists($self->{'queryparams'}{$n}{'limit'});</p> <p>$q{$p.$k}=$v while(($k,$v)=each %$opts);</p> <p>}</p> <p>}</p> <p>}</p> <p>$q{'titles'} = join( '|', @titles );</p> <p>}</p> <p>$r=$self->query(%q);</p> <p>return $r if($r->{'code'} ne 'success');</p> <p>if (exists($r->{'query'}{'userinfo'}{'anon'}) || exists($r->{'query'}{'userinfo'}{'temp'})) {</p> <p>$self->log("Not logged in, attempting to do so");</p> <p>$r=$self->login(1);</p> <p>return $r if($r->{'code'} ne 'success');</p> <p>next;</p> <p>}</p> <p>if (!exists($r->{'query'}{'tokens'}{"${toktype}token"})) {</p> <p>return {</p> <p>code => 'wtferror',</p> <p>error => $toktype . ' token was not returned.'</p> <p>}</p> <p>}</p> <p>my $token = $r->{'query'}{'tokens'}{"${toktype}token"};</p> <p>my $timestamp = $r->{'query'}{'curtimestamp'} // POSIX::strftime('%FT%TZ', gmtime);</p> <p>my %rights=();</p> <p>$rights{$_}=$_ foreach (@{$r->{'query'}{'userinfo'}{'rights'}});</p> <p>my @r=values(%{$r->{'query'}{'pages'}});</p> <p>unless($options{'NoShutoff'}//0) {</p> <p>my $sr = undef;</p> <p>foreach (@r){ $sr=$_ if $_->{'title'} eq $shutoff; }</p> <p>if(!defined($sr)){</p> <p>$self->warn('Shutoff token was not returned. WTF?');</p> <p>return {</p> <p>code => 'wtferror',</p> <p>error => 'Shutoff check failed. WTF?'</p> <p>};</p> <p>}</p> <p>if (!defined($self->{'noedit'}) && !exists($sr->{'missing'}) && $sr->{'revisions'}[0]{'slots'}{'main'}{'*'}=~/\S/) {</p> <p>return {</p> <p>code => 'shutoff',</p> <p>error => 'Task shutoff',</p> <p>content => $sr->{'revisions'}[0]{'slots'}{'main'}{'*'}</p> <p>};</p> <p>}</p> <p>}</p> <p>if (exists($options{'Title'})) {</p> <p>my $title = $options{'Title'};</p> <p>if(exists($r->{'query'}{'normalized'})){</p> <p>foreach (@{$r->{'query'}{'normalized'}}){</p> <p>$title=$_->{'to'} if $_->{'from'} eq $title;</p> <p>}</p> <p>}</p> <p>if(exists($r->{'query'}{'redirects'})){</p> <p>foreach (@{$r->{'query'}{'redirects'}}){</p> <p>$title=$_->{'to'} if $_->{'from'} eq $title;</p> <p>}</p> <p>}</p> <p>$r = undef;</p> <p>foreach (@r){ $r=$_ if $_->{'title'} eq $title; }</p> <p>if(!defined($r)){</p> <p>$self->warn('Data for title "'.$title.'" (normalized from "'.$options{'Title'}.'") was not returned. WTF?');</p> <p>return {</p> <p>code => 'wtferror',</p> <p>error => 'Data for title "'.$title.'" (normalized from "'.$options{'Title'}.'") was not returned.'</p> <p>};</p> <p>}</p> <p>if(exists($r->{'invalid'})){</p> <p>$self->warn("Invalid title $title was queried!");</p> <p>return {</p> <p>code => 'invalidtitle',</p> <p>error => "Bad title ``$title''",</p> <p>};</p> <p>}</p> <p># Check bot exclusion</p> <p>if(!exists($r->{'missing'}) && !(exists($options{'NoExclusion'}) && $options{'NoExclusion'})){</p> <p>my $deny='';</p> <p>my $type='';</p> <p>NOBOTS: {</p> <p>my $x;</p> <p>my $c=$r->{'revisions'}[0]{'slots'}{'main'}{'*'};</p> <p>if(!defined($c)){</p> <p>$self->warn("Page contents missing for $title, probably a MediaWiki:Missing-article error.");</p> <p>return {</p> <p>code => 'wtferror',</p> <p>error => "Page contents missing for $title.",</p> <p>};</p> <p>}</p> <p>$c=~s{<(nowiki|pre|source)(?:\s[^>]*)?(?:/>|(?<!/)>.*?(?:</\g{-1}(?:\s*)>|$))|<!--.*?(?:-->|$)}{}siog;</p> <p>if($c=~/\{\{[nN]obots\}\}/){ $deny='{{'.'nobots}}'; $type='bydefault'; last NOBOTS; }</p> <p>foreach my $allowed ($c=~/\{\{[bB]ots\s*\|\s*allow\s*=\s*(.*?)\s*\}\}/sg) {</p> <p>if($allowed eq 'all'){ next; }</p> <p>if($allowed eq 'none'){ $deny='{{'.'bots|allow=none}}'; $type='bydefault'; last NOBOTS; }</p> <p>unless(grep { $_ eq $self->{'lguser'} } split(/\s*,\s*/, $allowed)){</p> <p>$deny='{{'.'bots|allow=...}} without '.$self->{'lguser'};</p> <p>$type='bydefault';</p> <p>last NOBOTS;</p> <p>}</p> <p>}</p> <p>foreach my $denied ($c=~/\{\{[bB]ots\s*\|\s*deny\s*=\s*(.*?)\s*\}\}/sg) {</p> <p>if($denied eq 'all'){ $deny='{{'.'bots|deny=all}}'; $type='bydefault'; last NOBOTS; }</p> <p>if($denied eq 'none'){ next; }</p> <p>if(grep { $_ eq $self->{'lguser'} } split(/\s*,\s*/, $denied)){</p> <p>$deny='{{'.'bots|deny=...}} with '.$self->{'lguser'};</p> <p>$type='byname';</p> <p>last NOBOTS;</p> <p>}</p> <p>}</p> <p>if(exists($options{'OptOut'})) {</p> <p>foreach my $opted ($c=~/\{\{[bB]ots\s*\|\s*optout\s*=\s*(.*?)\s*\}\}/sg) {</p> <p>if($opted eq 'all'){ $deny='{{bots|optout=all}}'; $type='optout'; last NOBOTS; }</p> <p>if(grep { $_ eq $options{'OptOut'} } split(/\s*,\s*/, $opted)){</p> <p>$deny="{{bots|optout=...}} with ".$options{'OptOut'};</p> <p>$type='optout';</p> <p>last NOBOTS;</p> <p>}</p> <p>}</p> <p>}</p> <p>}</p> <p>if($deny ne ''){</p> <p>$r->{'code'}='botexcluded';</p> <p>$r->{'error'}="Found $deny";</p> <p>$r->{'type'}=$type;</p> <p>delete($r->{$toktype.'token'});</p> <p>return $r;</p> <p>}</p> <p>}</p> <p>} else {</p> <p>$r = {};</p> <p>}</p> <p>$r->{'token'} = $token;</p> <p>$r->{'curtimestamp'} = $timestamp;</p> <p>$r->{'rights'}=\%rights;</p> <p>$r->{'code'}='success';</p> <p>$r->{'error'}='Success';</p> <p>$r->{'self'}=$self;</p> <p>return $r;</p> <p>}</p> <p># Too many retries failed. Just error out now.</p> <p>$r->{'code'}='wtferror';</p> <p>$r->{'error'}='Login seems to succeed but we\'re still anon. WTF?';</p> <p>return $r;</p> <p>}</p> <p>=pod</p> <p>=item $api->edittoken( $title )</p> <p>=item $api->edittoken( $title, %options )</p> <p>Obtain an edit token for the specified page. This is much like C<gettoken()>,</p> <p>with the following differences:</p> <p>=over</p> <p>=item *</p> <p>C<Title> is always passed.</p> <p>=item *</p> <p>C<Redir> is set, unless C<EditRedir> is true.</p> <p>=back</p> <p>In addition to the options available for C<gettoken()>, the following are</p> <p>available:</p> <p>=over</p> <p>=item EditRedir</p> <p>C<Redir> is defaulted to true. This causes C<Redir> to be set false.</p> <p>=back</p> <p>The object returned here must be passed to C<edit()>.</p> <p>=cut</p> <p>sub edittoken {</p> <p>my ($self, $title, %options) = @_;</p> <p>$options{'Redir'}=1 unless $options{'EditRedir'} // 0;</p> <p>$options{'Title'}=$title;</p> <p>my $tok=$self->gettoken('csrf', %options);</p> <p>return $tok unless $tok->{'code'} eq 'success';</p> <p># Check page protection</p> <p>my %rights = %{$tok->{'rights'}};</p> <p>my $permfail='';</p> <p>if(!exists($tok->{'missing'})){</p> <p>$permfail='edit' unless exists($rights{'edit'});</p> <p>} elsif($tok->{'ns'}&1){</p> <p>$permfail='createtalk' unless exists($rights{'createtalk'});</p> <p>} else {</p> <p>$permfail='createpage' unless exists($rights{'createpage'});</p> <p>}</p> <p>if(exists($tok->{'protection'})){</p> <p>foreach (@{$tok->{'protection'}}){</p> <p>if(($_->{'type'} eq 'create' && exists($tok->{'missing'})) ||</p> <p>($_->{'type'} eq 'edit' && (!exists($tok->{'missing'}) || exists($_->{'source'})))){</p> <p>$_->{'level'}='editprotected' if($_->{'level'} eq 'sysop');</p> <p>$_->{'level'}='editsemiprotected' if($_->{'level'} eq 'autoconfirmed');</p> <p>$permfail=$_->{'level'} unless(exists($rights{$_->{'level'}}));</p> <p>$permfail='protect' if(!exists($rights{'protect'}) && (exists($_->{'cascade'}) || exists($_->{'source'})));</p> <p>}</p> <p>}</p> <p>}</p> <p>if($permfail ne ''){</p> <p>$tok->{'code'}='pageprotected';</p> <p>$tok->{'error'}="Editing this page requires the $permfail permission";</p> <p>delete($tok->{'token'});</p> <p>return $tok;</p> <p>}</p> <p>return $tok;</p> <p>}</p> <p>=pod</p> <p>=item $api->edit( $token, $text, $summary, $minor, $bot, %params )</p> <p>Perform an edit to the page.</p> <p>Note that the default configuration uses the C<assert> parameter to assert that</p> <p>the current user has the "bot" flag. This means that the edit will fail if your</p> <p>bot is not flagged; the AnomieBOT A<trial.pl> script overrides this default, so</p> <p>bot trials may still be done.</p> <p>=cut</p> <p>sub edit {</p> <p>my ($self, $token, $text, $summary, $minor, $bot, %params) = @_;</p> <p>if(ref($token) ne 'HASH' || $token->{'self'} ne $self){</p> <p>$self->warn("Invalid token");</p> <p>return {</p> <p>code => 'params',</p> <p>error => 'Invalid $token'</p> <p>};</p> <p>}</p> <p>my $text_to_md5 = $text;</p> <p>utf8::encode( $text_to_md5 );</p> <p>my %param=(</p> <p>action => 'edit',</p> <p>title => $token->{'title'},</p> <p>text => $text,</p> <p>token => $token->{'token'},</p> <p>summary => $summary,</p> <p>md5 => md5_hex($text_to_md5),</p> <p>starttimestamp => _ISO2wptime($token->{'curtimestamp'}),</p> <p>watchlist => $self->{'edit_watchlist'},</p> <p>);</p> <p>$param{$minor?'minor':'notminor'}=1 if defined($minor);</p> <p>$param{'bot'}=1 if(defined($bot) && $bot);</p> <p>if(exists($token->{'missing'})){</p> <p>$param{'basetimestamp'}=_ISO2wptime($token->{'curtimestamp'});</p> <p>$param{'createonly'}=1;</p> <p>} else {</p> <p>$param{'basetimestamp'}=_ISO2wptime($token->{'revisions'}[0]{'timestamp'});</p> <p>$param{'nocreate'}=1;</p> <p>}</p> <p>$param{'assert'}=$self->{'assert_edit'} if($self->{'assert_edit'} ne '');</p> <p>$param{'nassert'}=$self->{'nassert_edit'} if($self->{'nassert_edit'} ne '');</p> <p>%param = ( %param, %params ) if %params;</p> <p>if(defined($self->{'editlimit'}) && $self->{'editlimit'}<=0){</p> <p>die "Edit limit reached, bot halting.";</p> <p>}</p> <p>if(defined($self->{'noedit'})){</p> <p># Fake edit</p> <p>my $t=$param{'title'}.'<'.(exists($token->{'missing'})?'new':$token->{'lastrevid'}).POSIX::strftime('>%FT%TZ.txt', gmtime);</p> <p>$t=~s! !_!g;</p> <p>$t=~s!/!#!g;</p> <p>$t=$self->{'noedit'}.'/'.$t;</p> <p>open(X, ">:utf8", $t) or die("Could not open $t: $!\n");</p> <p>print X $text;</p> <p>close(X);</p> <p>print "\e[34mEDIT to ".$param{'title'}." ($summary): $t\e[0m\n";</p> <p>return {</p> <p>code => 'success',</p> <p>error => 'Success',</p> <p>edit => {</p> <p>oldrevid => $token->{'lastrevid'},</p> <p>newrevid => $token->{'lastrevid'},</p> <p>pageid => $token->{'pageid'},</p> <p>title => $token->{'title'},</p> <p>result => 'Success'</p> <p>}</p> <p>};</p> <p>}</p> <p>$self->_throttle('edit');</p> <p>my $res=$self->_query(%param);</p> <p>if($res->{'code'} eq 'success'){</p> <p># The edit API might return failure in a different way</p> <p>if(lc($res->{'edit'}{'result'}) eq 'success'){</p> <p>$self->{'editlimit'}-- if defined($self->{'editlimit'});</p> <p>return $res;</p> <p>}</p> <p>$res->{'code'}=$res->{'edit'}{'result'};</p> <p>$res->{'error'}='Edit hook error';</p> <p># Well-behaved extensions will include an explanation token in the</p> <p># result. Pull out some common ones.</p> <p>if(exists($res->{'edit'}{'spamblacklist'})){</p> <p>$res->{'error'}.=': Spam blacklist triggered on "'.$res->{'edit'}{'spamblacklist'}.'"';</p> <p>} elsif(exists($res->{'edit'}{'assert'})){</p> <p>$res->{'error'}.=': Assertion "'.$res->{'edit'}{'assert'}.'" failed';</p> <p>} elsif(exists($res->{'edit'}{'nassert'})){</p> <p>$res->{'error'}.=': Negative assertion "'.$res->{'edit'}{'nassert'}.'" failed (i.e. the condition passed)';</p> <p>} elsif(exists($res->{'edit'}{'captcha'})){</p> <p>$res->{'error'}.=': Captcha required';</p> <p>} elsif(exists($res->{'edit'}{'info'}) && ref($res->{'edit'}{'info'}) eq ""){</p> <p># Probably AbuseFilter</p> <p>$res->{'error'}.=': ' . $res->{'edit'}{'info'};</p> <p>$res->{'error'}.=' (' . $res->{'edit'}{'code'} . ')' if exists($res->{'edit'}{'code'});</p> <p>} else {</p> <p># Something unknown. Just tack on the whole response object.</p> <p>my $x=Dumper($res->{'edit'});</p> <p>$x=~s/\n\s*/ /g;</p> <p>$res->{'error'}.=": $x";</p> <p>}</p> <p>}</p> <p>carp "Edit error: ".$res->{'error'};</p> <p>return $res;</p> <p>}</p> <p>=pod</p> <p>=item $api->upload( $token, %options )</p> <p>Upload a file.</p> <p>The C<$token> must be obtained from C<< $api->gettoken >> with the target file</p> <p>name passed as C<Title>. To specify the file contents, exactly one of the</p> <p>following options must be given:</p> <p>=over</p> <p>=item Url</p> <p>Url that the MediaWiki server should download the file from.</p> <p>=item HttpStatus</p> <p>Do not actually upload anything, just return the status of the upload</p> <p>corresponding to the session key specified here.</p> <p>=item FileKey</p> <p>If the initial upload returned warnings (or C<Stash> was used), specify the</p> <p>returned file key here to complete the upload. C<SessionKey> is accepted as an</p> <p>alias for backwards compatability.</p> <p>=item File</p> <p>Filename on the local system to upload, accessible to the bot. Note the file</p> <p>will be read in binary (L<:raw|PerlIO(3perl)/:raw>) mode.</p> <p>=item Handle</p> <p>Open file handle from which the data to upload will be read.</p> <p>=item Data</p> <p>Raw file data to upload.</p> <p>=back</p> <p>Additional options are:</p> <p>=over</p> <p>=item Comment</p> <p>Comment for the upload; note that MediaWiki will also use this for the initial</p> <p>file page text if the file does not already exist and C<Text> is not used.</p> <p>=item Text</p> <p>Initial file page text if the file does not already exist, rather than using</p> <p>C<Comment>.</p> <p>=item AsyncDownload</p> <p>When using Url, setting a true value here tells MediaWiki to return us a</p> <p>session key immediately (which may be passed to HttpStatus in a later call)</p> <p>rather than waiting for the download to actually complete.</p> <p>=item IgnoreWarnings</p> <p>Ignore any warnings.</p> <p>=item Stash</p> <p>Stash file temporarily.</p> <p>=back</p> <p>=cut</p> <p>sub upload {</p> <p>my ($self, $token, %options) = @_;</p> <p>if(ref($token) ne 'HASH' || $token->{'self'} ne $self){</p> <p>$self->warn("Invalid token");</p> <p>return {</p> <p>code => 'params',</p> <p>error => 'Invalid $token'</p> <p>};</p> <p>}</p> <p>if(($token->{'ns'}//-1) ne 6){</p> <p>$self->warn("Token is not for a title in the File namespace.");</p> <p>return {</p> <p>code => 'params',</p> <p>error => 'Token is not for the File namespace.'</p> <p>};</p> <p>}</p> <p>if ( exists($options{'SessionKey'}) ) {</p> <p>$options{'FileKey'} = $options{'SessionKey'} unless exists( $options{'FileKey'} );</p> <p>delete $options{'SessionKey'};</p> <p>}</p> <p>my @req=qw/Url HttpStatus FileKey File Handle Data/;</p> <p>my $ct=0;</p> <p>foreach (@req){</p> <p>$ct++ if exists($options{$_});</p> <p>}</p> <p>if($ct!=1){</p> <p>$req[$#req]='or '.$req[$#req];</p> <p>return {</p> <p>code => 'params',</p> <p>error => 'Exactly one of '.join(', ', @req).' must be specified.'</p> <p>};</p> <p>}</p> <p>my %param=(</p> <p>action => 'upload',</p> <p>token => $token->{'token'},</p> <p>watchlist => $self->{'edit_watchlist'},</p> <p>);</p> <p>($param{'filename'}=$token->{'title'})=~s/^[^:]*://;</p> <p>$param{'comment'}=$options{'Comment'} if exists($options{'Comment'});</p> <p>$param{'text'}=$options{'Text'} if exists($options{'Text'});</p> <p>$param{'asyncdownload'}=1 if($options{'AsyncDownload'} // 0);</p> <p>$param{'ignorewarnings'}=1 if($options{'IgnoreWarnings'} // 0);</p> <p>$param{'stash'}=1 if($options{'Stash'} // 0);</p> <p>if(exists($options{'Url'})){</p> <p>$param{'url'}=$options{'Url'};</p> <p>} elsif(exists($options{'HttpStatus'})){</p> <p>$param{'httpstatus'}=1;</p> <p>$param{'filekey'}=$options{'HttpStatus'};</p> <p>} elsif(exists($options{'FileKey'})){</p> <p>$param{'filekey'}=$options{'FileKey'};</p> <p>} elsif(exists($options{'File'})){</p> <p>$param{'file'}=[ $options{'File'}, $param{'filename'} ];</p> <p>} elsif(exists($options{'Handle'})){</p> <p>{</p> <p>local $/=undef;</p> <p>my $fh=$options{'Handle'};</p> <p>$param{'file'}=[ undef, $param{'filename'}, Content => scalar <$fh> ];</p> <p>}</p> <p>} elsif(exists($options{'Data'})){</p> <p>$param{'file'}=[ undef, $param{'filename'}, Content => $options{'Data'} ];</p> <p>}</p> <p>if(defined($self->{'editlimit'}) && $self->{'editlimit'}<=0){</p> <p>die "Edit limit reached, bot halting.";</p> <p>}</p> <p>if(defined($self->{'noedit'})){</p> <p># Fake edit</p> <p>if(exists($param{'file'})){</p> <p>my $t='Upload##'.$param{'filename'}.'<'.(exists($token->{'missing'})?'new':$token->{'lastrevid'}).POSIX::strftime('>%FT%TZ.txt', gmtime);</p> <p>$t=~s! !_!g;</p> <p>$t=~s!/!#!g;</p> <p>$t=$self->{'noedit'}.'/'.$t;</p> <p>open(X, ">:raw", $t) or die("Could not open $t: $!\n");</p> <p>if(defined($param{'file'}[0])){{</p> <p>local $/=undef;</p> <p>open(XX, "<:raw", $param{'file'}[0]);</p> <p>print X scalar <XX>;</p> <p>close XX;</p> <p>}} else {</p> <p>print X $param{'file'}[3];</p> <p>}</p> <p>close(X);</p> <p>} elsif(exists($param{'url'})){</p> <p>print "\e[34mUPLOAD from ".$param{'url'}."\e[0m\n";</p> <p>}</p> <p>if(exists($token->{'missing'})){</p> <p>my $t='File:'.$param{'filename'}.'<new'.POSIX::strftime('>%FT%TZ.txt', gmtime);</p> <p>$t=~s! !_!g;</p> <p>$t=~s!/!#!g;</p> <p>$t=$self->{'noedit'}.'/'.$t;</p> <p>open(X, ">:utf8", $t) or die("Could not open $t: $!\n");</p> <p>print X $param{'comment'}//'';</p> <p>close(X);</p> <p>}</p> <p>print "\e[34mUPLOAD to ".$param{'filename'}."\e[0m\n";</p> <p>return {</p> <p>code => 'success',</p> <p>error => 'Success',</p> <p>upload => {</p> <p>result => 'Success'</p> <p># XXX: Fake up the rest somehow?</p> <p>}</p> <p>};</p> <p>}</p> <p>$self->_throttle('edit');</p> <p>my $res=$self->_query(%param);</p> <p>if($res->{'code'} eq 'success'){</p> <p># The upload API might return failure in a different way</p> <p>if(lc($res->{'upload'}{'result'} // 'Success') eq 'success'){</p> <p>$self->{'editlimit'}-- if defined($self->{'editlimit'});</p> <p>return $res;</p> <p>}</p> <p>if(lc($res->{'upload'}{'result'}) eq 'warning'){</p> <p>$res->{'code'}='uploadwarnings';</p> <p>$res->{'error'}="Upload warnings: ".join(', ', keys %{$res->{'upload'}{'warnings'}});</p> <p>return $res;</p> <p>}</p> <p>$res->{'code'}=$res->{'upload'}{'error'};</p> <p>$res->{'error'}="Upload error: ".$res->{'code'};</p> <p>}</p> <p>carp "Upload error: ".$res->{'error'};</p> <p>return $res;</p> <p>}</p> <p>=pod</p> <p>=item $api->movetoken( $title )</p> <p>=item $api->movetoken( $title, %options )</p> <p>Obtain a move token for the specified page. Options are:</p> <p>=over</p> <p>=item EditRedir</p> <p>Move the redirect page, instead of the page it points to.</p> <p>=item NoShutoff</p> <p>Do not check the shutoff page. Don't do this frivolously.</p> <p>=item NoExclusion</p> <p>Do not check for {{tl|bots}} / {{tl|nobots}}. Don't do this frivolously.</p> <p>=back</p> <p>The object returned here must be passed to C<move()>. The object contains the</p> <p>same properties as that returned by C<edittoken()>, plus the following:</p> <p>=over</p> <p>=item can_suppressredirect</p> <p>True if the C<$noredirect> parameter to C<move()> will be honored (i.e. the</p> <p>user has the 'suppressredirect' right).</p> <p>=back</p> <p>=cut</p> <p>sub movetoken {</p> <p>my ($self, $title, %options) = @_;</p> <p>my %rights=();</p> <p>delete $options{'OptOut'};</p> <p>$options{'Title'}=$title;</p> <p>my $tok=$self->gettoken('csrf', %options);</p> <p>return $tok unless $tok->{'code'} eq 'success';</p> <p>if(exists($tok->{'missing'})){</p> <p>$tok->{'code'}='pagemissing';</p> <p>$tok->{'error'}="Cannot move a nonexistent page";</p> <p>delete($tok->{'token'});</p> <p>return $tok;</p> <p>}</p> <p># Check page protection</p> <p>my $permfail='';</p> <p>my $rights = $tok->{'rights'};</p> <p>if(!exists($rights{'move'})){</p> <p>$permfail='move';</p> <p>} elsif($tok->{'ns'}==2 && index($tok->{'title'},'/')<0){</p> <p>$permfail='move-rootuserpages' unless exists($rights{'move-rootuserpages'});</p> <p>} elsif($tok->{'ns'}==6){</p> <p>$permfail='movefile' unless exists($rights{'movefile'});</p> <p>}</p> <p>if(exists($tok->{'protection'})){</p> <p>foreach (@{$tok->{'protection'}}){</p> <p>if($_->{'type'} eq 'move'){</p> <p>$_->{'level'}='protect' if($_->{'level'} eq 'sysop');</p> <p>$permfail=$_->{'level'} unless exists($rights{$_->{'level'}});</p> <p>}</p> <p>}</p> <p>}</p> <p>if($permfail ne ''){</p> <p>$tok->{'code'}='pageprotected';</p> <p>$tok->{'error'}="Moving this page requires the $permfail permission";</p> <p>delete($tok->{'token'});</p> <p>return $tok;</p> <p>}</p> <p>$tok->{'can_suppressredirect'}=exists($rights{'suppressredirect'});</p> <p>return $tok;</p> <p>}</p> <p>=pod</p> <p>=item $api->move( $token, $totitle, $reason, $movetalk, $noredirect )</p> <p>Move the page to C<$totitle>.</p> <p>=cut</p> <p>sub move {</p> <p>my ($self, $token, $totitle, $reason, $movetalk, $noredirect) = @_;</p> <p>if(ref($token) ne 'HASH' || $token->{'self'} ne $self){</p> <p>$self->warn("Invalid token");</p> <p>return {</p> <p>code => 'params',</p> <p>error => 'Invalid $token'</p> <p>};</p> <p>}</p> <p>my %param=(</p> <p>action => 'move',</p> <p>from => $token->{'title'},</p> <p>to => $totitle,</p> <p>token => $token->{'token'},</p> <p>reason => $reason,</p> <p>watchlist => $self->{'edit_watchlist'},</p> <p>);</p> <p>$param{'movetalk'}=1 if(defined($movetalk) && $movetalk);</p> <p>$param{'noredirect'}=1 if(defined($noredirect) && $noredirect);</p> <p>if(defined($self->{'editlimit'}) && $self->{'editlimit'}<=0){</p> <p>die "Edit limit reached, bot halting.";</p> <p>}</p> <p>if(defined($self->{'noedit'})){</p> <p># Fake edit</p> <p>print "\e[34mMOVE from ".$param{'from'}." to ".$param{'to'}." ($reason)\e[0m\n";</p> <p>my $ret={</p> <p>from => $token->{'title'},</p> <p>to => $totitle,</p> <p>reason => $reason,</p> <p>};</p> <p>$ret->{'redirectcreated'}='' unless($noredirect && exists($token->{'can_suppressredirect'}));</p> <p>return {</p> <p>code => 'success',</p> <p>error => 'Success',</p> <p>move => $ret</p> <p>};</p> <p>}</p> <p>$self->_throttle('edit');</p> <p>my $res=$self->_query(%param);</p> <p>if($res->{'code'} eq 'success'){</p> <p>$self->{'editlimit'}-- if defined($self->{'editlimit'});</p> <p>return $res;</p> <p>}</p> <p>carp "Move error: ".$res->{'error'};</p> <p>return $res;</p> <p>}</p> <p>=pod</p> <p>=item $api->action( $token, %param )</p> <p>Perform an action that doesn't have a custom function. C<%param> must contain</p> <p>an "action" key, as well as any other keys needed for the action ("token" may</p> <p>be omitted).</p> <p>=cut</p> <p>sub action {</p> <p>my ($self, $token, %param) = @_;</p> <p>if(ref($token) ne 'HASH' || $token->{'self'} ne $self){</p> <p>$self->warn("Invalid token");</p> <p>return {</p> <p>code => 'params',</p> <p>error => 'Invalid $token'</p> <p>};</p> <p>}</p> <p>unless(exists($param{'action'})){</p> <p>$self->warn("Missing action");</p> <p>return {</p> <p>code => 'params',</p> <p>error => 'Missing action'</p> <p>};</p> <p>}</p> <p>$param{'token'} //= $token->{'token'};</p> <p>if(defined($self->{'editlimit'}) && $self->{'editlimit'}<=0){</p> <p>die "Edit limit reached, bot halting.";</p> <p>}</p> <p>my $act = uc( $param{'action'} );</p> <p>if(defined($self->{'noedit'})){</p> <p># Fake edit</p> <p>my $title = $token->{'title'}//'<no title>';</p> <p>print "\e[34m$act on $title\e[0m\n" . Dumper( \%param ) . "\n";</p> <p>return {</p> <p>code => 'success',</p> <p>error => 'Success',</p> <p>};</p> <p>}</p> <p>$self->_throttle('edit');</p> <p>my $res=$self->_query(%param);</p> <p>if($res->{'code'} eq 'success'){</p> <p>$self->{'editlimit'}-- if defined($self->{'editlimit'});</p> <p>return $res;</p> <p>}</p> <p>carp "$act error: ".$res->{'error'};</p> <p>return $res;</p> <p>}</p> <p>sub DESTROY {</p> <p>my $self=shift;</p> <p>untie %{$self->{'storehash'}};</p> <p>delete $self->{'storehash'};</p> <p>delete $self->{'store'};</p> <p>}</p> <ol> <li>Utility funcs</li> </ol> <p>sub _handle_maxlag {</p> <p>my $self=shift;</p> <p>my $res=shift;</p> <p>my $reason=shift;</p> <p>my $delay=10;</p> <p>my $header=$res->header('Retry-After') // '';</p> <p>$header=~s/^\s+|\s+$//g;</p> <p>my $t;</p> <p>if($header=~/^\d+$/){</p> <p>$delay=$header;</p> <p>} elsif(defined($t=str2time($header))){</p> <p>$delay=POSIX::ceil($t-time());</p> <p>$delay=1 if $delay<1;</p> <p>}</p> <p>if(!$self->automaxlag){</p> <p>return {</p> <p>code => 'maxlag',</p> <p>error => $reason,</p> <p>delay => $delay,</p> <p>};</p> <p>}</p> <p>carp POSIX::strftime('[%F %T] ', localtime).$self->{'task'}.' ('.$self->{'botnum'}.'): Pausing for maxlag ('.$delay.'s): '.$reason;</p> <p>while($delay>10){</p> <p>sleep 10;</p> <p>$delay-=10;</p> <p>return {</p> <p>code => 'maxlag',</p> <p>error => $reason,</p> <p>delay => $delay,</p> <p>} if !$self->automaxlag;</p> <p>}</p> <p>sleep $delay;</p> <p>return undef;</p> <p>}</p> <p>use vars '$AUTOLOAD';</p> <p>sub AUTOLOAD {</p> <p>my $func=$AUTOLOAD;</p> <p>$func=~s/.*://;</p> <p>if($func!~/^_/ && @_ && ref($_[0]) && $_[0]->isa('AnomieBOT::API')){</p> <p>my $self=$_[0];</p> <p>foreach my $d (@{$self->{'decorators'}}){</p> <p>my $sub=$d->can($func);</p> <p>goto &$sub if defined($sub);</p> <p>}</p> <p>}</p> <p>croak "Undefined subroutine &$AUTOLOAD called";</p> <p>}</p> <p>=pod</p> <p>=back</p> <p>=head1 UTILITY METHODS</p> <p>=over</p> <p>=item AnomieBOT::API::load( $file )</p> <p>Load the task contained in the specified file, if it hasn't already been</p> <p>loaded.</p> <p>=cut</p> <p>my %loaded=();</p> <p>sub load {</p> <p>my $m=$_[0];</p> <p>if($m=~/::/){</p> <p>$m=~s/::/\//g;</p> <p>$m.='.pm';</p> <p>}</p> <p>my $file=realpath($m);</p> <p>croak "File not found: $m" unless defined($file);</p> <p>eval {</p> <p>require $file unless exists($loaded{$file});</p> <p>};</p> <p>croak "Could not load $file: $@" if $@;</p> <p>$loaded{$file}=1;</p> <p>}</p> <p>=pod</p> <p>=item $AnomieBOT::API::basedir</p> <p>Returns the base directory for the bot.</p> <p>Specifically, this returns the directory that contains A<AnomieBOT/API.pm>.</p> <p>Note this may croak if you loaded A<AnomieBOT::API> in some other way than</p> <p>C<require AnomieBOT::API;> or C<use AnomieBOT::API;>.</p> <p>=cut</p> <p>$AnomieBOT::API::basedir=undef;</p> <p>croak 'AnomieBOT::API not found in %INC' unless exists($INC{'AnomieBOT/API.pm'});</p> <p>my $basedir=realpath($INC{'AnomieBOT/API.pm'});</p> <p>croak $INC{'AnomieBOT/API.pm'}.' could not be found; did you chdir after loading it?' unless -e $basedir;</p> <p>$AnomieBOT::API::basedir=substr($basedir, 0, -17);</p> <p>1;</p> <p>=pod</p> <p>=back</p> <p>=head1 COPYRIGHT</p> <p>Copyright 2008–2013 Anomie</p> <p>This library is free software; you can redistribute it and/or</p> <p>modify it under the same terms as Perl itself.</p> <p></syntaxhighlight></p></div></section></div></main> <footer class="site-footer"> <div class="footer-container"> <div class="footer-links"> <a href="/about.php">About</a> <a href="/help.php">Help</a> <a href="/updates.php">Updates</a> <a href="/contact.php">Contact</a> <a href="/privacy.php">Privacy</a> <a href="/terms.php">Terms</a> <a href="https://github.com/yourusername/friendly-wiki" target="_blank" rel="noopener">GitHub</a> </div> <div class="footer-copy"> © 2025 Friendly Wiki. All rights reserved. </div> </div> </footer> <script> const toggle = document.getElementById('mobileMenuToggle'); const menu = document.getElementById('mobileMenu'); toggle.addEventListener('click', () => { menu.classList.toggle('active'); }); </script> <!-- Collapsible toggle --> <script> document.addEventListener("DOMContentLoaded", function () { const toggles = document.querySelectorAll('.section-toggle'); toggles.forEach(toggle => { toggle.addEventListener('click', function () { const section = toggle.closest('.collapsible'); const body = section.querySelector('.wiki-body'); body.classList.toggle('collapsed'); }); }); }); </script>