]> git.donarmstrong.com Git - infobot.git/blob - src/IRC/Schedulers.pl
- initial layout for Scheduler II.
[infobot.git] / src / IRC / Schedulers.pl
1 #
2 # ProcessExtra.pl: Extensions to Process.pl
3 #          Author: dms
4 #         Version: v0.5 (20010124)
5 #         Created: 20000117
6 #
7
8 # use strict;   # TODO
9
10 use POSIX qw(strftime);
11 use vars qw(%sched %schedule);
12
13 # format: function name = (
14 #       str     chanconfdefault,
15 #       int     internaldefault,
16 #       bool    deferred,
17 #       int     next run,               (optional)
18 # )
19
20 #%schedule = {
21 #       uptimeLoop => ("", 60, 1),
22 #};
23
24 sub setupSchedulersII {
25     foreach (keys %schedule) {
26         &queueTask($_, @{ $schedule{$_} });
27     }
28 }
29
30 sub queueTask {
31     my($codename, $chanconfdef, $intervaldef, $defer) = @_;
32     my $t = &getChanConfDefault($chanconfdef, $intervaldef);
33     my $waittime = &getRandomInt($t);
34
35     if (!defined $waittime) {
36         &WARN("interval == waittime == UNDEF for $codename.");
37         return;
38     }
39
40     my $time = $schedule{$codename}[3];
41     if (defined $time and $time > time()) {
42         &WARN("Sched for $codename already exists.");
43         return;
44     }
45
46 #    &VERB("Scheduling \&$codename() for ".&Time2String($waittime),3);
47
48     my $retval = $conn->schedule($waittime, sub {
49                 \&$codename;
50     }, @args );
51 }
52
53 sub setupSchedulers {
54     &VERB("Starting schedulers...",2);
55
56     # ONCE OFF.
57
58     # REPETITIVE.
59     # 1 for run straight away, 2 for on next-run.
60     &uptimeLoop(1);
61     &randomQuote(2);
62     &randomFactoid(2);
63     &randomFreshmeat(2);
64     &logLoop(1);
65     &chanlimitCheck(1);
66     &netsplitCheck(1);  # mandatory
67     &floodLoop(1);      # mandatory
68     &seenFlush(2);
69     &leakCheck(2);      # mandatory
70     &ignoreCheck(1);    # mandatory
71     &seenFlushOld(2);
72     &ircCheck(1);       # mandatory
73     &miscCheck(1);      # mandatory
74     &miscCheck2(2);     # mandatory
75     &shmFlush(1);       # mandatory
76     &slashdotLoop(2);
77     &plugLoop(2);
78     &freshmeatLoop(2);
79     &kernelLoop(2);
80     &wingateWriteFile(2);
81     &factoidCheck(2);   # takes a couple of seconds on a 486. defer it
82 # todo: convert to new format... or nuke altogether.
83     &newsFlush(1);
84
85     # todo: squeeze this into a one-liner.
86 #    my $count = map { exists $sched{$_}{TIME} } keys %sched;
87     my $count   = 0;
88     foreach (keys %sched) {
89         my $time = $sched{$_}{TIME};
90         next unless (defined $time and $time > time());
91
92         $count++;
93     }
94
95     &status("Schedulers: $count will be running.");
96 ###    &scheduleList();
97 }
98
99 sub ScheduleThis {
100     my ($interval, $codename, @args) = @_;
101     my $waittime = &getRandomInt($interval);
102
103     if (!defined $waittime) {
104         &WARN("interval == waittime == UNDEF for $codename.");
105         return;
106     }
107
108     my $time = $sched{$codename}{TIME};
109     if (defined $time and $time > time()) {
110         &WARN("Sched for $codename already exists.");
111         return;
112     }
113
114 #    &VERB("Scheduling \&$codename() for ".&Time2String($waittime),3);
115
116     my $retval = $conn->schedule($waittime, \&$codename, @args);
117     $sched{$codename}{LABEL}    = $retval;
118     $sched{$codename}{TIME}     = time()+$waittime;
119     $sched{$codename}{LOOP}     = 1;
120 }
121
122 ####
123 #### LET THE FUN BEGIN.
124 ####
125
126 sub randomQuote {
127     my $interval = &getChanConfDefault("randomQuoteInterval", 60);
128     if (@_) {
129         &ScheduleThis($interval, "randomQuote");
130         return if ($_[0] eq "2");       # defer.
131     }
132
133     my $line = &getRandomLineFromFile($bot_data_dir. "/blootbot.randtext");
134     if (!defined $line) {
135         &ERROR("random Quote: weird error?");
136         return;
137     }
138
139     foreach ( &ChanConfList("randomQuote") ) {
140         next unless (&validChan($_));
141
142         &status("sending random Quote to $_.");
143         &action($_, "Ponders: ".$line);
144     }
145     ### TODO: if there were no channels, don't reschedule until channel
146     ###         configuration is modified.
147 }
148
149 sub randomFactoid {
150     my ($key,$val);
151     my $error = 0;
152
153     my $interval = &getChanConfDefault("randomFactoidInterval", 60);
154     if (@_) {
155         &ScheduleThis($interval, "randomFactoid");
156         return if ($_[0] eq "2");       # defer.
157     }
158
159     while (1) {
160         ($key,$val) = &randKey("factoids","factoid_key,factoid_value");
161 ###     $val =~ tr/^[A-Z]/[a-z]/;       # blah is Good => blah is good.
162         last if (defined $val and $val !~ /^</);
163
164         $error++;
165         if ($error == 5) {
166             &ERROR("rF: tried 5 times but failed.");
167             return;
168         }
169     }
170
171     foreach ( &ChanConfList("randomFactoid") ) {
172         next unless (&validChan($_));
173
174         &status("sending random Factoid to $_.");
175         &action($_, "Thinks: \037$key\037 is $val");
176         ### FIXME: Use &getReply() on above to format factoid properly?
177         $good++;
178     }
179 }
180
181 sub randomFreshmeat {
182     my $interval = &getChanConfDefault("randomFreshmeatInterval", 60);
183
184     if (@_) {
185         &ScheduleThis($interval, "randomFreshmeat");
186         return if ($_[0] eq "2");       # defer.
187     }
188
189     my @chans = &ChanConfList("randomFreshmeat");
190     return unless (scalar @chans);
191
192     &Forker("freshmeat", sub {
193         my $retval = &Freshmeat::randPackage();
194
195         foreach (@chans) {
196             next unless (&validChan($_));
197
198             &status("sending random Freshmeat to $_.");
199             &say($_, $line);
200         }
201     } );
202 }
203
204 sub logLoop {
205     if (@_) {
206         &ScheduleThis(60, "logLoop");
207         return if ($_[0] eq "2");       # defer.
208     }
209
210     return unless (defined fileno LOG);
211     return unless (&IsParam("logfile"));
212     return unless (&IsParam("maxLogSize"));
213
214     ### check if current size is too large.
215     if ( -s $file{log} > $param{'maxLogSize'}) {
216         my $date = sprintf("%04d%02d%02d", (gmtime)[5,4,3]);
217         $file{log} = $param{'logfile'} ."-". $date;
218         &status("cycling log file.");
219
220         if ( -e $file{log}) {
221             my $i = 1;
222             my $newlog;
223             while () {
224                 $newlog = $file{log}."-".$i;
225                 last if (! -e $newlog);
226                 $i++;
227             }
228             $file{log} = $newlog;
229         }
230
231         &closeLog();
232         CORE::system("/bin/mv '$param{'logfile'}' '$file{log}'");
233         &compress($file{log});
234         &openLog();
235         &status("cycling log file.");
236     }
237
238     ### check if all the logs exceed size.
239     if (!opendir(LOGS, $bot_log_dir)) {
240         &WARN("logLoop: could not open dir '$bot_log_dir'");
241         return;
242     }
243
244     my $tsize           = 0;
245     my (%age, %size);
246     while (defined($_ = readdir LOGS)) {
247         my $logfile     = "$bot_log_dir/$_";
248
249         next unless ( -f $logfile);
250
251         my $size        = -s $logfile;
252         my $age         = (stat $logfile)[9];
253         $age{$age}      = $logfile;
254         $size{$logfile} = $size;
255         $tsize          += $size;
256     }
257     closedir LOGS;
258
259     my $delete  = 0;
260     while ($tsize > $param{'maxLogSize'}) {
261         &status("LOG: current size > max ($tsize > $param{'maxLogSize'})");
262         my $oldest      = (sort {$a <=> $b} keys %age)[0];
263         &status("LOG: unlinking $age{$oldest}.");
264         unlink $age{$oldest};
265         $tsize          -= $oldest;
266         $delete++;
267     }
268
269     ### TODO: add how many b,kb,mb removed?
270     &status("LOG: removed $delete logs.") if ($delete);
271 }
272
273 sub seenFlushOld {
274     if (@_) {
275         &ScheduleThis(1440, "seenFlushOld");
276         return if ($_[0] eq "2");       # defer.
277     }
278
279     # is this global-only?
280     return unless (&IsChanConf("seen") > 0);
281     return unless (&IsChanConf("seenFlushInterval") > 0);
282
283     # global setting. does not make sense for per-channel.
284     my $max_time = &getChanConfDefault("seenMaxDays", 30) *60*60*24;
285     my $delete   = 0;
286
287     if ($param{'DBType'} =~ /^pgsql|mysql|sqlite/i) {
288         my $query;
289
290         if ($param{'DBType'} =~ /^mysql|sqlite$/i) {
291             $query = "SELECT nick,time FROM seen GROUP BY nick HAVING ".
292                         "UNIX_TIMESTAMP() - time > $max_time";
293         } else {        # pgsql.
294             $query = "SELECT nick,time FROM seen WHERE ".
295                 "extract(epoch from timestamp 'now') - time > $max_time";
296         }
297
298         my $sth = $dbh->prepare($query);
299         if ($sth->execute) {
300             while (my @row = $sth->fetchrow_array) {
301                 my ($nick,$time) = @row;
302
303                 &dbDel("seen",{"nick"=>$nick});
304                 $delete++;
305             }
306             $sth->finish;
307         }
308     } elsif ($param{'DBType'} =~ /^dbm/i) {
309         my $time = time();
310
311         foreach (keys %seen) {
312             my $delta_time = $time - &dbGet("seen", "time", "nick", $_);
313             next unless ($delta_time > $max_time);
314
315             &DEBUG("seenFlushOld: ".&Time2String($delta_time) );
316             delete $seen{$_};
317             $delete++;
318         }
319     } else {
320         &FIXME("seenFlushOld: for PG/NO-DB.");
321     }
322     &VERB("SEEN deleted $delete seen entries.",2);
323
324 }
325
326 sub newsFlush {
327     if (@_) {
328         &ScheduleThis(60, "newsFlush");
329         return if ($_[0] eq "2");       # defer.
330     }
331
332     if (!&ChanConfList("news")) {
333         &DEBUG("newsFlush: news disabled? (chan => $chan)");
334         return;
335     }
336
337     my $delete  = 0;
338     my $oldest  = time();
339     my %none;
340     foreach $chan (keys %::news) {
341         my $i           = 0;
342         my $total       = scalar(keys %{ $::news{$chan} });
343
344         if (!$total) {
345             delete $::news{$chan};
346             next;
347         }
348
349         foreach $item (keys %{ $::news{$chan} }) {
350             my $t = $::news{$chan}{$item}{Expire};
351
352             my $tadd    = $::news{$chan}{$item}{Time};
353             $oldest     = $tadd if ($oldest > $tadd);
354
355             next if ($t == 0 or $t == -1);
356             if ($t < 1000) {
357                 &status("newsFlush: Fixed Expire time for $chan/$item, should not happen anyway.");
358                 $::news{$chan}{$item}{Expire} = time() + $t*60*60*24;
359                 next;
360             }
361
362             my $delta = $t - time();
363
364             next unless (time() > $t);
365
366             # todo: show how old it was.
367             delete $::news{$chan}{$item};
368             &status("NEWS: (newsflush) deleted '$item'");
369             $delete++;
370             $i++;
371         }
372
373         &status("NEWS (newsflush) {$chan}: deleted [$i/$total] news entries.") if ($i);
374         $none{$chan} = 1 if ($total == $i);
375     }
376
377     # todo: flush users aswell.
378     my $duser   = 0;
379     foreach $chan (keys %::newsuser) {
380         next if (exists $none{$chan});
381
382         foreach (keys %{ $::newsuser{$chan} }) {
383             my $t = $::newsuser{$chan}{$_};
384             if (!defined $t or ($t > 2 and $t < 1000)) {
385                 &DEBUG("something wrong with newsuser{$chan}{$_} => $t");
386                 next;
387             }
388
389             next unless ($oldest > $t);
390
391             delete $::newsuser{$chan}{$_};
392             $duser++;
393         }
394
395         my $i = scalar(keys %{ $::newsuser{$chan} });
396         delete $::newsuser{$chan} unless ($i);
397     }
398
399     if ($delete or $duser) {
400         &status("NewsFlush: deleted: $delete news entries; $duser user cache.");
401     }
402 }
403
404 sub chanlimitCheck {
405     my $interval = &getChanConfDefault("chanlimitcheckInterval", 10);
406
407     if (@_) {
408         &ScheduleThis($interval, "chanlimitCheck");
409         return if ($_[0] eq "2");
410     }
411
412     my $str = join(' ', &ChanConfList("chanlimitcheck") );
413
414     foreach $chan ( &ChanConfList("chanlimitcheck") ) {
415         next unless (&validChan($chan));
416
417         if ($chan eq "_default") {
418             &WARN("chanlimit: we're doing $chan!! HELP ME!");
419             next;
420         }
421
422         my $limitplus   = &getChanConfDefault("chanlimitcheckPlus", 5, $chan);
423         my $newlimit    = scalar(keys %{ $channels{$chan}{''} }) + $limitplus;
424         my $limit       = $channels{$chan}{'l'};
425
426         if (scalar keys %netsplitservers) {
427             if (defined $limit) {
428                 &status("chanlimit: netsplit; removing it for $chan.");
429                 $conn->mode($chan, "-l");
430                 $cache{chanlimitChange}{$chan} = time();
431                 &status("chanlimit: netsplit; removed.");
432             }
433
434             next;
435         }
436
437         if (defined $limit and scalar keys %{ $channels{$chan}{''} } > $limit) {
438             &FIXME("LIMIT: set too low!!! FIXME");
439             ### run NAMES again and flush it.
440         }
441
442         if (defined $limit and $limit == $newlimit) {
443             $cache{chanlimitChange}{$chan} = time();
444             next;
445         }
446
447         if (!exists $channels{$chan}{'o'}{$ident}) {
448             &status("chanlimit: dont have ops on $chan.") unless (exists $cache{warn}{chanlimit}{$chan});
449             $cache{warn}{chanlimit}{$chan} = 1;
450             &chanServCheck($chan);
451             next;
452         }
453         delete $cache{warn}{chanlimit}{$chan};
454
455         if (!defined $limit) {
456             &status("chanlimit: setting for first time or from netsplit, for $chan");
457         }
458
459         if (exists $cache{chanlimitChange}{$chan}) {
460             my $delta = time() - $cache{chanlimitChange}{$chan};
461             if ($delta < $interval*60) {
462                 &DEBUG("chanlimit: not going to change chanlimit! ($delta<$interval*60)");
463                 return;
464             }
465         }
466
467         $conn->mode($chan, "+l", $newlimit);
468         $cache{chanlimitChange}{$chan} = time();
469     }
470 }
471
472 sub netsplitCheck {
473     my ($s1,$s2);
474
475     if (@_) {
476         &ScheduleThis(15, "netsplitCheck");
477         return if ($_[0] eq "2");
478     }
479
480     $cache{'netsplitCache'}++;
481 #    &DEBUG("running netsplitCheck... $cache{netsplitCache}");
482
483     if (!scalar %netsplit and scalar %netsplitservers) {
484         &DEBUG("nsC: !hash netsplit but hash netsplitservers <- removing!");
485         undef %netsplitservers;
486         return;
487     }
488
489     # well... this shouldn't happen since %netsplit code does it anyway.
490     foreach $s1 (keys %netsplitservers) {
491
492         foreach $s2 (keys %{ $netsplitservers{$s1} }) {
493             my $delta = time() - $netsplitservers{$s1}{$s2};
494
495             if ($delta > 60*30) {
496                 &status("netsplit between $s1 and $s2 appears to be stale.");
497                 delete $netsplitservers{$s1}{$s2};
498                 &chanlimitCheck();
499             }
500         }
501
502         my $i = scalar(keys %{ $netsplitservers{$s1} });
503         delete $netsplitservers{$s1} unless ($i);
504     }
505
506     # %netsplit hash checker.
507     my $count   = scalar keys %netsplit;
508     my $delete  = 0;
509     foreach (keys %netsplit) {
510         if (&IsNickInAnyChan($_)) {     # why would this happen?
511 #           &DEBUG("nsC: $_ is in some chan; removing from netsplit list.");
512             delete $netsplit{$_};
513             $delete++;
514             next;
515         }
516
517         next unless (time() - $netsplit{$_} > 60*15);
518
519         $delete++;
520         delete $netsplit{$_};
521     }
522
523     # yet another hack.
524     foreach (keys %channels) {
525         my $i = $cache{maxpeeps}{$chan} || 0;
526         my $j = scalar(keys %{ $channels{$chan} });
527         next unless ($i > 10 and 0.25*$i > $j);
528
529         &DEBUG("netsplit: 0.25*max($i) > current($j); possible netsplit?");
530     }
531
532     if ($delete) {
533         my $j = scalar(keys %netsplit);
534         &status("nsC: removed from netsplit list: (before: $count; after: $j)");
535     }
536
537     if (!scalar %netsplit and scalar %netsplitservers) {
538         &DEBUG("nsC: ok hash netsplit is NULL; purging hash netsplitservers");
539         undef %netsplitservers;
540     }
541
542     if ($count and !scalar keys %netsplit) {
543         &DEBUG("nsC: netsplit is hopefully gone. reinstating chanlimit check.");
544         &chanlimitCheck();
545     }
546 }
547
548 sub floodLoop {
549     my $delete   = 0;
550     my $who;
551
552     if (@_) {
553         &ScheduleThis(60, "floodLoop"); # minutes.
554         return if ($_[0] eq "2");
555     }
556
557     my $time            = time();
558     my $interval        = &getChanConfDefault("floodCycle",60);
559
560     foreach $who (keys %flood) {
561         foreach (keys %{ $flood{$who} }) {
562             if (!exists $flood{$who}{$_}) {
563                 &WARN("flood{$who}{$_} undefined?");
564                 next;
565             }
566
567             if ($time - $flood{$who}{$_} > $interval) {
568                 delete $flood{$who}{$_};
569                 $delete++;
570             }
571         }
572     }
573     &VERB("floodLoop: deleted $delete items.",2);
574 }
575
576 sub seenFlush {
577     if (@_) {
578         my $interval = &getChanConfDefault("seenFlushInterval", 60);
579         &ScheduleThis($interval, "seenFlush");
580         return if ($_[0] eq "2");
581     }
582
583     my %stats;
584     my $nick;
585     my $flushed         = 0;
586     $stats{'count_old'} = &countKeys("seen") || 0;
587     $stats{'new'}       = 0;
588     $stats{'old'}       = 0;
589
590     if ($param{'DBType'} =~ /^(mysql|pgsql|sqlite|dbm)$/i) {
591         foreach $nick (keys %seencache) {
592             my $retval = &sqlReplace("seen", {
593                         nick    => lc $seencache{$nick}{'nick'},
594                         time    => $seencache{$nick}{'time'},
595                         host    => $seencache{$nick}{'host'},
596                         channel => $seencache{$nick}{'chan'},
597                         message => $seencache{$nick}{'msg'},
598             } );
599
600             delete $seencache{$nick};
601             $flushed++;
602         }
603     } else {
604         &DEBUG("seenFlush: NO VALID FACTOID SUPPORT?");
605     }
606
607     &status("Seen: Flushed $flushed entries.")  if ($flushed);
608     &VERB(sprintf("  new seen: %03.01f%% (%d/%d)",
609         $stats{'new'}*100/($stats{'count_old'} || 1),
610         $stats{'new'}, ( $stats{'count_old'} || 1) ), 2) if ($stats{'new'});
611     &VERB(sprintf("  now seen: %3.1f%% (%d/%d)",
612         $stats{'old'}*100 / ( &countKeys("seen") || 1),
613         $stats{'old'}, &countKeys("seen") ), 2)         if ($stats{'old'});
614
615     &WARN("scalar keys seenflush != 0!")        if (scalar keys %seenflush);
616 }
617
618 sub leakCheck {
619     my ($blah1,$blah2);
620     my $count = 0;
621
622     if (@_) {
623         &ScheduleThis(240, "leakCheck");
624         return if ($_[0] eq "2");
625     }
626
627     # flood. this is dealt with in floodLoop()
628     foreach $blah1 (keys %flood) {
629         foreach $blah2 (keys %{ $flood{$blah1} }) {
630             $count += scalar(keys %{ $flood{$blah1}{$blah2} });
631         }
632     }
633     &VERB("leak: hash flood has $count total keys.",2);
634
635     # floodjoin.
636     $count = 0;
637     foreach $blah1 (keys %floodjoin) {
638         foreach $blah2 (keys %{ $floodjoin{$blah1} }) {
639             $count += scalar(keys %{ $floodjoin{$blah1}{$blah2} });
640         }
641     }
642     &VERB("leak: hash floodjoin has $count total keys.",2);
643
644     # floodwarn.
645     $count = scalar(keys %floodwarn);
646     &VERB("leak: hash floodwarn has $count total keys.",2);
647
648     my $chan;
649     foreach $chan (grep /[A-Z]/, keys %channels) {
650         &DEBUG("leak: chan => '$chan'.");
651         my ($i,$j);
652         foreach $i (keys %{ $channels{$chan} }) {
653             foreach (keys %{ $channels{$chan}{$i} }) {
654                 &DEBUG("leak:   \$channels{$chan}{$i}{$_} ...");
655             }
656         }
657     }
658
659     # chanstats
660     $count = scalar(keys %chanstats);
661     &VERB("leak: hash chanstats has $count total keys.",2);
662
663     # nuh.
664     my $delete  = 0;
665     foreach (keys %nuh) {
666         next if (&IsNickInAnyChan($_));
667         next if (exists $dcc{CHAT}{$_});
668
669         delete $nuh{$_};
670         $delete++;
671     }
672
673     &status("leak: $delete nuh{} items deleted; now have ".
674                                 scalar(keys %nuh) ) if ($delete);
675 }
676
677 sub ignoreCheck {
678     if (@_) {
679         &ScheduleThis(60, "ignoreCheck");
680         return if ($_[0] eq "2");       # defer.
681     }
682
683     my $time    = time();
684     my $count   = 0;
685
686     foreach (keys %ignore) {
687         my $chan = $_;
688
689         foreach (keys %{ $ignore{$chan} }) {
690             my @array = @{ $ignore{$chan}{$_} };
691
692             next unless ($array[0] and $time > $array[0]);
693
694             delete $ignore{$chan}{$_};
695             &status("ignore: $_/$chan has expired.");
696             $count++;
697         }
698     }
699
700     $cache{ignoreCheckTime} = time();
701
702     &VERB("ignore: $count items deleted.",2);
703 }
704
705 sub ircCheck {
706     if (@_) {
707         &ScheduleThis(15, "ircCheck");
708         return if ($_[0] eq "2");       # defer.
709     }
710
711     $cache{statusSafe} = 1;
712
713     my @x       = &getJoinChans();
714     my $iconf   = scalar( @x );
715     my $inow    = scalar( keys %channels );
716     if ($iconf > 2 and $inow * 2 <= $iconf) {
717         &FIXME("ircCheck: current channels * 2 <= config channels. FIXME.");
718         &FIXME("ircCheck: iconf = $iconf");
719         &FIXME("ircCheck: inow  = $inow");
720 #       @joinchan       = @x;
721         &joinNextChan();
722     }
723
724     if (!$conn->connected or time() - $msgtime > 3600) {
725         # todo: shouldn't we use cache{connect} somewhere?
726         if (exists $cache{connect}) {
727             &WARN("ircCheck: no msg for 3600 and disco'd! reconnecting!");
728             $msgtime = time();  # just in case.
729             &ircloop();
730             delete $cache{connect};
731         } else {
732             &status("IRCTEST: possible lost in space; checking. ".
733                 scalar(gmtime) );
734             &msg($ident, "TEST");
735             $cache{connect} = time();
736         }
737     }
738
739     if ($ident !~ /^\Q$param{ircNick}\E$/) {
740         # this does not work unfortunately.
741         &WARN("ircCheck: ident($ident) != param{ircNick}($param{ircNick}).");
742
743         # this check is misleading... perhaps we should do a notify.
744         if (! &IsNickInAnyChan( $param{ircNick} ) ) {
745             &DEBUG("$param{ircNick} not in use... changing!");
746             &nick( $param{ircNick} );
747         } else {
748             &WARN("$param{ircNick} is still in use...");
749         }
750     }
751
752     if (grep /^\s*$/, keys %channels) {
753         &WARN("ircCheck: we have a NULL chan in hash channels? removing!");
754         if (!exists $channels{''}) {
755             &DEBUG("ircCheck: this should never happen!");
756         }
757
758         delete $channels{''};
759     }
760
761     $cache{statusSafe} = 0;
762
763     ### USER FILE.
764     if ($utime_userfile > $wtime_userfile and time() - $wtime_userfile > 3600) {
765         &writeUserFile();
766         $wtime_userfile = time();
767     }
768     ### CHAN FILE.
769     if ($utime_chanfile > $wtime_chanfile and time() - $wtime_chanfile > 3600) {
770         &writeChanFile();
771         $wtime_chanfile = time();
772     }
773 }
774
775 sub miscCheck {
776     if (@_) {
777         &ScheduleThis(120, "miscCheck");
778         return if ($_[0] eq "2");       # defer.
779     }
780
781     # SHM check.
782     my @ipcs;
783     if ( -x "/usr/bin/ipcs") {
784         @ipcs = `/usr/bin/ipcs`;
785     } else {
786         &WARN("ircCheck: no 'ipcs' binary.");
787         return;
788     }
789
790     # shmid stale remove.
791     foreach (@ipcs) {
792         chop;
793
794         # key, shmid, owner, perms, bytes, nattch
795         next unless (/^(0x\d+) (\d+)\s+(\S+)\s+(\d+)\s+(\d+)\s+/);
796
797         my ($shmid, $size) = ($2,$5);
798         next unless ($shmid != $shm and $size == 2000);
799         my $z   = &shmRead($shmid);
800         if ($z =~ /^(\d+): /) {
801             my $time    = $1;
802             next if (time() - $time < 60*60);
803
804         } else {
805 #           &DEBUG("shm: $shmid is not ours or old blootbot => ($z)");
806 #           next;
807         }
808
809         &status("SHM: nuking shmid $shmid");
810         CORE::system("/usr/bin/ipcrm shm $shmid >/dev/null");
811     }
812
813     # make backup of important files.
814     &mkBackup( $bot_state_dir."/blootbot.chan", 60*60*24*3);
815     &mkBackup( $bot_state_dir."/blootbot.users", 60*60*24*3);
816     &mkBackup( $bot_base_dir."/blootbot-news.txt", 60*60*24*1);
817
818     # flush cache{lobotomy}
819     foreach (keys %{ $cache{lobotomy} }) {
820         next unless (time() - $cache{lobotomy}{$_} > 60*60);
821         delete $cache{lobotomy}{$_};
822     }
823
824     ### check modules if they've been modified. might be evil.
825     &reloadAllModules();
826 }
827
828 sub miscCheck2 {
829     if (@_) {
830         &ScheduleThis(240, "miscCheck2");
831         return if ($_[0] eq "2");       # defer.
832     }
833
834     # debian check.
835     opendir(DEBIAN, "$bot_state_dir/debian");
836     foreach ( grep /gz$/, readdir(DEBIAN) ) {
837         my $exit = CORE::system("gzip -t $bot_state_dir/debian/$_");
838         next unless ($exit);
839
840         &status("debian: unlinking file => $_");
841         unlink "$bot_state_dir/debian/$_";
842     }
843     closedir DEBIAN;
844
845     # compress logs that should have been compressed.
846     # todo: use strftime?
847     my ($day,$month,$year) = (gmtime(time()))[3,4,5];
848     my $date = sprintf("%04d%02d%02d",$year+1900,$month+1,$day);
849
850     if (!opendir(DIR,"$bot_log_dir")) {
851         &ERROR("misccheck2: log dir $bot_log_dir does not exist.");
852         closedir DIR;
853         return -1;
854     }
855
856     while (my $f = readdir(DIR)) {
857         next unless ( -f "$bot_log_dir/$f");
858         next if ($f =~ /gz|bz2/);
859         next unless ($f =~ /(\d{8})/);
860         next if ($date eq $1);
861
862         &compress("$bot_log_dir/$f");
863     }
864     closedir DIR;
865 }
866
867 ### this is semi-scheduled
868 sub getNickInUse {
869     if ($ident eq $param{'ircNick'}) {
870         &status("okay, got my nick back.");
871         return;
872     }
873
874     if (@_) {
875         &ScheduleThis(30, "getNickInUse");
876         return if ($_[0] eq "2");       # defer.
877     }
878
879     &nick( $param{'ircNick'} );
880 }
881
882 sub uptimeLoop {
883     return if (!defined &uptimeWriteFile);
884 #    return unless &IsChanConf("uptime");
885
886     if (@_) {
887         &ScheduleThis(60, "uptimeLoop");
888         return if ($_[0] eq "2");       # defer.
889     }
890
891     &uptimeWriteFile();
892 }
893
894 sub slashdotLoop {
895
896     if (@_) {
897         &ScheduleThis(60, "slashdotLoop");
898         return if ($_[0] eq "2");
899     }
900
901     my @chans = &ChanConfList("slashdotAnnounce");
902     return unless (scalar @chans);
903
904     &Forker("slashdot", sub {
905         my $line = &Slashdot::slashdotAnnounce();
906         return unless (defined $line);
907
908         foreach (@chans) {
909             next unless (&::validChan($_));
910
911             &::status("sending slashdot update to $_.");
912             &notice($_, "Slashdot: $line");
913         }
914     } );
915 }
916
917 sub plugLoop {
918
919     if (@_) {
920         &ScheduleThis(60, "plugLoop");
921         return if ($_[0] eq "2");
922     }
923
924     my @chans = &ChanConfList("plugAnnounce");
925     return unless (scalar @chans);
926
927     &Forker("plug", sub {
928         my $line = &Plug::plugAnnounce();
929         return unless (defined $line);
930
931         foreach (@chans) {
932             next unless (&::validChan($_));
933
934             &::status("sending plug update to $_.");
935             &notice($_, "Plug: $line");
936         }
937     } );
938 }
939
940 sub freshmeatLoop {
941     if (@_) {
942         &ScheduleThis(60, "freshmeatLoop");
943         return if ($_[0] eq "2");
944     }
945
946     my @chans = &ChanConfList("freshmeatAnnounce");
947     return unless (scalar @chans);
948
949     &Forker("freshmeat", sub {
950         my $data = &Freshmeat::freshmeatAnnounce();
951
952         foreach (@chans) {
953             next unless (&::validChan($_));
954
955             &::status("sending freshmeat update to $_.");
956             &msg($_, $data);
957         }
958     } );
959 }
960
961 sub kernelLoop {
962     if (@_) {
963         &ScheduleThis(240, "kernelLoop");
964         return if ($_[0] eq "2");
965     }
966
967     my @chans = &ChanConfList("kernelAnnounce");
968     return unless (scalar @chans);
969
970     &Forker("kernel", sub {
971         my @data = &Kernel::kernelAnnounce();
972
973         foreach (@chans) {
974             next unless (&::validChan($_));
975
976             &::status("sending kernel update to $_.");
977             my $c = $_;
978             foreach (@data) {
979                 &notice($c, "Kernel: $_");
980             }
981         }
982     } );
983 }
984
985 sub wingateCheck {
986     return unless &IsChanConf("wingate");
987
988     ### FILE CACHE OF OFFENDING WINGATES.
989     foreach (grep /^$host$/, @wingateBad) {
990         &status("Wingate: RUNNING ON $host BY $who");
991         &ban("*!*\@$host", "") if &IsChanConf("wingateBan");
992
993         my $reason      = &getChanConf("wingateKick");
994
995         next unless ($reason);
996         &kick($who, "", $reason)
997     }
998
999     ### RUN CACHE OF TRIED WINGATES.
1000     if (grep /^$host$/, @wingateCache) {
1001         push(@wingateNow, $host);       # per run.
1002         push(@wingateCache, $host);     # cache per run.
1003     } else {
1004         &DEBUG("Already scanned $host. good.");
1005     }
1006
1007     my $interval = &getChanConfDefault("wingateInterval", 60); # seconds.
1008     return if (defined $forked{'wingate'});
1009     return if (time() - $wingaterun <= $interval);
1010     return unless (scalar(keys %wingateToDo));
1011
1012     $wingaterun = time();
1013
1014     &Forker("wingate", sub { &Wingate::Wingates(keys %wingateToDo); } );
1015     undef @wingateNow;
1016 }
1017
1018 ### TODO.
1019 sub wingateWriteFile {
1020     if (@_) {
1021         &ScheduleThis(60, "wingateWriteFile");
1022         return if ($_[0] eq "2");       # defer.
1023     }
1024
1025     return unless (scalar @wingateCache);
1026
1027     my $file = "$bot_base_dir/$param{'ircUser'}.wingate";
1028     if ($bot_pid != $$) {
1029         &DEBUG("wingateWriteFile: Reorganising!");
1030
1031         open(IN, $file);
1032         while (<IN>) {
1033             chop;
1034             push(@wingateNow, $_);
1035         }
1036         close IN;
1037
1038         # very lame hack.
1039         my %hash = map { $_ => 1 } @wingateNow;
1040         @wingateNow = sort keys %hash;
1041     }
1042
1043     &DEBUG("wingateWF: writing...");
1044     open(OUT, ">$file");
1045     foreach (@wingateNow) {
1046         print OUT "$_\n";
1047     }
1048     close OUT;
1049 }
1050
1051 sub factoidCheck {
1052     if (@_) {
1053         &ScheduleThis(720, "factoidCheck");
1054         return if ($_[0] eq "2");       # defer.
1055     }
1056
1057     my @list    = &searchTable("factoids", "factoid_key", "factoid_key", " #DEL#");
1058     my $stale   = &getChanConfDefault("factoidDeleteDelay", 14) *60*60*24;
1059     if ($stale < 1) {
1060         # disable it since it's "illegal".
1061         return;
1062     }
1063
1064     my $time    = time();
1065
1066     foreach (@list) {
1067         my $age = &getFactInfo($_, "modified_time");    
1068
1069         if (!defined $age or $age !~ /^\d+$/) {
1070             if (scalar @list > 50) {
1071                 if (!$cache{warnDel}) {
1072                     &WARN("list is over 50 (".scalar(@list)."... giving it a miss.");
1073                     $cache{warnDel} = 1;
1074                     last;
1075                 }
1076             }
1077
1078             &WARN("del factoid: old cruft (no time): $_");
1079             &delFactoid($_);
1080             next;
1081         }
1082
1083         next unless ($time - $age > $stale);
1084
1085         my $fix = $_;
1086         $fix =~ s/ #DEL#$//g;
1087         my $agestr = &Time2String($time - $age);
1088         &status("safedel: Removing '$_' for good. [$agestr old]");
1089
1090         &delFactoid($_);
1091     }
1092 }
1093
1094 sub dccStatus {
1095     return unless (scalar keys %{ $dcc{CHAT} });
1096
1097     if (@_) {
1098         &ScheduleThis(10, "dccStatus");
1099         return if ($_[0] eq "2");       # defer.
1100     }
1101
1102     my $time = strftime("%H:%M", gmtime(time()) );
1103
1104     my $c;
1105     foreach (keys %channels) {
1106         my $c           = $_;
1107         my $users       = keys %{ $channels{$c}{''} };
1108         my $chops       = keys %{ $channels{$c}{o}  };
1109         my $bans        = keys %{ $channels{$c}{b}  };
1110
1111         my $txt = "[$time] $c: $users members ($chops chops), $bans bans";
1112         foreach (keys %{ $dcc{'CHAT'} }) {
1113             next unless (exists $channels{$c}{''}{lc $_});
1114             $conn->privmsg($dcc{'CHAT'}{$_}, $txt);
1115         }
1116     }
1117 }
1118
1119 sub scheduleList {
1120     ###
1121     # custom:
1122     #   a - time == now.
1123     #   b - weird time.
1124     ###
1125
1126     &DEBUG("sched:");
1127     foreach (keys %{ $irc->{_queue} }) {
1128         my $q = $_;
1129
1130         my $sched;
1131         foreach (keys %sched) {
1132             next unless ($q eq $sched{$_});
1133             $sched = $_;
1134             last;
1135         }
1136
1137         my $time = $irc->{_queue}->{$q}->[0] - time();
1138
1139         if (defined $sched) {
1140             &DEBUG("   $sched($q): ".&Time2String($time) );
1141         } else {
1142             &DEBUG("   NULL($q): ".&Time2String($time) );
1143         }
1144     }
1145
1146     &DEBUG("end of sList.");
1147 }
1148
1149 sub mkBackup {
1150     my($file, $time)    = @_;
1151     my $backup          = 0;
1152
1153     if (! -f $file) {
1154         &VERB("mkB: file '$file' does not exist.",2);
1155         return;
1156     }
1157
1158     my $age     = "New";
1159     if ( -e "$file~" ) {
1160         $backup++       if ((stat $file)[9] - (stat "$file~")[9] > $time);
1161         my $delta       = time() - (stat "$file~")[9];
1162         $age            = &Time2String($delta);
1163     } else {
1164         $backup++;
1165     }
1166
1167     return unless ($backup);
1168
1169     ### TODO: do internal copying.
1170     &status("Backup: $file ($age)");
1171     CORE::system("/bin/cp $file $file~");
1172 }
1173
1174 1;