]> git.donarmstrong.com Git - infobot.git/blob - src/UserExtra.pl
case
[infobot.git] / src / UserExtra.pl
1 #
2 # UserExtra.pl: User Commands, Public.
3 #       Author: dms
4 #
5
6 use strict;
7 use vars qw($message $arg $qWord $verb $lobotomized $who $result $chan
8         $conn $msgType $query $talkchannel $ident $memusage);
9 use vars qw(%channels %chanstats %cmdstats %count %ircstats %param
10         %cache %mask %userstats);
11
12 ### hooks get added in CommandHooks.pl.
13
14 ###
15 ### Start of commands for hooks.
16 ###
17
18 sub chaninfo {
19     my $chan = lc shift(@_);
20     my $mode;
21
22     if ($chan eq "") {          # all channels.
23         my $i           = keys %channels;
24         my $reply       = "I'm on \002$i\002 ".&fixPlural("channel",$i);
25         my $tucount     = 0;    # total user count.
26         my $uucount     = 0;    # unique user count.
27         my @array;
28
29         ### line 1.
30         foreach (sort keys %channels) {
31             if ( /^\s*$/ or / / ) {
32                 &status("chanstats: fe channels: chan == NULL.");
33                 #&ircCheck();
34                 next;
35             }
36             next if (/^_default$/);
37
38             my $str = sprintf("%s/%d", $_, scalar(keys %{ $channels{$_}{''} }));
39             push(@array, $str);
40         }
41         &performStrictReply($reply.": ".join(', ', @array));
42
43         ### total user count.
44         foreach $chan (keys %channels) {
45             $tucount += scalar(keys %{ $channels{$chan}{''} });
46         }
47
48         ### unique user count.
49         my %nicks = ();
50         foreach $chan (keys %channels) {
51             my $nick;
52             foreach $nick (keys %{ $channels{$chan}{''} }) {
53                 $nicks{$nick}++;
54             }
55         }
56         $uucount = scalar(keys %nicks);
57
58         my $chans = scalar(keys %channels);
59         &performStrictReply(
60             "i've cached \002$tucount\002 ". &fixPlural("user",$tucount).
61             ", \002$uucount\002 unique ". &fixPlural("user",$uucount).
62             ", distributed over \002$chans\002 ".
63             &fixPlural("channel", $chans)."."
64         );
65         &ircCheck();
66
67         return;
68     }
69
70     # channel specific.
71
72     if (&validChan($chan) == 0) {
73         &msg($who,"error: invalid channel \002$chan\002");
74         return;
75     }
76
77     # Step 1:
78     my @array;
79     foreach (sort keys %{ $chanstats{$chan} }) {
80         my $int = $chanstats{$chan}{$_};
81         next unless ($int);
82
83         push(@array, "\002$int\002 ". &fixPlural($_,$int));
84     }
85     my $reply = "On \002$chan\002, there ".
86                 &fixPlural("has",scalar(@array)). " been ".
87                 &IJoin(@array);
88
89     # Step 1b: check channel inconstencies.
90     $chanstats{$chan}{'Join'}           ||= 0;
91     $chanstats{$chan}{'SignOff'}        ||= 0;
92     $chanstats{$chan}{'Part'}           ||= 0;
93
94     my $delta_stats = $chanstats{$chan}{'Join'}
95                 - $chanstats{$chan}{'SignOff'}
96                 - $chanstats{$chan}{'Part'};
97
98     if ($delta_stats) {
99         my $total = scalar(keys %{ $channels{$chan}{''} });
100         &status("chaninfo: join ~= signoff + part (drift of $delta_stats < $total).");
101
102         if ($delta_stats > $total) {
103             &ERROR("chaninfo: delta_stats exceeds total users.");
104         }
105     }
106
107     # Step 2:
108     undef @array;
109     my $type;
110     foreach ("v","o","") {
111         my $int = scalar(keys %{ $channels{$chan}{$_} });
112         next unless ($int);
113
114         $type = "Voice" if ($_ eq "v");
115         $type = "Opped" if ($_ eq "o");
116         $type = "Total" if ($_ eq "");
117
118         push(@array,"\002$int\002 $type");
119     }
120     $reply .= ".  At the moment, ". &IJoin(@array);
121
122     # Step 3:
123     my %new;
124     foreach (keys %userstats) {
125         next unless (exists $userstats{$_}{'Count'});
126         if ($userstats{$_}{'Count'} =~ /^\D+$/) {
127             &WARN("userstats{$_}{Count} is non-digit.");
128             next;
129         }
130
131         $new{$_} = $userstats{$_}{'Count'};
132     }
133
134     # TODO: show top 3 with percentages?
135     my($count) = (sort { $new{$b} <=> $new{$a} } keys %new)[0];
136     if ($count) {
137         $reply .= ".  \002$count\002 has said the most with a total of \002$new{$count}\002 messages";
138     }
139     &performStrictReply("$reply.");
140 }
141
142 # Command statistics.
143 sub cmdstats {
144     my @array;
145
146     if (!scalar(keys %cmdstats)) {
147         &performReply("no-one has run any commands yet");
148         return;
149     }
150
151     my %countstats;
152     foreach (keys %cmdstats) {
153         $countstats{ $cmdstats{$_} }{$_} = 1;
154     }
155
156     foreach (sort {$b <=> $a} keys %countstats) {
157         my $int = $_;
158         next unless ($int);
159
160         foreach (keys %{ $countstats{$int} }) {
161             push(@array, "\002$int\002 of $_");
162         }
163     }
164     &performStrictReply("command usage include ". &IJoin(@array).".");
165 }
166
167 # Factoid extension info. xk++
168 sub factinfo {
169     my $faqtoid = lc shift(@_);
170     my $query   = "";
171
172     if ($faqtoid =~ /^\-(\S+)(\s+(.*))$/) {
173         &msg($who,"error: individual factoid info queries not supported as yet.");
174         &msg($who,"it's possible that the factoid mistakenly begins with '-'.");
175         return;
176
177         $query   = lc $1;
178         $faqtoid = lc $3;
179     }
180
181     &CmdFactInfo($faqtoid, $query);
182 }
183
184 sub factstats {
185     my $type = shift(@_);
186
187     &Forker("Factoids", sub {
188         &performStrictReply( &CmdFactStats($type) );
189     } );
190 }
191
192 sub karma {
193     my $target  = lc( shift || $who );
194     my $karma   = &sqlSelect("stats", "counter",
195         { nick => $target, type => "karma" }) || 0;
196
197     if ($karma != 0) {
198         &performStrictReply("$target has karma of $karma");
199     } else {
200         &performStrictReply("$target has neutral karma");
201     }
202 }
203
204 sub tell {
205     my $args = shift;
206     my ($target, $tell_obj) = ('','');
207     my $dont_tell_me    = 0;
208     my $reply;
209
210     ### is this fixed elsewhere?
211     $args =~ s/\s+/ /g;         # fix up spaces.
212     $args =~ s/^\s+|\s+$//g;    # again.
213
214     # this one catches most of them
215     if ($args =~ /^(\S+) (-?)about (.*)$/i) {
216         $target         = $1;
217         $tell_obj       = $3;
218         $dont_tell_me   = ($2) ? 1 : 0;
219
220         $tell_obj       = $who  if ($tell_obj =~ /^(me|myself)$/i);
221         $query          = $tell_obj;
222     } elsif ($args =~ /^(\S+) where (\S+) can (\S+) (.*)$/i) {
223         # i'm sure this could all be nicely collapsed
224         $target         = $1;
225         $tell_obj       = $4;
226         $query          = $tell_obj;
227
228     } elsif ($args =~ /^(\S+) (what|where) (.*?) (is|are)[.?!]*$/i) {
229         $target         = $1;
230         $qWord          = $2;
231         $tell_obj       = $3;
232         $verb           = $4;
233         $query          = "$qWord $verb $tell_obj";
234
235     } elsif ($args =~ /^(.*?) to (\S+)$/i) {
236         $target         = $3;
237         $tell_obj       = $2;
238         $query          = $tell_obj;
239     }
240
241     # check target type. Deny channel targets.
242     if ($target !~ /^$mask{nick}$/ or $target =~ /^$mask{chan}$/) {
243         &msg($who,"No, $who, I won't. (target invalid?)");
244         return;
245     }
246
247     $target     = $talkchannel  if ($target =~ /^us$/i);
248     $target     = $who          if ($target =~ /^(me|myself)$/i);
249
250     &status("tell: target = $target, query = $query");
251
252     # "intrusive".
253 #    if ($target !~ /^$mask{chan}$/ and !&IsNickInAnyChan($target)) {
254 #       &msg($who, "No, $target is not in any of my chans.");
255 #       return;
256 #    }
257
258     # self.
259     if ($target =~  /^\Q$ident\E$/i) {
260         &msg($who, "Isn't that a bit silly?");
261         return;
262     }
263
264     my $oldwho          = $who;
265     my $oldmtype        = $msgType;
266     $who                = $target;
267     my $result = &doQuestion($tell_obj);
268         # ^ returns '0' if nothing was found.
269     $who                = $oldwho;
270
271     # no such factoid.
272     if (!defined $result || $result =~ /^0?$/) {
273         $who            = $target;
274         $msgType        = "private";
275
276         # support command redirection.
277         # recursive cmdHooks aswell :)
278         my $done = 0;
279         $done++ if &parseCmdHook($tell_obj);
280         $message        = $tell_obj;
281         $done++ unless (&Modules());
282
283         &VERB("tell: setting old values of who and msgType.",2);
284         $who            = $oldwho;
285         $msgType        = $oldmtype;
286
287         if ($done) {
288             &msg($who, "told $target about CMD '$tell_obj'");
289         } else {
290             &msg($who, "i dunno what is '$tell_obj'.");
291         }
292
293         return;
294     }
295
296     # success.
297     &status("tell: <$who> telling $target about $tell_obj.");
298     if ($who ne $target) {
299         if ($dont_tell_me) {
300             &msg($who, "told $target about $tell_obj.");
301         } else {
302             &msg($who, "told $target about $tell_obj ($result)");
303         }
304
305         $reply = "$who wants you to know: $result";
306     } else {
307         $reply = "telling yourself: $result";
308     }
309
310     &msg($target, $reply);
311 }
312
313 sub countryStats {
314     if (exists $cache{countryStats}) {
315         &msg($who,"countrystats is already running!");
316         return;
317     }
318
319     if ($chan eq "") {
320         $chan = $_[0];
321     }
322
323     if ($chan eq "") {
324         &help("countrystats");
325         return;
326     }
327
328     $conn->who($chan);
329     $cache{countryStats}{chan}  = $chan;
330     $cache{countryStats}{mtype} = $msgType;
331     $cache{countryStats}{who}   = $who;
332     $cache{on_who_Hack}         = 1;
333 }
334
335 sub do_countrystats {
336     $chan       = $cache{countryStats}{chan};
337     $msgType    = $cache{countryStats}{mtype};
338     $who        = $cache{countryStats}{who};
339
340     my $total   = 0;
341     my %cstats;
342     foreach (keys %{ $cache{nuhInfo} }) {
343         my $h = $cache{nuhInfo}{$_}{Host};
344
345         if ($h =~ /^.*\.(\D+)$/) {      # host
346             $cstats{$1}++;
347         } else {                        # ip
348             $cstats{unresolve}++;
349         }
350         $total++;
351     }
352     my %count;
353     foreach (keys %cstats) {
354         $count{ $cstats{$_} }{$_} = 1;
355     }
356
357     my @list;
358     foreach (sort {$b <=> $a} keys %count) {
359         my $str = join(", ", sort keys %{ $count{$_} });
360 #       push(@list, "$str ($_)");
361         my $perc        = sprintf("%.01f", 100 * $_ / $total);
362         $perc           =~ s/\.0+$//;
363         push(@list, "$str ($_, $perc %)");
364     }
365
366     # TODO: move this into a scheduler
367     $msgType    = "private";
368     &performStrictReply( &formListReply(0, "Country Stats ", @list) );
369
370     delete $cache{countryStats};
371     delete $cache{on_who_Hack};
372 }
373
374 ###
375 ### amalgamated commands.
376 ###
377
378 sub userCommands {
379     # conversion: ascii.
380     if ($message =~ /^(asci*|chr) (\d+)$/) {
381         &DEBUG("ascii/chr called ...");
382         return unless (&IsChanConfOrWarn("allowConv"));
383
384         &DEBUG("ascii/chr called");
385
386         $arg    = $2;
387         $result = chr($arg);
388         $result = "NULL"        if ($arg == 0);
389
390         &performReply( sprintf("ascii %s is '%s'", $arg, $result) );
391
392         return;
393     }
394
395     # conversion: ord.
396     if ($message =~ /^ord(\s+(.*))$/) {
397         return unless (&IsChanConfOrWarn("allowConv"));
398
399         $arg = $2;
400
401         if (!defined $arg or length $arg != 1) {
402             &help("ord");
403             return;
404         }
405
406         if (ord($arg) < 32) {
407             $arg = chr(ord($arg) + 64);
408             if ($arg eq chr(64)) {
409                 $arg = 'NULL';
410             } else {
411                 $arg = '^'.$arg;
412             }
413         }
414
415         &performReply( sprintf("'%s' is ascii %s", $arg, ord $arg) );
416         return;
417     }
418
419     # hex.
420     if ($message =~ /^hex(\s+(.*))?$/i) {
421         return unless (&IsChanConfOrWarn("allowConv"));
422         my $arg = $2;
423
424         if (!defined $arg) {
425             &help("hex");
426             return;
427         }
428
429         if (length $arg > 80) {
430             &msg($who, "Too long.");
431             return;
432         }
433
434         my $retval;
435         foreach (split //, $arg) {
436             $retval .= sprintf(" %X", ord($_));
437         }
438
439         &performStrictReply("$arg is$retval");
440
441         return;
442     }
443
444     # crypt.
445     if ($message =~ /^crypt(\s+(.*))?$/i) {
446         my @args        = split /\s+/, $2;
447
448         if (!scalar @args or scalar @args > 2) {
449             &help("crypt");
450             return;
451         }
452
453         if (scalar @args == 2) {
454 # disable cause $1$ will use md5
455 #           if (length $args[0] != 2) {
456 #               &msg($who, "invalid format...");
457 #               return;
458 #           }
459
460             &performStrictReply( crypt($args[1], $args[0]) );
461         } else {
462             &performStrictReply( &mkcrypt($args[0]) );
463         }
464
465         return;
466     }
467
468     # cycle.
469     if ($message =~ /^(cycle)(\s+(\S+))?$/i) {
470         return unless (&hasFlag("o"));
471         my $chan = lc $3;
472
473         if ($chan eq "") {
474             if ($msgType =~ /public/) {
475                 $chan = $talkchannel;
476                 &DEBUG("cycle: setting chan to '$chan'.");
477             } else {
478                 &help("cycle");
479                 return;
480             }
481         }
482
483         if (&validChan($chan) == 0) {
484             &msg($who,"error: invalid channel \002$chan\002");
485             return;
486         }
487
488         &msg($chan, "I'm coming back. (courtesy of $who)");
489         &part($chan);
490 ###     &ScheduleThis(5, "getNickInUse") if (@_);
491         &status("Schedule rejoin in 5secs to $chan by $who.");
492         $conn->schedule(5, sub { &joinchan($chan); });
493
494         return;
495     }
496
497     # reload.
498     if ($message =~ /^reload$/i) {
499         return unless (&hasFlag("n"));
500
501         &status("USER reload $who");
502         &performStrictReply("reloading...");
503         my $modules = &reloadAllModules();
504         &performStrictReply("reloaded:$modules");
505         return;
506     }
507
508     # redir.
509     if ($message =~ /^redir(\s+(.*))?/i) {
510         return unless (&hasFlag("o"));
511         my $factoid = $2;
512
513         if (!defined $factoid) {
514             &help("redir");
515             return;
516         }
517
518         my $val  = &getFactInfo($factoid, "factoid_value");
519         if (!defined $val or $val eq "") {
520             &msg($who, "error: '$factoid' does not exist.");
521             return;
522         }
523         &DEBUG("val => '$val'.");
524         my @list = &searchTable("factoids", "factoid_key",
525                                         "factoid_value", "^$val\$");
526
527         if (scalar @list == 1) {
528             &msg($who, "hrm... '$factoid' is unique.");
529             return;
530         }
531         if (scalar @list > 5) {
532             &msg($who, "A bit too many factoids to be redirected, hey?");
533             return;
534         }
535
536         my @redir;
537         &status("Redirect '$factoid' (". ($#list) .")...");
538         for (@list) {
539             my $x = $_;
540             next if (/^\Q$factoid\E$/i);
541
542             &status("  Redirecting '$_'.");
543             my $was = &getFactoid($_);
544             if ($was =~ /<REPLY> see/i) {
545                 &status("warn: not redirecting a redirection.");
546                 next;
547             }
548
549             &DEBUG("  was '$was'.");
550             push(@redir,$x);
551             &setFactInfo($x, "factoid_value", "<REPLY> see $factoid");
552         }
553         &status("Done.");
554
555         &msg($who, &formListReply(0, "'$factoid' is redirected to by '", @redir));
556
557         return;
558     }
559
560     # rot13 it.
561     if ($message =~ /^rot([0-9]*)(\s+(.*))?/i) {
562         my $reply = $3;
563
564         if (!defined $reply) {
565             &help("rot13");
566             return;
567         }
568         my $num = $1 % 26;
569         my $upper="ABCDEFGHIJKLMNOPQRSTUVWXYZ";
570         my $lower="abcdefghijklmnopqrstuvwxyz";
571         my $to=substr($upper,$num).substr($upper,0,$num).substr($lower,$num).substr($lower,0,$num);
572         eval "\$reply =~ tr/$upper$lower/$to/;";
573
574         #$reply =~ y/A-Za-z/N-ZA-Mn-za-m/;
575         &performStrictReply($reply);
576
577         return;
578     }
579
580     # cpustats.
581     if ($message =~ /^cpustats$/i) {
582         if ($^O !~ /linux/) {
583             &ERROR("cpustats: your OS is not supported yet.");
584             return;
585         }
586
587         ### poor method to get info out of file, please fix.
588         open(STAT,"/proc/$$/stat");
589         my $line = <STAT>;
590         chop $line;
591         my @data = split(/ /, $line);
592         close STAT;
593
594         # utime(13) + stime(14).
595         my $cpu_usage   = sprintf("%.01f", ($data[13]+$data[14]) / 100 );
596         # cutime(15) + cstime (16).
597         my $cpu_usage2  = sprintf("%.01f", ($data[15]+$data[16]) / 100 );
598         my $time        = time() - $^T;
599         my $raw_perc    = $cpu_usage*100/$time;
600         my $raw_perc2   = $cpu_usage2*100/$time;
601         my $perc;
602         my $perc2;
603         my $total;
604         my $ratio;
605
606         if ($raw_perc > 1) {
607             $perc       = sprintf("%.01f", $raw_perc);
608             $perc2      = sprintf("%.01f", $raw_perc2);
609             $total      = sprintf("%.01f", $raw_perc+$raw_perc2);
610         } elsif ($raw_perc > 0.1) {
611             $perc       = sprintf("%.02f", $raw_perc);
612             $perc2      = sprintf("%.02f", $raw_perc2);
613             $total      = sprintf("%.02f", $raw_perc+$raw_perc2);
614         } else {                        # <=0.1
615             $perc       = sprintf("%.03f", $raw_perc);
616             $perc2      = sprintf("%.03f", $raw_perc2);
617             $total      = sprintf("%.03f", $raw_perc+$raw_perc2);
618         }
619         $ratio  = sprintf("%.01f", 100*$perc/($perc+$perc2) );
620
621         &performStrictReply("Total CPU usage: \002$cpu_usage\002 s ... ".
622                 "Total used: \002$total\002 % ".
623                 "(parent/child ratio: $ratio %)"
624         );
625
626         return;
627     }
628
629     # ircstats.
630     if ($message =~ /^ircstats?$/i) {
631         $ircstats{'TotalTime'}  ||= 0;
632         $ircstats{'OffTime'}    ||= 0;
633
634         my $count       = $ircstats{'ConnectCount'};
635         my $format_time = &Time2String(time() - $ircstats{'ConnectTime'});
636         my $total_time  = time() - $ircstats{'ConnectTime'} +
637                                 $ircstats{'TotalTime'};
638         my $reply;
639
640         my $connectivity = 100 * ($total_time - $ircstats{'OffTime'}) /
641                                 $total_time;
642         my $p = sprintf("%.03f", $connectivity);
643         $p =~ s/(\.\d*)0+$/$1/;
644         if ($p =~ s/\.0$//) {
645             # this should not happen... but why...
646         } else {
647             $p =~ s/\.$//
648         }
649
650         if ($total_time != (time() - $ircstats{'ConnectTime'}) ) {
651             my $tt_format = &Time2String($total_time);
652             &DEBUG("tt_format => $tt_format");
653         }
654
655         ### RECONNECT COUNT.
656         if ($count == 1) {      # good.
657             $reply = "I'm connected to $ircstats{'Server'} and have been so".
658                 " for $format_time";
659         } else {
660             $reply = "Currently I'm hooked up to $ircstats{'Server'} but only".
661                 " for $format_time.  ".
662                 "I had to reconnect \002$count\002 times.".
663                 "   Connectivity: $p %";
664         }
665
666         ### REASON.
667         my $reason = $ircstats{'DisconnectReason'};
668         if (defined $reason) {
669             $reply .= ".  I was last disconnected for '$reason'.";
670         }
671
672         &performStrictReply($reply);
673
674         return;
675     }
676
677     # status.
678     if ($message =~ /^statu?s$/i) {
679         my $startString = scalar(gmtime $^T);
680         my $upString    = &Time2String(time() - $^T);
681         my ($puser,$psystem,$cuser,$csystem) = times;
682         my $factoids    = &countKeys("factoids");
683         my $forks = 0;
684         foreach (keys %forked) {
685             $forks += scalar keys %{ $forked{$_} };
686         }
687         $forks /= 2;
688         $count{'Commands'}      = 0;
689         foreach (keys %cmdstats) {
690             $count{'Commands'} += $cmdstats{$_};
691         }
692
693         &performStrictReply(
694         "Since $startString, there have been".
695           " \002$count{'Update'}\002 ".
696                 &fixPlural("modification", $count{'Update'}).
697           ", \002$count{'Question'}\002 ".
698                 &fixPlural("question",$count{'Question'}).
699           ", \002$count{'Dunno'}\002 ".
700                 &fixPlural("dunno",$count{'Dunno'}).
701           ", \002$count{'Moron'}\002 ".
702                 &fixPlural("moron",$count{'Moron'}).
703           " and \002$count{'Commands'}\002 ".
704                 &fixPlural("command",$count{'Commands'}).
705           ".  I have been awake for $upString this session, and ".
706           "currently reference \002$factoids\002 factoids.  ".
707           "I'm using about \002$memusage\002 ".
708           "kB of memory. With \002$forks\002 active ".
709                 &fixPlural("fork",$forks).
710           ". Process time user/system $puser/$psystem child $cuser/$csystem"
711         );
712
713         return;
714     }
715
716     # wantNick. xk++
717     # FIXME does not try to get nick "back", just switches nicks
718     if ($message =~ /^wantNick\s(.*)?$/i) {
719         return unless (&hasFlag("o"));
720         my $wantnick = lc $1;
721         my $mynick = $conn->nick();
722
723         if ($mynick eq $wantnick) {
724             &msg($who, "I hope you're right. I'll try anyway (mynick=$mynick, wantnick=$wantnick).");
725         }
726
727         # fallback check, I guess.  needed?
728         if (! &IsNickInAnyChan( $wantnick ) ) {
729             my $str = "attempting to change nick from $mynick to $wantnick";
730             &status($str);
731             &msg($who, $str);
732             &nick($wantnick);
733             return;
734         }
735
736         # idea from dondelecarlo :)
737         # TODO: use cache{nickserv}
738         if ($param{'nickServ_pass'}) {
739             my $str = "someone is using nick $wantnick; GHOSTing";
740             &status($str);
741             &msg($who, $str);
742             &msg("NickServ", "GHOST $wantnick $param{'nickServ_pass'}");
743
744             $conn->schedule(5, sub {
745                 &status("going to change nick from $mynick to $wantnick after GHOST.");
746                 &nick($wantnick);
747             } );
748
749             return;
750         }
751
752         return;
753     }
754
755     return "CONTINUE";
756 }
757
758 1;