]> icculus.org git repositories - divverent/nexuiz.git/blob - Docs/server/rcon2irc/rcon2irc.pl
make the quotes have the right color
[divverent/nexuiz.git] / Docs / server / rcon2irc / rcon2irc.pl
1 #!/usr/bin/perl
2
3 our $VERSION = '0.4.2 svn $Revision$';
4
5 # Copyright (c) 2008 Rudolf "divVerent" Polzer
6
7 # Permission is hereby granted, free of charge, to any person
8 # obtaining a copy of this software and associated documentation
9 # files (the "Software"), to deal in the Software without
10 # restriction, including without limitation the rights to use,
11 # copy, modify, merge, publish, distribute, sublicense, and/or sell
12 # copies of the Software, and to permit persons to whom the
13 # Software is furnished to do so, subject to the following
14 # conditions:
15
16 # The above copyright notice and this permission notice shall be
17 # included in all copies or substantial portions of the Software.
18
19 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 # OTHER DEALINGS IN THE SOFTWARE.
27
28 # Interfaces:
29 #   Connection:
30 #     $conn->sockname() returns a connection type specific representation
31 #       string of the local address, or undef if not applicable.
32 #     $conn->send("string") sends something over the connection.
33 #     $conn->recv() receives a string from the connection, or returns "" if no
34 #       data is available.
35 #     $conn->fds() returns all file descriptors used by the connection, so one
36 #       can use select() on them.
37 #   Channel:
38 #     Usually wraps around a connection and implements a command based
39 #     structure over it. It usually is constructed using new
40 #     ChannelType($connection, someparameters...)
41 #     @cmds = $chan->join_commands(@cmds) joins multiple commands to a single
42 #       command string if the protocol supports it, or does nothing and leaves
43 #       @cmds unchanged if the protocol does not support that usage (this is
44 #       meant to save send() invocations).
45 #     $chan->send($command, $nothrottle) sends a command over the channel. If
46 #       $nothrottle is sent, the command must not be left out even if the channel
47 #       is saturated (for example, because of IRC's flood control mechanism).
48 #     $chan->quote($str) returns a string in a quoted form so it can safely be
49 #       inserted as a substring into a command, or returns $str as is if not
50 #       applicable. It is assumed that the result of the quote method is used
51 #       as part of a quoted string, if the protocol supports that.
52 #     $chan->recv() returns a list of received commands from the channel, or
53 #       the empty list if none are available.
54 #     $conn->fds() returns all file descriptors used by the channel's
55 #       connections, so one can use select() on them.
56
57
58
59
60
61
62
63 # Socket connection.
64 # Represents a connection over a socket.
65 # Mainly used to wrap a channel around it for, in this case, line based or rcon-like operation.
66 package Connection::Socket;
67 use strict;
68 use warnings;
69 use IO::Socket::INET;
70 use IO::Handle;
71
72 # Constructor:
73 #   my $conn = new Connection::Socket(tcp => "localaddress" => "remoteaddress" => 6667);
74 # If the remote address does not contain a port number, the numeric port is
75 # used (it serves as a default port).
76 sub new($$)
77 {
78         my ($class, $proto, $local, $remote, $defaultport) = @_;
79         my $sock = IO::Socket::INET->new(
80                 Proto => $proto,
81                 (length($local) ? (LocalAddr => $local) : ()),
82                 PeerAddr => $remote,
83                 PeerPort => $defaultport
84         ) or die "socket $proto/$local/$remote: $!";
85         $sock->blocking(0);
86         my $you = {
87                 # Mortal fool! Release me from this wretched tomb! I must be set free
88                 # or I will haunt you forever! I will hide your keys beneath the
89                 # cushions of your upholstered furniture... and NEVERMORE will you be
90                 # able to find socks that match!
91                 sock => $sock,
92                 # My demonic powers have made me OMNIPOTENT! Bwahahahahahahaha!
93         };
94         return
95                 bless $you, 'Connection::Socket';
96 }
97
98 # $sock->sockname() returns the local address of the socket.
99 sub sockname($)
100 {
101         my ($self) = @_;
102         my ($port, $addr) = sockaddr_in $self->{sock}->sockname();
103         return "@{[inet_ntoa $addr]}:$port";
104 }
105
106 # $sock->send($data) sends some data over the socket; on success, 1 is returned.
107 sub send($$)
108 {
109         my ($self, $data) = @_;
110         return 1
111                 if not length $data;
112         if(not eval { $self->{sock}->send($data); })
113         {
114                 warn "$@";
115                 return 0;
116         }
117         return 1;
118 }
119
120 # $sock->recv() receives as much as possible from the socket (or at most 32k). Returns "" if no data is available.
121 sub recv($)
122 {
123         my ($self) = @_;
124         my $data = "";
125         $self->{sock}->recv($data, 32768, 0);
126         return $data;
127 }
128
129 # $sock->fds() returns the socket file descriptor.
130 sub fds($)
131 {
132         my ($self) = @_;
133         return fileno $self->{sock};
134 }
135
136
137
138
139
140
141
142 # Line-based buffered connectionless FIFO channel.
143 # Whatever is sent to it using send() is echoed back when using recv().
144 package Channel::FIFO;
145 use strict;
146 use warnings;
147
148 # Constructor:
149 #   my $chan = new Channel::FIFO();
150 sub new($)
151 {
152         my ($class) = @_;
153         my $you = {
154                 buffer => []
155         };
156         return
157                 bless $you, 'Channel::FIFO';
158 }
159
160 sub join_commands($@)
161 {
162         my ($self, @data) = @_;
163         return @data;
164 }
165
166 sub send($$$)
167 {
168         my ($self, $line, $nothrottle) = @_;
169         push @{$self->{buffer}}, $line;
170 }
171
172 sub quote($$)
173 {
174         my ($self, $data) = @_;
175         return $data;
176 }
177
178 sub recv($)
179 {
180         my ($self) = @_;
181         my $r = $self->{buffer};
182         $self->{buffer} = [];
183         return @$r;
184 }
185
186 sub fds($)
187 {
188         my ($self) = @_;
189         return ();
190 }
191
192
193
194
195
196
197
198 # QW rcon protocol channel.
199 # Wraps around a UDP based Connection and sends commands as rcon commands as
200 # well as receives rcon replies. The quote and join_commands methods are using
201 # DarkPlaces engine specific rcon protocol extensions.
202 package Channel::QW;
203 use strict;
204 use warnings;
205
206 # Constructor:
207 #   my $chan = new Channel::QW($connection, "password");
208 sub new($$)
209 {
210         my ($class, $conn, $password) = @_;
211         my $you = {
212                 connector => $conn,
213                 password => $password,
214                 recvbuf => "",
215         };
216         return
217                 bless $you, 'Channel::QW';
218 }
219
220 # Note: multiple commands in one rcon packet is a DarkPlaces extension.
221 sub join_commands($@)
222 {
223         my ($self, @data) = @_;
224         return join "\0", @data;
225 }
226
227 sub send($$$)
228 {
229         my ($self, $line, $nothrottle) = @_;
230         return $self->{connector}->send("\377\377\377\377rcon $self->{password} $line");
231 }
232
233 # Note: backslash and quotation mark escaping is a DarkPlaces extension.
234 sub quote($$)
235 {
236         my ($self, $data) = @_;
237         $data =~ s/[\000-\037]//g;
238         $data =~ s/([\\"])/\\$1/g;
239         $data =~ s/\$/\$\$/g;
240         return $data;
241 }
242
243 sub recv($)
244 {
245         my ($self) = @_;
246         for(;;)
247         {
248                 length(my $s = $self->{connector}->recv())
249                         or last;
250                 next
251                         if $s !~ /^\377\377\377\377n(.*)$/s;
252                 $self->{recvbuf} .= $1;
253         }
254         my @out = ();
255         while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
256         {
257                 push @out, $1;
258         }
259         return @out;
260 }
261
262 sub fds($)
263 {
264         my ($self) = @_;
265         return $self->{connector}->fds();
266 }
267
268
269
270
271
272
273
274 # Line based protocol channel.
275 # Wraps around a TCP based Connection and sends commands as text lines
276 # (separated by CRLF). When reading responses from the Connection, any type of
277 # line ending is accepted.
278 # A flood control mechanism is implemented.
279 package Channel::Line;
280 use strict;
281 use warnings;
282 use Time::HiRes qw/time/;
283
284 # Constructor:
285 #   my $chan = new Channel::Line($connection);
286 sub new($$)
287 {
288         my ($class, $conn) = @_;
289         my $you = {
290                 connector => $conn,
291                 recvbuf => "",
292                 capacity => undef,
293                 linepersec => undef,
294                 maxlines => undef,
295                 lastsend => time()
296         };
297         return 
298                 bless $you, 'Channel::Line';
299 }
300
301 sub join_commands($@)
302 {
303         my ($self, @data) = @_;
304         return @data;
305 }
306
307 # Sets new flood control parameters:
308 #   $chan->throttle(maximum lines per second, maximum burst length allowed to
309 #     exceed the lines per second limit);
310 #   RFC 1459 describes these parameters to be 0.5 and 5 for the IRC protocol.
311 #   If the $nothrottle flag is set while sending, the line is sent anyway even
312 #   if flooding would take place.
313 sub throttle($$$)
314 {
315         my ($self, $linepersec, $maxlines) = @_;
316         $self->{linepersec} = $linepersec;
317         $self->{maxlines} = $maxlines;
318         $self->{capacity} = $maxlines;
319 }
320
321 sub send($$$)
322 {
323         my ($self, $line, $nothrottle) = @_;
324         my $t = time();
325         if(defined $self->{capacity})
326         {
327                 $self->{capacity} += ($t - $self->{lastsend}) * $self->{linepersec};
328                 $self->{lastsend} = $t;
329                 $self->{capacity} = $self->{maxlines}
330                         if $self->{capacity} > $self->{maxlines};
331                 if(!$nothrottle)
332                 {
333                         return -1
334                                 if $self->{capacity} < 0;
335                 }
336                 $self->{capacity} -= 1;
337         }
338         $line =~ s/\r|\n//g;
339         return $self->{connector}->send("$line\r\n");
340 }
341
342 sub quote($$)
343 {
344         my ($self, $data) = @_;
345         $data =~ s/\r\n?/\n/g;
346         $data =~ s/\n/*/g;
347         return $data;
348 }
349
350 sub recv($)
351 {
352         my ($self) = @_;
353         for(;;)
354         {
355                 length(my $s = $self->{connector}->recv())
356                         or last;
357                 $self->{recvbuf} .= $s;
358         }
359         my @out = ();
360         while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
361         {
362                 push @out, $1;
363         }
364         return @out;
365 }
366
367 sub fds($)
368 {
369         my ($self) = @_;
370         return $self->{connector}->fds();
371 }
372
373
374
375
376
377
378 # main program... a gateway between IRC and DarkPlaces servers
379 package main;
380
381 use strict;
382 use warnings;
383 use IO::Select;
384 use Digest::MD5;
385 use Time::HiRes qw/time/;
386
387 our @handlers = (); # list of [channel, expression, sub to handle result]
388 our @tasks = (); # list of [time, sub]
389 our %channels = ();
390 our %store = (
391         irc_nick => "",
392         playernick_0 => "(console)",
393 );
394 our %config = (
395         irc_server => undef,
396         irc_nick => undef,
397         irc_user => undef,
398         irc_channel => undef,
399         irc_ping_delay => 120,
400
401         irc_nickserv_password => "",
402         irc_nickserv_identify => 'PRIVMSG NickServ :IDENTIFY %2$s',
403         irc_nickserv_ghost => 'PRIVMSG NickServ :GHOST %1$s %2$s',
404         irc_nickserv_ghost_attempts => 3,
405
406         irc_quakenet_authname => "",
407         irc_quakenet_password => "",
408         irc_quakenet_getchallenge => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGE',
409         irc_quakenet_challengeauth => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGEAUTH',
410         irc_quakenet_challengeprefix => ':Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :CHALLENGE',
411
412         dp_server => undef,
413         dp_listen => "", 
414         dp_password => undef,
415         dp_status_delay => 30,
416         dp_server_from_wan => "",
417
418         plugins => "",
419 );
420
421
422
423 # MISC STRING UTILITY ROUTINES to convert between DarkPlaces and IRC conventions
424
425 # convert mIRC color codes to DP color codes
426 our @color_irc2dp_table = (7, 0, 4, 2, 1, 1, 6, 1, 3, 2, 5, 5, 4, 6, 7, 7);
427 our @color_dp2irc_table = (14, 4, 9, 8, 12, 11, 13, 14, 15, 15); # not accurate, but legible
428 our @color_dp2ansi_table = ("m", "1;31m", "1;32m", "1;33m", "1;34m", "1;36m", "1;35m", "m", "1m", "1m"); # not accurate, but legible
429 our %color_team2dp_table = (5 => 1, 14 => 4, 13 => 3, 10 => 6);
430 our %color_team2irc_table = (5 => 4, 14 => 12, 13 => 8, 10 => 13);
431 sub color_irc2dp($)
432 {
433         my ($message) = @_;
434         $message =~ s/\^/^^/g;
435         my $color = 7;
436         $message =~ s{\003(\d\d?)(?:,(\d?\d?))?|(\017)}{
437                 # $1 is FG, $2 is BG, but let's ignore BG
438                 my $oldcolor = $color;
439                 if($3)
440                 {
441                         $color = 7;
442                 }
443                 else
444                 {
445                         $color = $color_irc2dp_table[$1];
446                         $color = $oldcolor if not defined $color;
447                 }
448                 ($color == $oldcolor) ? '' : '^' . $color;
449         }esg;
450         $message =~ s{[\000-\037]}{}gs; # kill bold etc. for now
451         return $message;
452 }
453
454 our @text_qfont_table = ( # ripped from DP console.c qfont_table
455     "\0", '#',  '#',  '#',  '#',  '.',  '#',  '#',
456     '#',  9,    10,   '#',  ' ',  13,   '.',  '.',
457     '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
458     '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
459     ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
460     '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
461     '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
462     '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
463     '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
464     'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
465     'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
466     'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
467     '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
468     'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
469     'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
470     'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<',
471     '<',  '=',  '>',  '#',  '#',  '.',  '#',  '#',
472     '#',  '#',  ' ',  '#',  ' ',  '>',  '.',  '.',
473     '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
474     '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
475     ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
476     '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
477     '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
478     '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
479     '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
480     'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
481     'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
482     'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
483     '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
484     'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
485     'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
486     'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<'
487 );
488 sub text_dp2ascii($)
489 {
490         my ($message) = @_;
491         $message = join '', map { $text_qfont_table[ord $_] } split //, $message;
492 }
493
494 sub color_dp2none($)
495 {
496         my ($message) = @_;
497         my $color = -1;
498         $message =~ s{\^(.)(?=([0-9,]?))}{
499                 my $c = $1;
500                 $c eq '^' ? '^' :
501                 $c =~ /^[0-9]$/ ? '' : "^$c";
502         }esg;
503         return text_dp2ascii $message;
504 }
505
506 sub color_dp2irc($)
507 {
508         my ($message) = @_;
509         my $color = -1;
510         $message =~ s{\^(.)(?=([0-9,]?))}{
511                 my $c = $1;
512                 my $f = $2;
513                 $c eq '^' ? '^' :
514                 $c =~ /^[0-9]$/ ? do {
515                         my $oldcolor = $color;
516                         $c = 0 if $c >= 7; # map 0, 7, 8, 9 to default (no bright white or such stuff)
517                         $color = $color_dp2irc_table[$c];
518                         ($color == $oldcolor) ? '' :
519                         $c == 0 ? "\0001" :
520                         $f eq ',' ? "\0003$color\0002\0002" :
521                         $f ne ''  ? sprintf "\0003%02d", $color : "\0003$color";
522                 } : "^$c";
523         }esg;
524         $message = text_dp2ascii $message;
525         $message =~ s/\0001/\017/g;
526         $message =~ s/\0002/\002/g;
527         $message =~ s/\0003/\003/g;
528         return $message;
529 }
530
531 sub color_dp2ansi($)
532 {
533         my ($message) = @_;
534         my $color = -1;
535         $message =~ s{\^(.)}{
536                 my $c = $1;
537                 $c eq '^' ? '^' :
538                 $c =~ /^[0-9]$/ ? do {
539                         my $oldcolor = $color;
540                         $color = $color_dp2ansi_table[$c];
541                         ($color eq $oldcolor) ? '' :
542                         "\000[${color}" # "
543                 } : "^$c";
544         }esg;
545         $message = text_dp2ascii $message;
546         $message =~ s/\000/\033/g;
547         return $message;
548 }
549
550 sub color_dpfix($)
551 {
552         my ($message) = @_;
553         # if the message ends with an odd number of ^, kill one
554         chop $message if $message =~ /(?:^|[^\^])\^(\^\^)*$/;
555         return $message;
556 }
557
558
559
560 # Nexuiz specific parsing of some server messages
561
562 sub nex_is_teamplay($)
563 {
564         my ($map) = @_;
565         return $map =~ /^(?:kh|ctf|tdm|dom)_/;
566 }
567
568 sub nex_slotsstring()
569 {
570         my $slotsstr = "";
571         if(defined $store{slots_max})
572         {
573                 my $slots = $store{slots_max} - $store{slots_active};
574                 my $slots_s = ($slots == 1) ? '' : 's';
575                 $slotsstr = " ($slots free slot$slots_s)";
576                 my $s = $config{dp_server_from_wan} || $config{dp_server};
577                 $slotsstr .= "; join now: \002nexuiz +connect $s"
578                         if $slots >= 1 and not $store{lms_blocked};
579         }
580         return $slotsstr;
581 }
582
583
584
585 # Do we have a config file? If yes, read and parse it (syntax: key = value
586 # pairs, separated by newlines), if not, complain.
587 die "Usage: $0 configfile\n"
588         unless @ARGV == 1;
589
590 open my $fh, "<", $ARGV[0]
591         or die "open $ARGV[0]: $!";
592 while(<$fh>)
593 {
594         chomp;
595         /^#/ and next;
596         /^(.*?)\s+=(?:\s+(.*))?$/ or next;
597         warn "Undefined config item: $1"
598                 unless exists $config{$1};
599         $config{$1} = defined $2 ? $2 : "";
600 }
601 close $fh;
602 my @missing = grep { !defined $config{$_} } keys %config;
603 die "The following config items are missing: @missing"
604         if @missing;
605
606
607
608 # Create a channel for error messages and other internal status messages...
609
610 $channels{system} = new Channel::FIFO();
611
612 # for example, quit messages caused by signals (if SIGTERM or SIGINT is first
613 # received, try to shut down cleanly, and if such a signal is received a second
614 # time, just exit)
615 my $quitting = 0;
616 $SIG{INT} = sub {
617         exit 1 if $quitting++;
618         $channels{system}->send("quit SIGINT");
619 };
620 $SIG{TERM} = sub {
621         exit 1 if $quitting++;
622         $channels{system}->send("quit SIGTERM");
623 };
624
625
626
627 # Create the two channels to gateway between...
628
629 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => "" => $config{irc_server} => 6667));
630 $channels{dp} = new Channel::QW(my $dpsock = new Connection::Socket(udp => $config{dp_listen} => $config{dp_server} => 26000), $config{dp_password});
631 $config{dp_listen} = $dpsock->sockname();
632 print "Listening on $config{dp_listen}\n";
633
634
635
636 # Utility routine to write to a channel by name, also outputting what's been written and some status
637 sub out($$@)
638 {
639         my $chanstr = shift;
640         my $nothrottle = shift;
641         my $chan = $channels{$chanstr};
642         if(!$chan)
643         {
644                 print "UNDEFINED: $chanstr, ignoring message\n";
645                 return;
646         }
647         @_ = $chan->join_commands(@_);
648         for(@_)
649         {
650                 my $result = $chan->send($_, $nothrottle);
651                 if($result > 0)
652                 {
653                         print "           $chanstr << $_\n";
654                 }
655                 elsif($result < 0)
656                 {
657                         print "FLOOD:     $chanstr << $_\n";
658                 }
659                 else
660                 {
661                         print "ERROR:     $chanstr << $_\n";
662                         $channels{system}->send("error $chanstr", 0);
663                 }
664         }
665 }
666
667
668
669 # Schedule a task for later execution by the main loop; usage: schedule sub {
670 # task... }, $time; When a scheduled task is run, a reference to the task's own
671 # sub is passed as first argument; that way, the task is able to re-schedule
672 # itself so it gets periodically executed.
673 sub schedule($$)
674 {
675         my ($sub, $time) = @_;
676         push @tasks, [time() + $time, $sub];
677 }
678
679 # Build up an IO::Select object for all our channels.
680 my $s = IO::Select->new();
681 for my $chan(values %channels)
682 {
683         $s->add($_) for $chan->fds();
684 }
685
686 # On IRC error, delete some data store variables of the connection, and
687 # reconnect to the IRC server soon (but only if someone is actually playing)
688 sub irc_error()
689 {
690         # prevent multiple instances of this timer
691         return if $store{irc_error_active};
692         $store{irc_error_active} = 1;
693
694         delete $channels{irc};
695         schedule sub {
696                 my ($timer) = @_;
697                 if(!defined $store{slots_full})
698                 {
699                         # DP is not running, then delay IRC reconnecting
700                         #use Data::Dumper; print Dumper \$timer;
701                         schedule $timer => 1;;
702                         return;
703                         # this will keep irc_error_active
704                 }
705                 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => "" => $config{irc_server}));
706                 delete $store{$_} for grep { /^irc_/ } keys %store;
707                 $store{irc_nick} = "";
708                 schedule sub {
709                         my ($timer) = @_;
710                         out dp => 0, 'status', 'log_dest_udp';
711                 } => 1;
712                 # this will clear irc_error_active
713         } => 30;
714         return 0;
715 }
716
717 # IRC joining (if this is called as response to a nick name collision, $is433 is set);
718 # among other stuff, it performs NickServ or Quakenet authentication. This is to be called
719 # until the channel has been joined for every message that may be "interesting" (basically,
720 # IRC 001 hello messages, 443 nick collision messages and some notices by services).
721 sub irc_joinstage($)
722 {
723         my($is433) = @_;
724
725         return 0
726                 if $store{irc_joined_channel};
727         
728                 #use Data::Dumper; print Dumper \%store;
729
730         if($is433)
731         {
732                 if(length $store{irc_nick})
733                 {
734                         # we already have another nick, but couldn't change to the new one
735                         # try ghosting and then get the nick again
736                         if(length $config{irc_nickserv_password})
737                         {
738                                 if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
739                                 {
740                                         $store{irc_nick_requested} = $config{irc_nick};
741                                         out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
742                                         schedule sub {
743                                                 out irc => 1, "NICK $config{irc_nick}";
744                                         } => 1;
745                                         return; # we'll get here again for the NICK success message, or for a 433 failure
746                                 }
747                                 # otherwise, we failed to ghost and will continue with the wrong
748                                 # nick... also, no need to try to identify here
749                         }
750                         # otherwise, we can't handle this and will continue with our wrong nick
751                 }
752                 else
753                 {
754                         # we failed to get an initial nickname
755                         # change ours a bit and try again
756                         if(length $store{irc_nick_requested} < 9)
757                         {
758                                 $store{irc_nick_requested} .= '_';
759                         }
760                         else
761                         {
762                                 substr $store{irc_nick_requested}, int(rand length $store{irc_nick_requested}), 1, chr(97 + int rand 26);
763                         }
764                         out irc => 1, "NICK $store{irc_nick_requested}";
765                         return; # when it fails, we'll get here again, and when it succeeds, we will continue
766                 }
767         }
768
769         # we got a 001 or a NICK message, so $store{irc_nick} has been updated
770         if(length $config{irc_nickserv_password})
771         {
772                 if($store{irc_nick} eq $config{irc_nick})
773                 {
774                         # identify
775                         out irc => 1, sprintf($config{irc_nickserv_identify}, $config{irc_nick}, $config{irc_nickserv_password});
776                 }
777                 else
778                 {
779                         # ghost
780                         if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
781                         {
782                                 $store{irc_nick_requested} = $config{irc_nick};
783                                 out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
784                                 schedule sub {
785                                         out irc => 1, "NICK $config{irc_nick}";
786                                 } => 1;
787                                 return; # we'll get here again for the NICK success message, or for a 433 failure
788                         }
789                         # otherwise, we failed to ghost and will continue with the wrong
790                         # nick... also, no need to try to identify here
791                 }
792         }
793
794         # we are on Quakenet. Try to authenticate.
795         if(length $config{irc_quakenet_password} and length $config{irc_quakenet_authname})
796         {
797                 if(defined $store{irc_quakenet_challenge})
798                 {
799                         if($store{irc_quakenet_challenge} =~ /^MD5 (.*)/)
800                         {
801                                 out irc => 1, "$config{irc_quakenet_challengeauth} $config{irc_quakenet_authname} " . Digest::MD5::md5_hex("$config{irc_quakenet_password} $1");
802                         }
803                 }
804                 else
805                 {
806                         out irc => 1, $config{irc_quakenet_getchallenge};
807                         return;
808                         # we get here again when Q asks us
809                 }
810         }
811         
812         # if we get here, we are on IRC
813         $store{irc_joined_channel} = 1;
814         schedule sub {
815                 out irc => 1, "JOIN $config{irc_channel}";
816         } => 1;
817         return 0;
818 }
819
820
821
822 # List of all handlers on the various sockets. Additional handlers can be added by a plugin.
823 @handlers = (
824         # detect a server restart and set it up again
825         [ dp => q{ *(?:Warning: Could not expand \$|Unknown command ")(?:rcon2irc_[a-z0-9_]*)[" ]*} => sub {
826                 out dp => 0,
827                         'alias rcon2irc_eval "$*"',
828                         'log_dest_udp',
829                         'sv_logscores_console 0',
830                         'sv_logscores_bots 1',
831                         'sv_eventlog 1',
832                         'sv_eventlog_console 1',
833                         'alias rcon2irc_say_as "set say_as_restorenick \"$sv_adminnick\"; sv_adminnick \"$1^3\"; say \"^7$2\"; rcon2irc_say_as_restore"',
834                         'alias rcon2irc_say_as_restore "set sv_adminnick \"$say_as_restorenick\""',
835                         'alias rcon2irc_quit "echo \"quitting rcon2irc $1: log_dest_udp is $log_dest_udp\""'; # note: \\\\\\" ->perl \\\" ->console \"
836                 return 0;
837         } ],
838
839         # detect missing entry in log_dest_udp and fix it
840         [ dp => q{"log_dest_udp" is "([^"]*)" \["[^"]*"\]} => sub {
841                 my ($dest) = @_;
842                 my @dests = split ' ', $dest;
843                 return 0 if grep { $_ eq $config{dp_listen} } @dests;
844                 out dp => 0, 'log_dest_udp "' . join(" ", @dests, $config{dp_listen}) . '"';
845                 return 0;
846         } ],
847
848         # retrieve hostname from status replies
849         [ dp => q{host:     (.*)} => sub {
850                 my ($name) = @_;
851                 $store{dp_hostname} = $name;
852                 return 0;
853         } ],
854
855         # retrieve version from status replies
856         [ dp => q{version:  (.*)} => sub {
857                 my ($version) = @_;
858                 $store{dp_version} = $version;
859                 return 0;
860         } ],
861
862         # retrieve number of open player slots
863         [ dp => q{players:  (\d+) active \((\d+) max\)} => sub {
864                 my ($active, $max) = @_;
865                 my $full = ($active >= $max);
866                 $store{slots_max} = $max;
867                 $store{slots_active} = $active;
868                 if($full != ($store{slots_full} || 0))
869                 {
870                         $store{slots_full} = $full;
871                         return 0
872                                 if $store{lms_blocked};
873                         if($full)
874                         {
875                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION is full!\001";
876                         }
877                         else
878                         {
879                                 my $slotsstr = nex_slotsstring();
880                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can be joined again$slotsstr!\001";
881                         }
882                 }
883                 return 0;
884         } ],
885
886         # LMS: detect "no more lives" message
887         [ dp => q{\^4.*\^4 has no more lives left} => sub {
888                 if(!$store{lms_blocked})
889                 {
890                         $store{lms_blocked} = 1;
891                         if(!$store{slots_full})
892                         {
893                                 schedule sub {
894                                         if($store{lms_blocked})
895                                         {
896                                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can't be joined until next round (a player has no more lives left)\001";
897                                         }
898                                 } => 1;
899                         }
900                 }
901         } ],
902
903         # detect IRC errors and reconnect
904         [ irc => q{ERROR .*} => \&irc_error ],
905         [ system => q{error irc} => \&irc_error ],
906
907         # IRC nick in use
908         [ irc => q{:[^ ]* 433 .*} => sub {
909                 return irc_joinstage(433);
910         } ],
911
912         # IRC welcome
913         [ irc => q{:[^ ]* 001 .*} => sub {
914                 $store{irc_seen_welcome} = 1;
915                 $store{irc_nick} = $store{irc_nick_requested};
916                 return irc_joinstage(0);
917         } ],
918
919         # IRC my nickname changed
920         [ irc => q{:(?i:(??{$store{irc_nick}}))![^ ]* (?i:NICK) :(.*)} => sub {
921                 my ($n) = @_;
922                 $store{irc_nick} = $n;
923                 return irc_joinstage(0);
924         } ],
925
926         # Quakenet: challenge from Q
927         [ irc => q{(??{$config{irc_quakenet_challengeprefix}}) (.*)} => sub {
928                 $store{irc_quakenet_challenge} = $1;
929                 return irc_joinstage(0);
930         } ],
931
932         # shut down everything on SIGINT
933         [ system => q{quit (.*)} => sub {
934                 my ($cause) = @_;
935                 out irc => 1, "QUIT :$cause";
936                 $store{quitcookie} = int rand 1000000000;
937                 out dp => 0, "rcon2irc_quit $store{quitcookie}";
938         } ],
939
940         # remove myself from the log destinations and exit everything
941         [ dp => q{quitting rcon2irc (??{$store{quitcookie}}): log_dest_udp is (.*) *} => sub {
942                 my ($dest) = @_;
943                 my @dests = grep { $_ ne $config{dp_listen} } split ' ', $dest;
944                 out dp => 0, 'log_dest_udp "' . join(" ", @dests) . '"';
945                 exit 0;
946                 return 0;
947         } ],
948
949         # IRC PING
950         [ irc => q{PING (.*)} => sub {
951                 my ($data) = @_;
952                 out irc => 1, "PONG $data";
953                 return 1;
954         } ],
955
956         # IRC PONG
957         [ irc => q{:[^ ]* PONG .* :(.*)} => sub {
958                 my ($data) = @_;
959                 return 0
960                         if not defined $store{irc_pingtime};
961                 return 0
962                         if $data ne $store{irc_pingtime};
963                 print "* measured IRC line delay: @{[time() - $store{irc_pingtime}]}\n";
964                 undef $store{irc_pingtime};
965                 return 0;
966         } ],
967
968         # detect channel join message and note hostname length to get the maximum allowed line length
969         [ irc => q{(:(?i:(??{$store{irc_nick}}))![^ ]* )(?i:JOIN) :(?i:(??{$config{irc_channel}}))} => sub {
970                 $store{irc_maxlen} = 510 - length($1);
971                 $store{irc_joined_channel} = 1;
972                 print "* detected maximum line length for channel messages: $store{irc_maxlen}\n";
973                 return 0;
974         } ],
975
976         # chat: Nexuiz server -> IRC channel
977         [ dp => q{\001(.*?)\^7: (.*)} => sub {
978                 my ($nick, $message) = map { color_dp2irc $_ } @_;
979                 out irc => 0, "PRIVMSG $config{irc_channel} :<$nick\017> $message";
980                 return 0;
981         } ],
982
983         # chat: Nexuiz server -> IRC channel, nick set
984         [ dp => q{:join:(\d+):(?:player|bot):(.*)} => sub {
985                 my ($id, $nick) = @_;
986                 $nick = color_dp2irc $nick;
987                 $store{"playernick_$id"} = $nick;
988                 return 0;
989         } ],
990
991         # chat: Nexuiz server -> IRC channel, nick change/set
992         [ dp => q{:name:(\d+):(.*)} => sub {
993                 my ($id, $nick) = @_;
994                 $nick = color_dp2irc $nick;
995                 my $oldnick = $store{"playernick_$id"};
996                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 is now known as $nick";
997                 $store{"playernick_$id"} = $nick;
998                 return 0;
999         } ],
1000
1001         # chat: Nexuiz server -> IRC channel, vote call
1002         [ dp => q{:vote:vcall:(\d+):(.*)} => sub {
1003                 my ($id, $command) = @_;
1004                 $command = color_dp2irc $command;
1005                 my $oldnick = $store{"playernick_$id"};
1006                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 calls a vote for \"$command\017\"";
1007                 return 0;
1008         } ],
1009
1010         # chat: Nexuiz server -> IRC channel, vote stop
1011         [ dp => q{:vote:vstop:(\d+)} => sub {
1012                 my ($id) = @_;
1013                 my $oldnick = $store{"playernick_$id"};
1014                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 stopped the vote";
1015                 return 0;
1016         } ],
1017
1018         # chat: Nexuiz server -> IRC channel, master login
1019         [ dp => q{:vote:vlogin:(\d+)} => sub {
1020                 my ($id) = @_;
1021                 my $oldnick = $store{"playernick_$id"};
1022                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 logged in as master";
1023                 return 0;
1024         } ],
1025
1026         # chat: Nexuiz server -> IRC channel, master do
1027         [ dp => q{:vote:vdo:(\d+):(.*)} => sub {
1028                 my ($id, $command) = @_;
1029                 my $oldnick = $store{"playernick_$id"};
1030                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 used his master status to do \"$command\017\"";
1031                 return 0;
1032         } ],
1033
1034         # chat: Nexuiz server -> IRC channel, result
1035         [ dp => q{:vote:v(yes|no|timeout):(\d+):(\d+):(\d+):(\d+):(-?\d+)} => sub {
1036                 my ($result, $yes, $no, $abstain, $not, $min) = @_;
1037                 my $spam = "$yes:$no" . (($min >= 0) ? " ($min needed)" : "") . " $abstain didn't care, $not didn't vote";
1038                 out irc => 0, "PRIVMSG $config{irc_channel} :* the vote ended with $result: $spam";
1039                 return 0;
1040         } ],
1041
1042         # chat: IRC channel -> Nexuiz server
1043         [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$store{irc_nick}}))(?: |: ?)(.*)} => sub {
1044                 my ($nick, $message) = @_;
1045                 $nick = color_dpfix $nick;
1046                         # allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
1047                 $message = color_irc2dp $message;
1048                 $message =~ s/(["\\])/\\$1/g;
1049                 out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
1050                 return 0;
1051         } ],
1052
1053         # irc: CTCP VERSION reply
1054         [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$store{irc_nick}})) :\001VERSION( .*)?\001} => sub {
1055                 my ($nick) = @_;
1056                 my $ver = $store{dp_version} or return 0;
1057                 $ver .= ", rcon2irc $VERSION";
1058                 out irc => 0, "NOTICE $nick :\001VERSION $ver\001";
1059         } ],
1060
1061         # on game start, notify the channel
1062         [ dp => q{:gamestart:(.*):[0-9.]*} => sub {
1063                 my ($map) = @_;
1064                 $store{playing} = 1;
1065                 $store{map} = $map;
1066                 $store{map_starttime} = time();
1067                 my $slotsstr = nex_slotsstring();
1068                 out irc => 0, "PRIVMSG $config{irc_channel} :\00304" . $map . "\017 has begun$slotsstr";
1069                 delete $store{lms_blocked};
1070                 return 0;
1071         } ],
1072
1073         # on game over, clear the current map
1074         [ dp => q{:gameover} => sub {
1075                 $store{playing} = 0;
1076                 return 0;
1077         } ],
1078
1079         # scores: Nexuiz server -> IRC channel (start)
1080         [ dp => q{:scores:(.*):(\d+)} => sub {
1081                 my ($map, $time) = @_;
1082                 $store{scores} = {};
1083                 $store{scores}{map} = $map;
1084                 $store{scores}{time} = $time;
1085                 $store{scores}{players} = [];
1086                 delete $store{lms_blocked};
1087                 return 0;
1088         } ],
1089
1090         # scores: Nexuiz server -> IRC channel
1091         [ dp => q{:player:(-?\d+):(\d+):(\d+):(\d+):(\d+):(.*)} => sub {
1092                 my ($frags, $deaths, $time, $team, $id, $name) = @_;
1093                 return if not exists $store{scores};
1094                 push @{$store{scores}{players}}, [$frags, $team, $name]
1095                         unless $frags <= -666; # no spectators
1096                 return 0;
1097         } ],
1098
1099         # scores: Nexuiz server -> IRC channel
1100         [ dp => q{:end} => sub {
1101                 return if not exists $store{scores};
1102                 my $s = $store{scores};
1103                 delete $store{scores};
1104                 my $teams_matter = nex_is_teamplay($s->{map});
1105
1106                 my @t = ();
1107                 my @p = ();
1108
1109                 if($teams_matter)
1110                 {
1111                         # put players into teams
1112                         my %t = ();
1113                         for(@{$s->{players}})
1114                         {
1115                                 my $thisteam = ($t{$_->[1]} ||= {score => 0, team => $_->[1], players => []});
1116                                 push @{$thisteam->{players}}, [$_->[0], $_->[1], $_->[2]];
1117                                 $thisteam->{score} += $_->[0];
1118                         }
1119
1120                         # sort by team score
1121                         @t = sort { $b->{score} <=> $a->{score} } values %t;
1122
1123                         # sort by player score
1124                         @p = ();
1125                         for(@t)
1126                         {
1127                                 @{$_->{players}} = sort { $b->[0] <=> $a->[0] } @{$_->{players}};
1128                                 push @p, @{$_->{players}};
1129                         }
1130                 }
1131                 else
1132                 {
1133                         @p = sort { $b->[0] <=> $a->[0] } @{$s->{players}};
1134                 }
1135
1136                 # no display for empty server
1137                 return 0
1138                         if !@p;
1139
1140                 # make message fit somehow
1141                 for my $maxnamelen(reverse 3..64)
1142                 {
1143                         my $scores_string = "PRIVMSG $config{irc_channel} :\00304" . $s->{map} . "\017 ended:";
1144                         if($teams_matter)
1145                         {
1146                                 my $sep = ' ';
1147                                 for(@t)
1148                                 {
1149                                         $scores_string .= $sep . sprintf "\003%02d\%d\017", $color_team2irc_table{$_->{team}}, $_->{score};
1150                                         $sep = ':';
1151                                 }
1152                         }
1153                         my $sep = '';
1154                         for(@p)
1155                         {
1156                                 my ($frags, $team, $name) = @$_;
1157                                 $name = color_dpfix substr($name, 0, $maxnamelen);
1158                                 if($teams_matter)
1159                                 {
1160                                         $name = "\003" . $color_team2irc_table{$team} . " " . color_dp2none $name;
1161                                 }
1162                                 else
1163                                 {
1164                                         $name = " " . color_dp2irc $name;
1165                                 }
1166                                 $scores_string .= "$sep$name\017 $frags";
1167                                 $sep = ',';
1168                         }
1169                         if(length($scores_string) <= ($store{irc_maxlen} || 256))
1170                         {
1171                                 out irc => 0, $scores_string;
1172                                 return 0;
1173                         }
1174                 }
1175                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION would have LIKED to put the scores here, but they wouldn't fit :(\001";
1176                 return 0;
1177         } ],
1178
1179         # complain when system load gets too high
1180         [ dp => q{timing:   (([0-9.]*)% CPU, ([0-9.]*)% lost, offset avg ([0-9.]*)ms, max ([0-9.]*)ms, sdev ([0-9.]*)ms)} => sub {
1181                 my ($all, $cpu, $lost, $avg, $max, $sdev) = @_;
1182                 return 0 # don't complain when just on the voting screen
1183                         if !$store{playing};
1184                 return 0 # don't complain if it was less than 0.5%
1185                         if $lost < 0.5;
1186                 return 0 # don't complain if nobody is looking
1187                         if $store{slots_active} == 0;
1188                 return 0 # don't complain in the first two minutes
1189                         if time() - $store{map_starttime} < 120;
1190                 return 0 # don't complain if it was already at least half as bad in this round
1191                         if $store{map_starttime} == $store{timingerror_map_starttime} and $lost <= 2 * $store{timingerror_lost};
1192                 $store{timingerror_map_starttime} = $store{map_starttime};
1193                 $store{timingerror_lost} = $lost;
1194                 out dp => 0, 'rcon2irc_say_as server "There are currently some severe system load problems. The admins have been notified."';
1195                 out irc => 1, "PRIVMSG $config{irc_channel} :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1196                 #out irc => 1, "PRIVMSG OpBaI :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1197                 return 0;
1198         } ],
1199 );
1200
1201
1202
1203 # Load plugins and add them to the handler list in the front.
1204 for my $p(split ' ', $config{plugins})
1205 {
1206         my @h = eval { do $p; }
1207                 or die "Invalid plugin $p: $@";
1208         for(reverse @h)
1209         {
1210                 ref $_ eq 'ARRAY' or die "Invalid plugin $p: did not return a list of arrays";
1211                 @$_ == 3 or die "Invalid plugin $p: did not return a list of three-element arrays";
1212                 !ref $_->[0] && !ref $_->[1] && ref $_->[2] eq 'CODE' or die "Invalid plugin $p: did not return a list of string-string-sub arrays";
1213                 unshift @handlers, $_;
1214         }
1215 }
1216
1217
1218
1219 # verify that the server is up by letting it echo back a string that causes
1220 # re-initialization of the required aliases
1221 out dp => 0, 'echo "Unknown command \"rcon2irc_eval\""'; # assume the server has been restarted
1222
1223
1224
1225 # regularily, query the server status and if it still is connected to us using
1226 # the log_dest_udp feature. If not, we will detect the response to this rcon
1227 # command and re-initialize the server's connection to us (either by log_dest_udp
1228 # not containing our own IP:port, or by rcon2irc_eval not being a defined command).
1229 schedule sub {
1230         my ($timer) = @_;
1231         out dp => 0, 'status', 'log_dest_udp', 'rcon2irc_eval set dummy 1';
1232         schedule $timer => (exists $store{dp_hostname} ? $config{dp_status_delay} : 1);;
1233 } => 1;
1234
1235
1236
1237 # Continue with connecting to IRC as soon as we get our first status reply from
1238 # the DP server (which contains the server's hostname that we'll use as
1239 # realname for IRC).
1240 schedule sub {
1241         my ($timer) = @_;
1242
1243         # log on to IRC when needed
1244         if(exists $store{dp_hostname} && !exists $store{irc_logged_in})
1245         {
1246                 $store{irc_nick_requested} = $config{irc_nick};
1247                 out irc => 1, "NICK $config{irc_nick}", "USER $config{irc_user} localhost localhost :$store{dp_hostname}";
1248                 $store{irc_logged_in} = 1;
1249                 undef $store{irc_maxlen};
1250                 undef $store{irc_pingtime};
1251         }
1252
1253         schedule $timer => 1;;
1254 } => 1;
1255
1256
1257
1258 # Regularily ping the IRC server to detect if the connection is down. If it is,
1259 # schedule an IRC error that will cause reconnection later.
1260 schedule sub {
1261         my ($timer) = @_;
1262
1263         if($store{irc_logged_in})
1264         {
1265                 if(defined $store{irc_pingtime})
1266                 {
1267                         # IRC connection apparently broke
1268                         # so... KILL IT WITH FIRE
1269                         $channels{system}->send("error irc", 0);
1270                 }
1271                 else
1272                 {
1273                         # everything is fine, send a new ping
1274                         $store{irc_pingtime} = time();
1275                         out irc => 1, "PING $store{irc_pingtime}";
1276                 }
1277         }
1278
1279         schedule $timer => $config{irc_ping_delay};;
1280 } => 1;
1281
1282
1283
1284 # Main loop.
1285 for(;;)
1286 {
1287         # wait for something to happen on our sockets, or wait 2 seconds without anything happening there
1288         $s->can_read(2);
1289         my @errors = $s->has_exception(0);
1290
1291         # on every channel, look for incoming messages
1292         CHANNEL:
1293         for my $chanstr(keys %channels)
1294         {
1295                 my $chan = $channels{$chanstr};
1296                 my @chanfds = $chan->fds();
1297
1298                 for my $chanfd(@chanfds)
1299                 {
1300                         if(grep { $_ == $chanfd } @errors)
1301                         {
1302                                 # STOP! This channel errored!
1303                                 $channels{system}->send("error $chanstr", 0);
1304                                 next CHANNEL;
1305                         }
1306                 }
1307
1308                 for my $line($chan->recv())
1309                 {
1310                         # found one! Check if it matches the regular expression of one of
1311                         # our handlers...
1312                         my $handled = 0;
1313                         for my $h(@handlers)
1314                         {
1315                                 my ($chanstr_wanted, $re, $sub) = @$h;
1316                                 next
1317                                         if $chanstr_wanted ne $chanstr;
1318                                 use re 'eval';
1319                                 my @matches = ($line =~ /^$re$/s);
1320                                 no re 'eval';
1321                                 next
1322                                         unless @matches;
1323                                 # and if it is a match, handle it.
1324                                 ++$handled;
1325                                 my $result = $sub->(@matches);
1326                                 last
1327                                         if $result;
1328                         }
1329                         # print the message, together with info on whether it has been handled or not
1330                         if($handled)
1331                         {
1332                                 print "           $chanstr >> $line\n";
1333                         }
1334                         else
1335                         {
1336                                 print "unhandled: $chanstr >> $line\n";
1337                         }
1338                 }
1339         }
1340
1341         # handle scheduled tasks...
1342         my @t = @tasks;
1343         my $t = time();
1344         # by emptying the list of tasks...
1345         @tasks = ();
1346         for(@t)
1347         {
1348                 my ($time, $sub) = @$_;
1349                 if($t >= $time)
1350                 {
1351                         # calling them if they are schedled for the "past"...
1352                         $sub->($sub);
1353                 }
1354                 else
1355                 {
1356                         # or re-adding them to the task list if they still are scheduled for the "future"
1357                         push @tasks, [$time, $sub];
1358                 }
1359         }
1360 }