File Coverage

blib/lib/MediaWiki/Bot/Shell.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1 1     1   22381 use strict;
  1         3  
  1         39  
2 1     1   5 use warnings;
  1         1  
  1         55  
3             #use diagnostics;
4             package MediaWiki::Bot::Shell;
5             BEGIN {
6 1     1   27 $MediaWiki::Bot::Shell::VERSION = '0.002';
7             }
8              
9 1     1   10 use base qw(Term::Shell);
  1         1  
  1         973  
10 1     1   31024 use MediaWiki::Bot '3.1.6';
  0            
  0            
11             use Config::General qw(ParseConfig SaveConfig);
12             use Term::Prompt;
13             use Getopt::Long qw(GetOptionsFromArray);
14             use Pod::Select;
15             use Pod::Text::Termcap;
16             use IO::Handle;
17             use Encode;
18             use File::Copy qw(cp);
19              
20             binmode(STDOUT, ':encoding(utf8)');
21             binmode(STDERR, ':encoding(utf8)');
22              
23             =head1 NAME
24              
25             MediaWiki::Bot::Shell - a shell interface to your MediaWiki::Bot
26              
27             =head1 SYNOPSIS
28              
29             use MediaWiki::Bot::Shell;
30              
31             my $shell = MediaWiki::Bot::Shell->new();
32             $shell->cmdloop();
33              
34             =head1 DESCRIPTION
35              
36             This provides a shell interface to your L. By initializing one
37             or more MediaWiki::Bot objects and using them for the duration of your shell
38             session, initialization costs are amortized.
39              
40             Configuration data is read from F<~/.perlwikibot-shell.rc>, or prompted for at
41             startup.
42              
43             You should probably run C, which is included with this module, to enter
44             your shell.
45              
46             =head1 OPTIONS
47              
48             Options are passed to the constructor as a hashref. Options are currently:
49              
50             =over 4
51              
52             =item debug
53              
54             Like it says on the tin. Setting debug provides debug output from this module,
55             as well as from the underlying L object(s).
56              
57             =item norc
58              
59             Don't read options from F<~/.perlwikibot-shell.rc>. This will result in the
60             user being prompted for username, password etc.
61              
62             =back
63              
64             =head1 COMMANDS
65              
66             =cut
67              
68             # Take some pod and Termcap it!
69             sub _render_help {
70             my $pod = shift;
71             my $pod_parser = Pod::Text::Termcap->new(
72             width => 72,
73             utf8 => 1,
74             );
75             my $rendered_pod;
76             $pod_parser->output_string(\$rendered_pod);
77             $pod_parser->parse_string_document($pod);
78              
79             return $rendered_pod;
80             }
81              
82             # Sets the shell prompt - How can I get rid of the underline?
83             sub prompt_str {
84             my $o = shift;
85             my $u = $o->{'SHELL'}->{'bot'};
86              
87             return (
88             $u
89             ? ( $u->{'host'} eq 'secure.wikimedia.org'
90             ? $u->{'username'} . '>'
91             : $u->{'username'} . '@' . $u->domain_to_db($u->{'host'}) . '>')
92             : 'perlwikibot>'
93             );
94             }
95              
96             # At shell startup - right before cmdloop() begins
97             sub preloop {
98             my $o = shift;
99             my $options = $o->{'API'}->{'args'}->[0];
100              
101             $o->{'SHELL'}->{'debug'} = $options->{'verbose'} || 0;
102             my $debug = $o->{'SHELL'}->{'debug'};
103             print "Debugging on\n" if $debug;
104              
105             print "Setting STDOUT and STDERR to autoflush.\n" if $debug;
106             STDOUT->autoflush(1);
107             STDERR->autoflush(1);
108              
109             my $filename = "$ENV{'HOME'}/.perlwikibot-shell.rc";
110             if (-r $filename and !$options->{'norc'}) {
111             my $chmod = (stat(_))[2] & 07777;
112             if ($chmod != 0600) {
113             my $do_chmod = prompt('y', "$filename isn't private - should I try to make it so?", '', 'y');
114             if ($do_chmod) {
115             chmod 0600, $filename or die "Couldn't chmod $filename: $!";
116             }
117             }
118              
119             my %main = ParseConfig (
120             -ConfigFile => $filename,
121             -AutoTrue => 1,
122             -UTF8 => 1,
123             );
124              
125             print "Logging into $main{'username'}...";
126             my $bot = MediaWiki::Bot->new({
127             login_data => { username => $main{'username'},
128             password => $main{'password'},
129             do_sul => $main{'do_sul'},
130             },
131             protocol => $main{'protocol'},
132             host => $main{'host'},
133             path => $main{'path'},
134             debug => $debug,
135             }) or die q{Couldn't log in};
136             $bot->steward_new() or die q{Couldn't log in};
137             print ($bot ? " OK\n" : " FAILED\n");
138             $o->{'SHELL'}->{'bot'} = $bot;
139             }
140             else {
141             my %main;
142             $main{'username'} = prompt('x', 'Account name:', '', '');
143             $main{'password'} = prompt('p', 'Password:', '', '');
144             print "\n";
145             $main{'host'} = prompt('x', 'Domain name:', '', 'meta.wikimedia.org');
146             $main{'path'} = 'w';
147             $main{'protocol'} = 'http';
148             if ($main{'host'} eq 'secure.wikimedia.org') {
149             $main{'path'} = prompt('x', 'Path:', '', 'wikipedia/meta/w');
150             $main{'protocol'} = 'https';
151             $main{'do_sul'} = 0;
152             }
153             else {
154             $main{'do_sul'} = prompt('y', 'Use SUL?', '', 'y');
155             }
156              
157             my $save = prompt('y', 'Save settings for next time?', '', 'y');
158             if ($save) {
159             if (-e $filename) {
160             my $clobber = prompt('y', "$filename already exists - overwrite?", '', 'n');
161             cp($filename, "$filename.bak") or die "Couldn't make a backup: $!";
162             }
163             SaveConfig($filename, \%main);
164             chmod 0600, $filename or die "Couldn't chmod 600 $filename: $!";
165             }
166             else {
167             print "You didn't want to save the settings, so I threw them away\n";
168             }
169              
170             print "Logging in...";
171             my $bot = MediaWiki::Bot->new({
172             login_data => { username => $main{'username'},
173             password => $main{'password'},
174             do_sul => $main{'do_sul'},
175             },
176             protocol => $main{'protocol'},
177             host => $main{'host'},
178             path => $main{'path'},
179             debug => $main{'debug'},
180             }) or die q{Couldn't log in};
181             $bot->steward_new() or die q{Couldn't log in};
182             print ($bot ? " OK\n" : " FAILED\n");
183             $o->{'SHELL'}->{'bot'} = $bot;
184             }
185              
186             die "Login failed; can't do anything fun without a MediaWiki::Bot\n" unless $o->{'SHELL'}->{'bot'};
187             }
188              
189             # At shell shutdown - right after cmdloop() ends
190             sub postloop {
191             my $o = shift;
192             my $u = $o->{'SHELL'}->{'bot'};
193              
194             $u = $u->logout() if ($u and $u->can('logout'));
195             print "Logged out\n";
196             }
197              
198             sub run_delete {
199             my $o = shift;
200             my $page = shift;
201             my $summary = shift || 'Vandalism ([[m:S|S]])';
202             if (@_ > 0) {
203             my $abort = prompt('y', 'Abort?', qq{Check your quoting - did you mean to delete [[$page]] with reason "$summary"?}, 'y');
204             return 1 if $abort;
205             }
206             my $u = $o->{'SHELL'}->{'bot'};
207              
208             my $success = $u->delete($page, $summary);
209              
210             if ($success) {
211             print "Deletion successful\n";
212             }
213             else {
214             print "Deletion failed:\n"
215             . " $u->{'error'}->{'details'}\n";
216             }
217             }
218             sub smry_delete {
219             return 'delete a page';
220             }
221             sub help_delete {
222             my $help = <<'=cut';
223             =head2 delete
224              
225             This will delete a page on the wiki you're currently using:
226              
227             delete "Main Page" "for teh lulz"
228              
229             To delete a page on another wiki, use [[w:fr:Page Title]]:
230              
231             delete "[[w:fr:Page Title]]" "pour les lulz"
232              
233             Make sure you quote your input correctly.
234              
235             =cut
236              
237             return _render_help($help);
238             }
239              
240             sub run_set_wiki {
241             my $o = shift;
242             my $domain = shift;
243             my $path = shift;
244              
245             my $u = $o->{'SHELL'}->{'bot'};
246              
247             if (!$domain) {
248             $domain = prompt('x', 'Switch to what domain?', '', '');
249             $path = prompt('x', "What path on $domain?", '', 'w');
250             }
251              
252             my $success;
253             if ($domain eq 'secure.wikimedia.org') {
254             $path = prompt('x', 'What path on secure.wikimedia.org?', '', '') unless defined($path);
255             $success = $u->set_wiki({
256             protocol => 'https',
257             host => $domain,
258             path => $path,
259             });
260             }
261             else {
262             $success = $u->set_wiki({
263             host => $domain,
264             path => $path
265             });
266             }
267              
268             if ($success) {
269             print "Switched successfully\n";
270             }
271             else {
272             print "Couldn't switch wiki\n";
273             }
274             }
275             sub smry_set_wiki {
276             return 'switch to another wiki';
277             }
278             sub help_set_wiki {
279             my $help = <<'=cut';
280             =head2 set_wiki
281              
282             Switch wikis:
283              
284             set_wiki meta.wikimedia.org
285             set_wiki secure.wikimedia.org wikipedia/meta/w
286              
287             =cut
288              
289             return _render_help($help);
290             }
291              
292             sub run_debug {
293             my $o = shift;
294             my $debug = shift;
295              
296             my $on = "Debug output on\n";
297             my $off = "Debug output off\n";
298             if (!defined($debug)) {
299             $o->{'SHELL'}->{'debug'} = 1;
300             print $on;
301             }
302             elsif (defined($debug) and $debug =~ m/^(y|yes|1|true|on)$/i) {
303             $o->{'SHELL'}->{'debug'} = 1;
304             print $on;
305             }
306             elsif (defined($debug) and $debug =~ m/^(n|no|0|false|off)$/i) {
307             $o->{'SHELL'}->{'debug'} = 0;
308             print $off;
309             }
310             else {
311             $debug = prompt('y', 'Provide debug output?', '', 'n');
312             $o->{'SHELL'}->{'debug'} = $debug;
313             print ($debug ? $on : $off);
314             }
315             $o->{'SHELL'}->{'bot'} = $o->{'SHELL'}->{'debug'};
316             }
317             sub smry_debug {
318             return 'switch debugging on or off';
319             }
320             sub help_debug {
321             my $help = <<'=cut';
322             =head2 debug
323              
324             Turn debugging on or off
325              
326             debug on
327             debug off
328              
329             =cut
330              
331             return _render_help($help);
332             }
333              
334             sub run_read {
335             my $o = shift;
336             my $page = shift;
337             if (@_ > 0) {
338             $page = "$page " . join(' ', @_);
339             my $continue = prompt('y', "Did you mean [[$page]]?", '', 'y');
340             return unless $continue;
341             }
342              
343             my $u = $o->{'SHELL'}->{'bot'};
344              
345             my $text = $u->get_text($page);
346             if (defined($text)) {
347             $o->page($text);
348             print "\n";
349             }
350             else {
351             print "[[$page]] doesn't exist\n";
352             }
353             }
354             sub smry_read {
355             return 'read a wiki page';
356             }
357             sub help_read {
358             my $help = <<'=cut';
359             =head2 read
360              
361             Read the wikitext of the given page. Remember to quote the page title correctly:
362              
363             read "Main Page"
364              
365             =cut
366              
367             return _render_help($help);
368             }
369              
370             sub run_kill {
371             my $o = shift;
372             my $username = decode('utf8', shift);
373              
374             my $u = $o->{'SHELL'}->{'bot'};
375              
376             my $success = $u->ca_lock({
377             user => $username,
378             lock => 1,
379             hide => 0,
380             reason => 'cross-wiki abuse',
381             });
382              
383             if ($success) {
384             print "'$username' locked\n";
385             }
386             else {
387             print "Couldn't lock '$username':\n"
388             . " $u->{'error'}->{'details'}\n";
389             }
390             }
391             sub smry_kill {
392             return 'lock a vandalism account';
393             }
394             sub help_kill {
395             my $help = <<'=cut';
396             =head2 kill
397              
398             Lock a cross-wiki vandal's account:
399              
400             kill "Some stupid vandal"
401              
402             =cut
403              
404             return _render_help($help);
405             }
406              
407             sub run_nuke {
408             my $o = shift;
409             my $username = decode('utf8', shift);
410              
411             my $u = $o->{'SHELL'}->{'bot'};
412              
413             my $success = $u->ca_lock({
414             user => $username,
415             lock => 1,
416             hide => 2,
417             reason => 'cross-wiki abuse',
418             });
419              
420             if ($success) {
421             print "'$username' locked and hidden\n";
422             }
423             else {
424             print "Couldn't lock and hide '$username':\n"
425             . " $u->{'error'}->{'details'}\n";
426             }
427             }
428             sub smry_nuke {
429             return 'lock and hide a vandalism account';
430             }
431             sub help_nuke {
432             my $help = <<'=cut';
433             =head2 nuke
434              
435             The nuclear option: globally lock I an account:
436              
437             nuke "Mike.lifeguard lives at 123 Main St."
438              
439             =cut
440              
441             return _render_help($help);
442             }
443              
444             sub run_globalblock {
445             my $o = shift;
446             my $ip = shift;
447             my @args = @_;
448              
449             my $u = $o->{'SHELL'}->{'bot'};
450              
451             my $reason = 'cross-wiki abuse';
452             my $expiry = '31 hours';
453             my $anon_only = 0;
454             my $block = 1;
455             my $result = GetOptionsFromArray (\@args,
456             'reason|summary=s' => \$reason,
457             'expiry|length=s' => \$expiry,
458             'anon-only|ao!' => \$anon_only,
459             'block!' => \$block,
460             );
461              
462             if ($block) {
463             my $success = $u->g_block({
464             ip => $ip,
465             ao => $anon_only,
466             reason => $reason,
467             expiry => $expiry,
468             });
469              
470             if ($success) {
471             print "$ip blocked.\n"
472             }
473             else {
474             print "Couldn't block $ip:\n"
475             . " $u->{'error'}->{'details'}\n";
476             }
477             }
478             else {
479             my $success = $u->g_unblock({
480             ip => $ip,
481             reason => $reason,
482             });
483              
484             if ($success) {
485             print "$ip unblocked.\n";
486             }
487             else {
488             print "Couldn't unblock $ip:\n"
489             . " $u->{'error'}->{'details'}\n";
490             }
491             }
492             }
493             sub smry_globalblock {
494             return 'place or remove a global IP block';
495             }
496             sub help_globalblock {
497             my $help = <<'=cut';
498             =head2 globalblock
499              
500             Apply a global block to an IP or CIDR range:
501              
502             globalblock 127.0.0.1 --expiry "31 hours"
503             globalblock 192.168.0.1 --no-anon-only
504              
505             =head3 Options:
506              
507             =over 4
508              
509             =item B<--block>, --no-block
510              
511             Whether to block or unblock the target. Default is to block. When unblocking,
512             all settings except C<--reason> are ignored.
513              
514             =item B<--anon-only>, --ao
515              
516             =item B<--no-anon-only>, --no-ao
517              
518             Whether to block only anonymous users, or all users. Default is to hardblock
519             (no-anon-only).
520              
521             =item B<--reason>, --summary
522              
523             Sets the block reason.
524              
525             =item B<--expiry>, --length
526              
527             Sets the block expiry.
528              
529             =back
530              
531             =cut
532              
533             return _render_help($help);
534             }
535              
536             sub run_rollback {
537             my $o = shift;
538             my $user = shift;
539             my @args = @_;
540              
541             my $force = 0;
542             my $result = GetOptionsFromArray (\@args,
543             'force' => \$force,
544             );
545             my $max = ($force ? 0 : 10);
546              
547             my $u = $o->{'SHELL'}->{'bot'};
548             my $debug = $o->{'SHELL'}->{'debug'};
549              
550             $u->top_edits($user, {
551             max => $max,
552             hook => sub {
553             my $pages = shift;
554              
555             RV: foreach my $page (@$pages) {
556             # CAREFUL! top_edits can't filter results when you use a callback
557             next RV unless exists($page->{'top'});
558              
559             my $title = $page->{'title'};
560             print "Rolling back edit on $title... " if $debug;
561             my $success = $u->rollback($title, $user, undef, 1);
562             print ($success ? "OK\n" : "FAILED\n") if $debug;
563             }
564             },
565             });
566              
567             print "Finished reverting edits by $user.\n" if $debug;
568             }
569             sub smry_rollback {
570             return q{rollback all of a vandal's edits};
571             }
572             sub help_rollback {
573             my $help = <<'=cut';
574             =head2 rollback
575              
576             Finds all top edits by the specified user and rolls them back using
577             mark-as-bot.
578              
579             rollback "Fugly vandal"
580              
581             =over 4
582              
583             =item B<--force>
584              
585             Override the 10-query limit. Use this when reverting mass vandals which
586             have done hundreds of edits. The limit is intended to guard against
587             accidentally rolling back edits of an established user.
588              
589             =back
590              
591             Note that this does not (currently) revert page moves, nor delete page
592             creations.
593              
594             =cut
595              
596             return _render_help($help);
597             }
598              
599             =head1 SEE ALSO
600              
601             L, L and L.
602              
603             =head1 BUGS
604              
605             Yes. This is a proof-of-concept, and there are several issues with both
606             L and this module. It should be considered
607             pre-alpha. I'm releasing the code so others can find and fix them, in the
608             hope that it will be useful for others.
609              
610             Please do not report bugs to me without a patch. I have no plans to work on
611             this further.
612              
613             =head1 AUTHOR
614              
615             Mike.lifeguard
616              
617             =head1 COPYING
618              
619             Copyright (C) 2010 by Mike.lifeguard.
620              
621             This program is free software; you can redistribute it and/or modify it
622             under the terms of the GNU General Public License as published by the
623             Free Software Foundation; either version 3 of the License, or (at your
624             option) any later version.
625              
626             This library is distributed in the hope that it will be useful, but
627             WITHOUT ANY WARRANTY; without even the implied warranty of
628             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
629             General Public License for more details.
630              
631             You should have received a copy of the GNU General Public License along
632             with this program. If not, see .
633              
634             =cut
635              
636             1;
637              
638             __END__