]> git.donarmstrong.com Git - infobot.git/blob - src/IRC/Schedulers.pl
ircCheck now checks @joinchan for chans left to join, but should never happen.
[infobot.git] / src / IRC / Schedulers.pl
1 #
2 # ProcessExtra.pl: Extensions to Process.pl
3 #          Author: dms
4 #         Version: v0.3 (20000707)
5 #         Created: 20000117
6 #
7
8 if (&IsParam("useStrict")) { use strict; }
9
10 sub setupSchedulers {
11     &VERB("Starting schedulers...",2);
12
13     # ONCE OFF.
14
15     # REPETITIVE.
16     &uptimeCycle(1)     if (&IsParam("uptime"));
17     &randomQuote(1)     if (&IsParam("randomQuote"));
18     &randomFactoid(1)   if (&IsParam("randomFactoid"));
19     &logCycle(1)        if ($loggingstatus and &IsParam("logFile") and &IsParam("maxLogSize"));
20     &limitCheck(1)      if (&IsParam("limitcheck"));
21     &netsplitCheck(1);  # mandatory
22     &floodCycle(1);     # mandatory
23     &seenFlush(1)       if (&IsParam("seen") and &IsParam("seenFlushInterval"));
24     &leakCheck(1);      # mandatory
25     &ignoreListCheck(1);# mandatory
26     &seenFlushOld(1)    if (&IsParam("seen"));
27     &ircCheck(1);       # mandatory
28     &shmFlush(1);       # mandatory
29     &slashdotCycle(1)   if (&IsParam("slashdot") and &IsParam("slashdotAnnounce"));
30     &freshmeatCycle(1)  if (&IsParam("freshmeat") and &IsParam("freshmeatAnnounce"));
31     &kernelCycle(1)     if (&IsParam("kernel") and &IsParam("kernelAnnounce"));
32     &wingateWriteFile(1) if (&IsParam("wingate"));
33 }
34
35 sub ScheduleThis {
36     my ($interval, $codename, @args) = @_;
37     my $waittime = &getRandomInt($interval);
38
39     &VERB("Scheduling \&$codename() for ".&Time2String($waittime),3);
40     $conn->schedule($waittime, \&$codename, @args);
41 }
42
43 sub randomQuote {
44     my $line = &getRandomLineFromFile($bot_misc_dir. "/blootbot.randtext");
45     if (!defined $line) {
46         &ERROR("random Quote: weird error?");
47         return;
48     }
49
50     my @channels = split(/[\s\t]+/, lc $param{'randomQuoteChannels'});
51     @channels    = keys(%channels) unless (scalar @channels);
52
53     my $good = 0;
54     foreach (@channels) {
55         next unless (&validChan($_));
56
57         &status("sending random Quote to $_.");
58         &action($_, "Ponders: ".$line);
59         $good++;
60     }
61
62     if (!$good) {
63         &WARN("randomQuote: no valid channels?");
64         return;
65     }
66
67     my $interval = $param{'randomQuoteInterval'} || 60;
68     &ScheduleThis($interval, "randomQuote") if (@_);
69 }
70
71 sub randomFactoid {
72     my ($key,$val);
73     my $error = 0;
74     while (1) {
75         ($key,$val) = &randKey("factoids","factoid_key,factoid_value");
76 ###     $val =~ tr/^[A-Z]/[a-z]/;       # blah is Good => blah is good.
77         last if ($val !~ /^</);
78         $error++;
79         if ($error == 5) {
80             &ERROR("rF: tried 5 times but failed.");
81             return;
82         }
83     }
84
85     my @channels = split(/[\s\t]+/, lc $param{'randomFactoidChannels'});
86     @channels    = keys(%channels) unless (scalar @channels);
87
88     my $good = 0;
89     foreach (@channels) {
90         next unless (&validChan($_));
91
92         &status("sending random Factoid to $_.");
93 ###     &msg($_, "$key is $val");
94         &action($_, "Thinks: \037$key\037 is $val");
95         ### FIXME: Use &getReply() on above to format factoid properly?
96         $good++;
97     }
98
99     if (!$good) {
100         &WARN("randomFactoid: no valid channels?");
101         return;
102     }
103
104     my $interval = $param{'randomFactoidInterval'} || 60;
105     &ScheduleThis($interval, "randomFactoid") if (@_);
106 }
107
108 sub logCycle {
109     # check if current size is too large.
110     if ( -s $file{log} > $param{'maxLogSize'}) {
111         my $date = sprintf("%04d%02d%02d", (localtime)[5,4,3]);
112         $file{log} = $param{'logfile'} ."-". $date;
113         &status("cycling log file.");
114
115         if ( -e $file{log}) {
116             my $i = 1;
117             my $newlog;
118             while () {
119                 $newlog = $file{log}."-".$i;
120                 last if (! -e $newlog);
121                 $i++;
122             }
123             $file{log} = $newlog;
124         }
125
126         &closeLog();
127         system("/bin/mv '$param{'logfile'}' '$file{log}'");
128         &compress($file{log});
129         &openLog();
130         &status("cycling log file.");
131     }
132
133     # check if all the logs exceed size.
134     my $logdir = "$bot_base_dir/log/";
135     if (opendir(LOGS, $logdir)) {
136         my $tsize = 0;
137         my (%age, %size);
138
139         while (defined($_ = readdir LOGS)) {
140             my $logfile = "$logdir/$_";
141
142             next unless ( -f $logfile);
143             my $size = -s $logfile;
144             my $age = (stat $logfile)[9]; ### or 8 ?
145
146             $age{$age}          = $logfile;
147             $size{$logfile}     = $size;
148
149             $tsize              += $size;
150         }
151         closedir LOGS;
152
153         my $delete = 0;
154         while ($tsize > $param{'maxLogSize'}) {
155             &status("LOG: current size > max ($tsize > $param{'maxLogSize'})");
156             my $oldest = (sort {$a <=> $b} keys %age)[0];
157             &status("LOG: unlinking $age{$oldest}.");
158             ### NOT YET.
159             # unlink $age{$oldest};
160             $tsize -= $oldest;
161             $delete++;
162         }
163
164         ### TODO: add how many b,kb,mb removed?
165         if ($delete) {
166             &status("LOG: removed $delete logs.");
167         }
168     } else {
169         &WARN("could not open dir $logdir");
170     }
171
172     &ScheduleThis(60, "logCycle") if (@_);
173 }
174
175 sub seenFlushOld {
176     my $max_time = $param{'seenMaxDays'}*60*60*24;
177     my $delete   = 0;
178
179     if ($param{'DBType'} =~ /^pg|postgres|mysql/i) {
180         my $query = "SELECT nick,time FROM seen GROUP BY nick HAVING UNIX_TIMESTAMP() - time > $max_time";
181         my $sth = $dbh->prepare($query);
182         $sth->execute;
183
184         while (my @row = $sth->fetchrow_array) {
185             my ($nick,$time) = @row;
186
187             &dbDel("seen","nick",$nick);
188             $delete++;
189         }
190         $sth->finish;
191     } elsif ($param{'DBType'} =~ /^dbm/i) {
192         my $time = time();
193
194         foreach (keys %seen) {
195             my $delta_time = $time - &dbGet("seen", "NULL", $_, "time");
196             next unless ($delta_time > $max_time);
197
198             &DEBUG("seenFlushOld: ".&Time2String($delta_time) );
199             delete $seen{$_};
200             $delete++;
201         }
202     } else {
203         &FIXME("seenFlushOld: for PG/NO-DB.");
204     }
205     &VERB("SEEN deleted $delete seen entries.",2);
206
207     &ScheduleThis(1440, "seenFlushOld") if (@_);
208 }
209
210 sub limitCheck {
211     my $limitplus = $param{'limitcheckPlus'} || 5;
212
213     if (scalar keys %netsplit) {
214         &status("limitcheck: netsplit active.");
215         return;
216     }
217
218     my @channels = split(/[\s\t]+/, lc $param{'limitcheck'});
219
220     foreach (@channels) {
221         next unless (&validChan($_));
222
223         if (!exists $channels{$_}{'o'}{$ident}) {
224             &ERROR("limitcheck: dont have ops on $_.");
225             next;
226         }
227
228         my $newlimit = scalar(keys %{$channels{$_}{''}}) + $limitplus;
229         my $limit = $channels{$_}{'l'};
230
231         next unless (!defined $limit or $limit != $newlimit);
232
233         &rawout("MODE $_ +l $newlimit");
234     }
235
236     my $interval = $param{'limitcheckInterval'} || 10;
237     &ScheduleThis($interval, "limitCheck") if (@_);
238 }
239
240 sub netsplitCheck {
241     my ($s1,$s2);
242
243     foreach $s1 (keys %netsplitservers) {
244         foreach $s2 (keys %{$netsplitservers{$s1}}) {
245             if (time() - $netsplitservers{$s1}{$s2} > 3600) {
246                 &status("netsplit between $s1 and $s2 appears to be stale.");
247                 delete $netsplitservers{$s1}{$s2};
248             }
249         }
250     }
251
252     # %netsplit hash checker.
253     foreach (keys %netsplit) {
254         if (&IsNickInAnyChan($_)) {
255             &DEBUG("netsplitC: $_ is in some chan; removing from netsplit list.");
256             delete $netsplit{$_};
257         }
258         next unless (time() - $netsplit{$_} > 60*60*2); # 2 hours.
259
260         if (!&IsNickInAnyChan($_)) {
261             &DEBUG("netsplitC: $_ didn't come back from netsplit in 2 hours; removing from netsplit list.");
262             delete $netsplit{$_};
263         }
264     }
265
266     &ScheduleThis(30, "netsplitCheck") if (@_);
267 }
268
269 sub floodCycle {
270     my $interval = $param{'floodInterval'} || 60;       # seconds.
271     my $delete = 0;
272
273     my $who;
274     foreach $who (keys %flood) {
275         foreach (keys %{$flood{$who}}) {
276             if (time() - $flood{$who}{$_} > $interval) {
277                 delete $flood{$who}{$_};
278                 $delete++;
279             }
280         }
281     }
282     &VERB("floodCycle: deleted $delete items.",2);
283
284     &ScheduleThis($interval, "floodCycle") if (@_);     # minutes.
285 }
286
287 sub seenFlush {
288     my $nick;
289     my $flushed = 0;
290
291     if ($param{'DBType'} =~ /^mysql|pg|postgres/i) {
292         foreach $nick (keys %seencache) {
293             my $exists = &dbGet("seen","nick", $nick, "nick");
294
295             if (defined $exists and $exists) {
296                 &dbUpdate("seen", "nick", $nick, (
297                         "time" => $seencache{$nick}{'time'},
298                         "host" => $seencache{$nick}{'host'},
299                         "channel" => $seencache{$nick}{'chan'},
300                         "message" => $seencache{$nick}{'msg'},
301                 ) );
302             } else {
303                 my $retval = &dbInsert("seen", $nick, (
304                         "nick" => $seencache{$nick}{'nick'},
305                         "time" => $seencache{$nick}{'time'},
306                         "host" => $seencache{$nick}{'host'},
307                         "channel" => $seencache{$nick}{'chan'},
308                         "message" => $seencache{$nick}{'msg'},
309                 ) );
310
311                 ### TODO: put bad nick into a list and don't do it again!
312                 if ($retval == 0) {
313                     &ERROR("Should never happen! (nick => $nick) FIXME");
314                 }
315             }
316
317             delete $seencache{$nick};
318             $flushed++;
319         }
320
321     } elsif ($param{'DBType'} =~ /^dbm/i) {
322
323         foreach $nick (keys %seencache) {
324             my $retval = &dbInsert("seen", $nick, (
325                 "nick" => $seencache{$nick}{'nick'},
326                 "time" => $seencache{$nick}{'time'},
327                 "host" => $seencache{$nick}{'host'},
328                 "channel" => $seencache{$nick}{'chan'},
329                 "message" => $seencache{$nick}{'msg'},
330             ) );
331
332             ### TODO: put bad nick into a list and don't do it again!
333             if ($retval == 0) {
334                 &ERROR("Should never happen! (nick => $nick) FIXME");
335             }
336
337             delete $seencache{$nick};
338             $flushed++;
339         }
340     } else {
341         &DEBUG("seenFlush: NO VALID FACTOID SUPPORT?");
342     }
343
344     &VERB("Flushed $flushed seen entries.",2);
345
346     my $interval = $param{'seenFlushInterval'} || 60;
347     &ScheduleThis($interval, "seenFlush") if (@_);
348 }
349
350 sub leakCheck {
351     my ($blah1,$blah2);
352     my $count = 0;
353
354     # flood.
355     foreach $blah1 (keys %flood) {
356         foreach $blah2 (keys %{$flood{$blah1}}) {
357             $count += scalar(keys %{$flood{$blah1}{$blah2}});
358         }
359     }
360     &VERB("\%flood has $count total keys.",2);
361
362     my $chan;
363     foreach $chan (grep /[A-Z]/, keys %channels) {
364         &DEBUG("leak: chan => '$chan'.");
365         my ($i,$j);
366         foreach $i (keys %{$channels{$chan}}) {
367             foreach (keys %{$channels{$chan}{$i}}) {
368                 &DEBUG("leak:   \$channels{$chan}{$i}{$_} ...");
369             }
370         }
371     }
372
373     &ScheduleThis(60, "leakCheck") if (@_);
374 }
375
376 sub ignoreListCheck {
377     my $time = time();
378     my $count = 0;
379
380     foreach (keys %ignoreList) {
381         next if ($ignoreList{$_} == 1);
382         next unless ($time > $ignoreList{$_});
383
384         delete $ignoreList{$_};
385         &status("ignore: $_ has expired.");
386         $count++;
387     }
388     &VERB("ignore: $count items deleted.",2);
389
390     &ScheduleThis(30, "ignoreListCheck") if (@_);
391 }
392
393 sub ircCheck {
394     my @array = split /[\t\s]+/, $param{'join_channels'};
395     my $iconf = scalar(@array);
396     my $inow  = scalar(keys %channels);
397     if ($iconf > 2 and $inow * 2 <= $iconf) {
398         &FIXME("ircCheck: current channels * 2 <= config channels. FIXME.");
399     }
400
401     my @ipcs;
402     if ( -x "/usr/bin/ipcs") {
403         @ipcs = `/usr/bin/ipcs`;
404     } else {
405         &WARN("ircCheck: no 'ipcs' binary.");
406     }
407
408     # shmid stale remove.
409     foreach (@ipcs) {
410         chop;
411
412         # key, shmid, owner, perms, bytes, nattch
413         next unless (/^(0x\d+) (\d+)\s+(\S+)\s+(\d+)\s+(\d+)\s+/);
414
415         my ($shmid, $size) = ($2,$5);
416         next unless ($shmid != $shm and $size == 2000);
417
418         &status("SHM: nuking shmid $shmid");
419         system("/usr/bin/ipcrm shm $shmid >/dev/null");
420     }
421
422     if (!$conn->connected and time - $msgtime > 3600) {
423         &WARN("ircCheck: no msg for 3600 and disco'd! reconnecting!");
424         $msgtime = time();      # just in case.
425         &ircloop();
426     }
427
428     if ($ident !~ /^\Q$param{ircNick}\E$/) {
429         &WARN("ircCheck: ident($ident) != param{ircNick}($param{IrcNick}).");
430     }
431
432     if (scalar @joinchan) {
433         &WARN("We have some channels to join, doing so.");
434         &joinNextChan();
435     }
436
437     &ScheduleThis(240, "ircCheck") if (@_);
438 }
439
440 sub shmFlush {
441     my $shmmsg = &shmRead($shm);
442     $shmmsg =~ s/\0//g;         # remove padded \0's.
443
444     foreach (split '\|\|', $shmmsg) {
445         &status("shm: Processing '$_'.");
446
447         if (/^DCC SEND (\S+) (\S+)$/) {
448             my ($nick,$file) = ($1,$2);
449             if (exists $dcc{'SEND'}{$who}) {
450                 &msg($nick,"DCC already active.");
451             } else {
452                 &DEBUG("shm: dcc sending $2 to $1.");
453                 $conn->new_send($1,$2);
454                 $dcc{'SEND'}{$who} = time();
455             }
456         } elsif (/^DELETE FORK (\S+)$/) {
457             delete $forked{$1};
458         } elsif (/^EVAL (.*)$/) {
459             &DEBUG("evaling '$1'.");
460             eval $1;
461         } else {
462             &DEBUG("shm: unknown msg. ($_)");
463         }
464     }
465
466     &shmWrite($shm,"") if ($shmmsg ne "");
467
468     &ScheduleThis(5, "shmFlush") if (@_);
469 }
470
471 sub getNickInUse {
472     if ($ident eq $param{'ircNick'}) {
473         &status("okay, got my nick back.");
474         return;
475     }
476
477     &status("Trying to get my nick back.");
478     &nick($param{'ircNick'});
479
480     &ScheduleThis(5, "getNickInUse") if (@_);
481 }
482
483 sub uptimeCycle {
484     &uptimeWriteFile();
485
486     &ScheduleThis(60, "uptimeCycle") if (@_);
487 }
488
489 sub slashdotCycle {
490     &Forker("slashdot", sub { &Slashdot::slashdotAnnounce(); } );
491
492     &ScheduleThis(60, "slashdotCycle") if (@_);
493 }
494
495 sub freshmeatCycle {
496     &Forker("freshmeat", sub { &Freshmeat::freshmeatAnnounce(); } );
497
498     &ScheduleThis(60, "freshmeatCycle") if (@_);
499 }
500
501 sub kernelCycle {
502     &Forker("kernel", sub { &Kernel::kernelAnnounce(); } );
503
504     &ScheduleThis(240, "kernelCycle") if (@_);
505 }
506
507 sub wingateCheck {
508     return unless &IsParam("wingate");
509     return unless ($param{'wingate'} =~ /^(.*\s+)?$chan(\s+.*)?$/i);
510
511     ### FILE CACHE OF OFFENDING WINGATES.
512     foreach (grep /^$host$/, @wingateBad) {
513         &status("Wingate: RUNNING ON $host BY $who");
514         &ban("*!*\@$host", "") if &IsParam("wingateBan");
515
516         next unless (&IsParam("wingateKick"));
517         &kick($who, "", $param{'wingateKick'})
518     }
519
520     ### RUN CACHE OF TRIED WINGATES.
521     if (grep /^$host$/, @wingateCache) {
522         push(@wingateNow, $host);       # per run.
523         push(@wingateCache, $host);     # cache per run.
524     } else {
525         &DEBUG("Already scanned $host. good.");
526     }
527
528     my $interval = $param{'wingateInterval'} || 60;     # seconds.
529     return if (defined $forked{'wingate'});
530     return if (time() - $wingaterun <= $interval);
531     return unless (scalar(keys %wingateToDo));
532
533     $wingaterun = time();
534
535     &Forker("wingate", sub { &Wingate::Wingates(keys %wingateToDo); } );
536     undef @wingateNow;
537 }
538
539 ### TODO.
540 sub wingateWriteFile {
541     return unless (scalar @wingateCache);
542
543     my $file = "$bot_base_dir/$param{'ircUser'}.wingate";
544     if ($bot_pid != $$) {
545         &DEBUG("wingateWriteFile: Reorganising!");
546
547         open(IN, $file);
548         while (<IN>) {
549             chop;
550             push(@wingateNow, $_);
551         }
552         close IN;
553
554         # very lame hack.
555         my %hash = map { $_ => 1 } @wingateNow;
556         @wingateNow = sort keys %hash;
557     }
558
559     &DEBUG("wingateWF: writing...");
560     open(OUT, ">$file");
561     foreach (@wingateNow) {
562         print OUT "$_\n";
563     }
564     close OUT;
565
566     &ScheduleThis(60, "wingateWriteFile") if (@_);
567 }
568
569 1;