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