]> git.xonotic.org Git - xonotic/xonotic.git/blob - server/rcon2irc/rcon2irc.pl
Merge remote branch 'origin/master' into divVerent/crypto2
[xonotic/xonotic.git] / 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 # MISC STRING UTILITY ROUTINES to convert between DarkPlaces and IRC conventions
29
30 # convert mIRC color codes to DP color codes
31 our @color_irc2dp_table = (7, 0, 4, 2, 1, 1, 6, 1, 3, 2, 5, 5, 4, 6, 7, 7);
32 our @color_dp2irc_table = (-1, 4, 9, 8, 12, 11, 13, -1, -1, -1); # not accurate, but legible
33 our @color_dp2ansi_table = ("m", "1;31m", "1;32m", "1;33m", "1;34m", "1;36m", "1;35m", "m", "1m", "1m"); # not accurate, but legible
34 our %color_team2dp_table = (5 => 1, 14 => 4, 13 => 3, 10 => 6);
35 our %color_team2irc_table = (5 => 4, 14 => 12, 13 => 8, 10 => 13);
36 sub color_irc2dp($)
37 {
38         my ($message) = @_;
39         $message =~ s/\^/^^/g;
40         my $color = 7;
41         $message =~ s{\003(\d\d?)(?:,(\d?\d?))?|(\017)}{
42                 # $1 is FG, $2 is BG, but let's ignore BG
43                 my $oldcolor = $color;
44                 if($3)
45                 {
46                         $color = 7;
47                 }
48                 else
49                 {
50                         $color = $color_irc2dp_table[$1];
51                         $color = $oldcolor if not defined $color;
52                 }
53                 ($color == $oldcolor) ? '' : '^' . $color;
54         }esg;
55         $message =~ s{[\000-\037]}{}gs; # kill bold etc. for now
56         return $message;
57 }
58
59 our @text_qfont_table = ( # ripped from DP console.c qfont_table
60     "\0", '#',  '#',  '#',  '#',  '.',  '#',  '#',
61     '#',  9,    10,   '#',  ' ',  13,   '.',  '.',
62     '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
63     '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
64     ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
65     '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
66     '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
67     '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
68     '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
69     'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
70     'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
71     'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
72     '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
73     'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
74     'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
75     'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<',
76     '<',  '=',  '>',  '#',  '#',  '.',  '#',  '#',
77     '#',  '#',  ' ',  '#',  ' ',  '>',  '.',  '.',
78     '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
79     '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
80     ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
81     '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
82     '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
83     '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
84     '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
85     'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
86     'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
87     'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
88     '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
89     'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
90     'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
91     'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<'
92 );
93 sub text_dp2ascii($)
94 {
95         my ($message) = @_;
96         $message = join '', map { $text_qfont_table[ord $_] } split //, $message;
97 }
98
99 sub color_dp_transform(&$)
100 {
101         my ($block, $message) = @_;
102
103         $message =~ s{(?:(\^\^)|\^x([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])|\^([0-9])|(.))(?=([0-9,]?))}{
104                 defined $1 ? $block->(char => '^', $7) :
105                 defined $2 ? $block->(rgb => [hex $2, hex $3, hex $4], $7) :
106                 defined $5 ? $block->(color => $5, $7) :
107                 defined $6 ? $block->(char => $6, $7) :
108                         die "Invalid match";
109         }esg;
110
111         return $message;
112 }
113
114 sub color_dp2none($)
115 {
116         my ($message) = @_;
117
118         return color_dp_transform
119         {
120                 my ($type, $data, $next) = @_;
121                 $type eq 'char'
122                         ? $text_qfont_table[ord $data]
123                         : "";
124         }
125         $message;
126 }
127
128 sub color_rgb2basic($)
129 {
130         my ($data) = @_;
131         my ($R, $G, $B) = @$data;
132         my $min = [sort { $a <=> $b } ($R, $G, $B)]->[0];
133         my $max = [sort { $a <=> $b } ($R, $G, $B)]->[-1];
134
135         my $v = $max / 15;
136         my $s = ($max == $min) ? 0 : 1 - $min/$max;
137
138         if($s < 0.2)
139         {
140                 return 0 if $v < 0.5;
141                 return 7;
142         }
143
144         my $h;
145         if($max == $min)
146         {
147                 $h = 0;
148         }
149         elsif($max == $R)
150         {
151                 $h = (60 * ($G - $B) / ($max - $min)) % 360;
152         }
153         elsif($max == $G)
154         {
155                 $h = (60 * ($B - $R) / ($max - $min)) + 120;
156         }
157         elsif($max == $B)
158         {
159                 $h = (60 * ($R - $G) / ($max - $min)) + 240;
160         }
161
162         return 1 if $h < 36;
163         return 3 if $h < 80;
164         return 2 if $h < 150;
165         return 5 if $h < 200;
166         return 4 if $h < 270;
167         return 6 if $h < 330;
168         return 1;
169 }
170
171 sub color_dp_rgb2basic($)
172 {
173         my ($message) = @_;
174         return color_dp_transform
175         {
176                 my ($type, $data, $next) = @_;
177                 $type eq 'char'  ? ($data eq '^' ? '^^' : $data) :
178                 $type eq 'color' ? "^$data" :
179                 $type eq 'rgb'   ? "^" . color_rgb2basic $data :
180                         die "Invalid type";
181         }
182         $message;
183 }
184
185 sub color_dp2irc($)
186 {
187         my ($message) = @_;
188         my $color = -1;
189         return color_dp_transform
190         {
191                 my ($type, $data, $next) = @_;
192
193                 if($type eq 'rgb')
194                 {
195                         $type = 'color';
196                         $data = color_rgb2basic $data;
197                 }
198
199                 $type eq 'char'  ? $text_qfont_table[ord $data] :
200                 $type eq 'color' ? do {
201                         my $oldcolor = $color;
202                         $color = $color_dp2irc_table[$data];
203
204                         $color == $oldcolor               ? '' :
205                         $color < 0                        ? "\017" :
206                         (index '0123456789,', $next) >= 0 ? "\003$color\002\002" :
207                                                             "\003$color";
208                 } :
209                         die "Invalid type";
210         }
211         $message;
212 }
213
214 sub color_dp2ansi($)
215 {
216         my ($message) = @_;
217         my $color = -1;
218         return color_dp_transform
219         {
220                 my ($type, $data, $next) = @_;
221
222                 if($type eq 'rgb')
223                 {
224                         $type = 'color';
225                         $data = color_rgb2basic $data;
226                 }
227
228                 $type eq 'char'  ? $text_qfont_table[ord $data] :
229                 $type eq 'color' ? do {
230                         my $oldcolor = $color;
231                         $color = $color_dp2ansi_table[$data];
232
233                         $color eq $oldcolor ? '' :
234                                               "\033[${color}"
235                 } :
236                         die "Invalid type";
237         }
238         $message;
239 }
240
241 sub color_dpfix($)
242 {
243         my ($message) = @_;
244         # if the message ends with an odd number of ^, kill one
245         chop $message if $message =~ /(?:^|[^\^])\^(\^\^)*$/;
246         return $message;
247 }
248
249
250
251
252 # Interfaces:
253 #   Connection:
254 #     $conn->sockname() returns a connection type specific representation
255 #       string of the local address, or undef if not applicable.
256 #     $conn->send("string") sends something over the connection.
257 #     $conn->recv() receives a string from the connection, or returns "" if no
258 #       data is available.
259 #     $conn->fds() returns all file descriptors used by the connection, so one
260 #       can use select() on them.
261 #   Channel:
262 #     Usually wraps around a connection and implements a command based
263 #     structure over it. It usually is constructed using new
264 #     ChannelType($connection, someparameters...)
265 #     @cmds = $chan->join_commands(@cmds) joins multiple commands to a single
266 #       command string if the protocol supports it, or does nothing and leaves
267 #       @cmds unchanged if the protocol does not support that usage (this is
268 #       meant to save send() invocations).
269 #     $chan->send($command, $nothrottle) sends a command over the channel. If
270 #       $nothrottle is sent, the command must not be left out even if the channel
271 #       is saturated (for example, because of IRC's flood control mechanism).
272 #     $chan->quote($str) returns a string in a quoted form so it can safely be
273 #       inserted as a substring into a command, or returns $str as is if not
274 #       applicable. It is assumed that the result of the quote method is used
275 #       as part of a quoted string, if the protocol supports that.
276 #     $chan->recv() returns a list of received commands from the channel, or
277 #       the empty list if none are available.
278 #     $conn->fds() returns all file descriptors used by the channel's
279 #       connections, so one can use select() on them.
280
281
282
283
284
285
286
287 # Socket connection.
288 # Represents a connection over a socket.
289 # Mainly used to wrap a channel around it for, in this case, line based or rcon-like operation.
290 package Connection::Socket;
291 use strict;
292 use warnings;
293 use IO::Socket::INET;
294 use IO::Handle;
295
296 # Constructor:
297 #   my $conn = new Connection::Socket(tcp => "localaddress" => "remoteaddress" => 6667);
298 # If the remote address does not contain a port number, the numeric port is
299 # used (it serves as a default port).
300 sub new($$)
301 {
302         my ($class, $proto, $local, $remote, $defaultport) = @_;
303         my $sock = IO::Socket::INET->new(
304                 Proto => $proto,
305                 (length($local) ? (LocalAddr => $local) : ()),
306                 PeerAddr => $remote,
307                 PeerPort => $defaultport
308         ) or die "socket $proto/$local/$remote/$defaultport: $!";
309         $sock->blocking(0);
310         my $you = {
311                 # Mortal fool! Release me from this wretched tomb! I must be set free
312                 # or I will haunt you forever! I will hide your keys beneath the
313                 # cushions of your upholstered furniture... and NEVERMORE will you be
314                 # able to find socks that match!
315                 sock => $sock,
316                 # My demonic powers have made me OMNIPOTENT! Bwahahahahahahaha!
317         };
318         return
319                 bless $you, 'Connection::Socket';
320 }
321
322 # $sock->sockname() returns the local address of the socket.
323 sub sockname($)
324 {
325         my ($self) = @_;
326         my ($port, $addr) = sockaddr_in $self->{sock}->sockname();
327         return "@{[inet_ntoa $addr]}:$port";
328 }
329
330 # $sock->send($data) sends some data over the socket; on success, 1 is returned.
331 sub send($$)
332 {
333         my ($self, $data) = @_;
334         return 1
335                 if not length $data;
336         if(not eval { $self->{sock}->send($data); })
337         {
338                 warn "$@";
339                 return 0;
340         }
341         return 1;
342 }
343
344 # $sock->recv() receives as much as possible from the socket (or at most 32k). Returns "" if no data is available.
345 sub recv($)
346 {
347         my ($self) = @_;
348         my $data = "";
349         if(defined $self->{sock}->recv($data, 32768, 0))
350         {
351                 return $data;
352         }
353         elsif($!{EAGAIN})
354         {
355                 return "";
356         }
357         else
358         {
359                 return undef;
360         }
361 }
362
363 # $sock->fds() returns the socket file descriptor.
364 sub fds($)
365 {
366         my ($self) = @_;
367         return fileno $self->{sock};
368 }
369
370
371
372
373
374
375
376 # Line-based buffered connectionless FIFO channel.
377 # Whatever is sent to it using send() is echoed back when using recv().
378 package Channel::FIFO;
379 use strict;
380 use warnings;
381
382 # Constructor:
383 #   my $chan = new Channel::FIFO();
384 sub new($)
385 {
386         my ($class) = @_;
387         my $you = {
388                 buffer => []
389         };
390         return
391                 bless $you, 'Channel::FIFO';
392 }
393
394 sub join_commands($@)
395 {
396         my ($self, @data) = @_;
397         return @data;
398 }
399
400 sub send($$$)
401 {
402         my ($self, $line, $nothrottle) = @_;
403         push @{$self->{buffer}}, $line;
404 }
405
406 sub quote($$)
407 {
408         my ($self, $data) = @_;
409         return $data;
410 }
411
412 sub recv($)
413 {
414         my ($self) = @_;
415         my $r = $self->{buffer};
416         $self->{buffer} = [];
417         return @$r;
418 }
419
420 sub fds($)
421 {
422         my ($self) = @_;
423         return ();
424 }
425
426
427
428
429
430
431
432 # QW rcon protocol channel.
433 # Wraps around a UDP based Connection and sends commands as rcon commands as
434 # well as receives rcon replies. The quote and join_commands methods are using
435 # DarkPlaces engine specific rcon protocol extensions.
436 package Channel::QW;
437 use strict;
438 use warnings;
439 use Digest::HMAC;
440 use Digest::MD4;
441
442 # Constructor:
443 #   my $chan = new Channel::QW($connection, "password");
444 sub new($$$)
445 {
446         my ($class, $conn, $password, $secure, $timeout) = @_;
447         my $you = {
448                 connector => $conn,
449                 password => $password,
450                 recvbuf => "",
451                 secure => $secure,
452                 timeout => $timeout,
453         };
454         return
455                 bless $you, 'Channel::QW';
456 }
457
458 # Note: multiple commands in one rcon packet is a DarkPlaces extension.
459 sub join_commands($@)
460 {
461         my ($self, @data) = @_;
462         return join "\0", @data;
463 }
464
465 sub send($$$)
466 {
467         my ($self, $line, $nothrottle) = @_;
468         if($self->{secure} > 1)
469         {
470                 $self->{connector}->send("\377\377\377\377getchallenge");
471                 my $c = $self->recvchallenge();
472                 return 0 if not defined $c;
473                 my $key = Digest::HMAC::hmac("$c $line", $self->{password}, \&Digest::MD4::md4);
474                 return $self->{connector}->send("\377\377\377\377srcon HMAC-MD4 CHALLENGE $key $c $line");
475         }
476         elsif($self->{secure})
477         {
478                 my $t = sprintf "%ld.%06d", time(), int rand 1000000;
479                 my $key = Digest::HMAC::hmac("$t $line", $self->{password}, \&Digest::MD4::md4);
480                 return $self->{connector}->send("\377\377\377\377srcon HMAC-MD4 TIME $key $t $line");
481         }
482         else
483         {
484                 return $self->{connector}->send("\377\377\377\377rcon $self->{password} $line");
485         }
486 }
487
488 # Note: backslash and quotation mark escaping is a DarkPlaces extension.
489 sub quote($$)
490 {
491         my ($self, $data) = @_;
492         $data =~ s/[\000-\037]//g;
493         $data =~ s/([\\"])/\\$1/g;
494         $data =~ s/\$/\$\$/g;
495         return $data;
496 }
497
498 sub recvchallenge($)
499 {
500         my ($self) = @_;
501
502         my $sel = IO::Select->new($self->fds());
503         my $endtime_max = Time::HiRes::time() + $self->{timeout};
504         my $endtime = $endtime_max;
505
506         while((my $dt = $endtime - Time::HiRes::time()) > 0)
507         {
508                 if($sel->can_read($dt))
509                 {
510                         for(;;)
511                         {
512                                 my $s = $self->{connector}->recv();
513                                 die "read error\n"
514                                         if not defined $s;
515                                 length $s
516                                         or last;
517                                 if($s =~ /^\377\377\377\377challenge (.*)(?:$|\0)/s)
518                                 {
519                                         return $1;
520                                 }
521                                 next
522                                         if $s !~ /^\377\377\377\377n(.*)$/s;
523                                 $self->{recvbuf} .= $1;
524                         }
525                 }
526         }
527         return undef;
528 }
529
530 sub recv($)
531 {
532         my ($self) = @_;
533         for(;;)
534         {
535                 my $s = $self->{connector}->recv();
536                 die "read error\n"
537                         if not defined $s;
538                 length $s
539                         or last;
540                 next
541                         if $s !~ /^\377\377\377\377n(.*)$/s;
542                 $self->{recvbuf} .= $1;
543         }
544         my @out = ();
545         while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
546         {
547                 push @out, $1;
548         }
549         return @out;
550 }
551
552 sub fds($)
553 {
554         my ($self) = @_;
555         return $self->{connector}->fds();
556 }
557
558
559
560
561
562
563
564 # Line based protocol channel.
565 # Wraps around a TCP based Connection and sends commands as text lines
566 # (separated by CRLF). When reading responses from the Connection, any type of
567 # line ending is accepted.
568 # A flood control mechanism is implemented.
569 package Channel::Line;
570 use strict;
571 use warnings;
572 use Time::HiRes qw/time/;
573
574 # Constructor:
575 #   my $chan = new Channel::Line($connection);
576 sub new($$)
577 {
578         my ($class, $conn) = @_;
579         my $you = {
580                 connector => $conn,
581                 recvbuf => "",
582                 capacity => undef,
583                 linepersec => undef,
584                 maxlines => undef,
585                 lastsend => time()
586         };
587         return 
588                 bless $you, 'Channel::Line';
589 }
590
591 sub join_commands($@)
592 {
593         my ($self, @data) = @_;
594         return @data;
595 }
596
597 # Sets new flood control parameters:
598 #   $chan->throttle(maximum lines per second, maximum burst length allowed to
599 #     exceed the lines per second limit);
600 #   RFC 1459 describes these parameters to be 0.5 and 5 for the IRC protocol.
601 #   If the $nothrottle flag is set while sending, the line is sent anyway even
602 #   if flooding would take place.
603 sub throttle($$$)
604 {
605         my ($self, $linepersec, $maxlines) = @_;
606         $self->{linepersec} = $linepersec;
607         $self->{maxlines} = $maxlines;
608         $self->{capacity} = $maxlines;
609 }
610
611 sub send($$$)
612 {
613         my ($self, $line, $nothrottle) = @_;
614         my $t = time();
615         if(defined $self->{capacity})
616         {
617                 $self->{capacity} += ($t - $self->{lastsend}) * $self->{linepersec};
618                 $self->{lastsend} = $t;
619                 $self->{capacity} = $self->{maxlines}
620                         if $self->{capacity} > $self->{maxlines};
621                 if(!$nothrottle)
622                 {
623                         return -1
624                                 if $self->{capacity} < 0;
625                 }
626                 $self->{capacity} -= 1;
627         }
628         $line =~ s/\r|\n//g;
629         return $self->{connector}->send("$line\r\n");
630 }
631
632 sub quote($$)
633 {
634         my ($self, $data) = @_;
635         $data =~ s/\r\n?/\n/g;
636         $data =~ s/\n/*/g;
637         return $data;
638 }
639
640 sub recv($)
641 {
642         my ($self) = @_;
643         for(;;)
644         {
645                 my $s = $self->{connector}->recv();
646                 die "read error\n"
647                         if not defined $s;
648                 length $s
649                         or last;
650                 $self->{recvbuf} .= $s;
651         }
652         my @out = ();
653         while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
654         {
655                 push @out, $1;
656         }
657         return @out;
658 }
659
660 sub fds($)
661 {
662         my ($self) = @_;
663         return $self->{connector}->fds();
664 }
665
666
667
668
669
670
671 # main program... a gateway between IRC and DarkPlaces servers
672 package main;
673
674 use strict;
675 use warnings;
676 use IO::Select;
677 use Digest::SHA;
678 use Digest::HMAC;
679 use Time::HiRes qw/time/;
680
681 our @handlers = (); # list of [channel, expression, sub to handle result]
682 our @tasks = (); # list of [time, sub]
683 our %channels = ();
684 our %store = (
685         irc_nick => "",
686         playernick_byid_0 => "(console)",
687 );
688 our %config = (
689         irc_server => undef,
690         irc_nick => undef,
691         irc_nick_alternates => "",
692         irc_user => undef,
693         irc_channel => undef,
694         irc_ping_delay => 120,
695         irc_trigger => "",
696
697         irc_nickserv_password => "",
698         irc_nickserv_identify => 'PRIVMSG NickServ :IDENTIFY %2$s',
699         irc_nickserv_ghost => 'PRIVMSG NickServ :GHOST %1$s %2$s',
700         irc_nickserv_ghost_attempts => 3,
701
702         irc_quakenet_authname => "",
703         irc_quakenet_password => "",
704         irc_quakenet_authusers => "",
705         irc_quakenet_getchallenge => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGE',
706         irc_quakenet_challengeauth => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGEAUTH',
707         irc_quakenet_challengeprefix => ':Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :CHALLENGE',
708
709         irc_announce_slotsfree => 1,
710         irc_announce_mapchange => 'always',
711
712         dp_server => undef,
713         dp_secure => 1,
714         dp_secure_challengetimeout => 1,
715         dp_listen => "", 
716         dp_password => undef,
717         dp_status_delay => 30,
718         dp_server_from_wan => "",
719         irc_local => "",
720
721         irc_admin_password => "",
722         irc_admin_timeout => 3600,
723         irc_admin_quote_re => "",
724
725         irc_reconnect_delay => 300,
726
727         plugins => "",
728 );
729
730
731
732 # Xonotic specific parsing of some server messages
733
734 sub xon_slotsstring()
735 {
736         my $slotsstr = "";
737         if(defined $store{slots_max})
738         {
739                 my $slots = $store{slots_max} - $store{slots_active};
740                 my $slots_s = ($slots == 1) ? '' : 's';
741                 $slotsstr = " ($slots free slot$slots_s)";
742                 my $s = $config{dp_server_from_wan} || $config{dp_server};
743                 $slotsstr .= "; join now: \002xonotic +connect $s"
744                         if $slots >= 1 and not $store{lms_blocked};
745         }
746         return $slotsstr;
747 }
748
749
750
751 # Do we have a config file? If yes, read and parse it (syntax: key = value
752 # pairs, separated by newlines), if not, complain.
753 die "Usage: $0 configfile\n"
754         unless @ARGV == 1;
755
756 open my $fh, "<", $ARGV[0]
757         or die "open $ARGV[0]: $!";
758 while(<$fh>)
759 {
760         chomp;
761         /^#/ and next;
762         /^(.*?)\s*=(?:\s*(.*))?$/ or next;
763         warn "Undefined config item: $1"
764                 unless exists $config{$1};
765         $config{$1} = defined $2 ? $2 : "";
766 }
767 close $fh;
768 my @missing = grep { !defined $config{$_} } keys %config;
769 die "The following config items are missing: @missing"
770         if @missing;
771
772
773
774 # Create a channel for error messages and other internal status messages...
775
776 $channels{system} = new Channel::FIFO();
777
778 # for example, quit messages caused by signals (if SIGTERM or SIGINT is first
779 # received, try to shut down cleanly, and if such a signal is received a second
780 # time, just exit)
781 my $quitting = 0;
782 $SIG{INT} = sub {
783         exit 1 if $quitting++;
784         $channels{system}->send("quit SIGINT");
785 };
786 $SIG{TERM} = sub {
787         exit 1 if $quitting++;
788         $channels{system}->send("quit SIGTERM");
789 };
790
791
792
793 # Create the two channels to gateway between...
794
795 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => $config{irc_local} => $config{irc_server} => 6667));
796 $channels{dp} = new Channel::QW(my $dpsock = new Connection::Socket(udp => $config{dp_listen} => $config{dp_server} => 26000), $config{dp_password}, $config{dp_secure}, $config{dp_secure_challengetimeout});
797 $config{dp_listen} = $dpsock->sockname();
798 print "Listening on $config{dp_listen}\n";
799
800 $channels{irc}->throttle(0.5, 5);
801
802
803 # Utility routine to write to a channel by name, also outputting what's been written and some status
804 sub out($$@)
805 {
806         my $chanstr = shift;
807         my $nothrottle = shift;
808         my $chan = $channels{$chanstr};
809         if(!$chan)
810         {
811                 print "UNDEFINED: $chanstr, ignoring message\n";
812                 return;
813         }
814         @_ = $chan->join_commands(@_);
815         for(@_)
816         {
817                 my $result = $chan->send($_, $nothrottle);
818                 if($result > 0)
819                 {
820                         print "           $chanstr << $_\n";
821                 }
822                 elsif($result < 0)
823                 {
824                         print "FLOOD:     $chanstr << $_\n";
825                 }
826                 else
827                 {
828                         print "ERROR:     $chanstr << $_\n";
829                         $channels{system}->send("error $chanstr", 0);
830                 }
831         }
832 }
833
834
835
836 # Schedule a task for later execution by the main loop; usage: schedule sub {
837 # task... }, $time; When a scheduled task is run, a reference to the task's own
838 # sub is passed as first argument; that way, the task is able to re-schedule
839 # itself so it gets periodically executed.
840 sub schedule($$)
841 {
842         my ($sub, $time) = @_;
843         push @tasks, [time() + $time, $sub];
844 }
845
846 # On IRC error, delete some data store variables of the connection, and
847 # reconnect to the IRC server soon (but only if someone is actually playing)
848 sub irc_error()
849 {
850         # prevent multiple instances of this timer
851         return if $store{irc_error_active};
852         $store{irc_error_active} = 1;
853
854         delete $channels{irc};
855         schedule sub {
856                 my ($timer) = @_;
857                 if(!defined $store{slots_active})
858                 {
859                         # DP is not running, then delay IRC reconnecting
860                         #use Data::Dumper; print Dumper \$timer;
861                         schedule $timer => 1;
862                         return;
863                         # this will keep irc_error_active
864                 }
865                 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => "" => $config{irc_server} => 6667));
866                 delete $store{$_} for grep { /^irc_/ } keys %store;
867                 $store{irc_nick} = "";
868                 schedule sub {
869                         my ($timer) = @_;
870                         out dp => 0, 'sv_cmd bans', 'status 1', 'log_dest_udp';
871                         $store{status_waiting} = -1;
872                 } => 1;
873                 # this will clear irc_error_active
874         } => $config{irc_reconnect_delay};
875         return 0;
876 }
877
878 sub uniq(@)
879 {
880         my @out = ();
881         my %found = ();
882         for(@_)
883         {
884                 next if $found{$_}++;
885                 push @out, $_;
886         }
887         return @out;
888 }
889
890 # IRC joining (if this is called as response to a nick name collision, $is433 is set);
891 # among other stuff, it performs NickServ or Quakenet authentication. This is to be called
892 # until the channel has been joined for every message that may be "interesting" (basically,
893 # IRC 001 hello messages, 443 nick collision messages and some notices by services).
894 sub irc_joinstage($)
895 {
896         my($is433) = @_;
897
898         return 0
899                 if $store{irc_joined_channel};
900         
901                 #use Data::Dumper; print Dumper \%store;
902
903         if($is433)
904         {
905                 if(length $store{irc_nick})
906                 {
907                         # we already have another nick, but couldn't change to the new one
908                         # try ghosting and then get the nick again
909                         if(length $config{irc_nickserv_password})
910                         {
911                                 if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
912                                 {
913                                         $store{irc_nick_requested} = $config{irc_nick};
914                                         out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
915                                         schedule sub {
916                                                 out irc => 1, "NICK $config{irc_nick}";
917                                         } => 1;
918                                         return; # we'll get here again for the NICK success message, or for a 433 failure
919                                 }
920                                 # otherwise, we failed to ghost and will continue with the wrong
921                                 # nick... also, no need to try to identify here
922                         }
923                         # otherwise, we can't handle this and will continue with our wrong nick
924                 }
925                 else
926                 {
927                         # we failed to get an initial nickname
928                         # change ours a bit and try again
929
930                         my @alternates = uniq ($config{irc_nick}, grep { $_ ne "" } split /\s+/, $config{irc_nick_alternates});
931                         my $nextnick = undef;
932                         for(0..@alternates-2)
933                         {
934                                 if($store{irc_nick_requested} eq $alternates[$_])
935                                 {
936                                         $nextnick = $alternates[$_+1];
937                                 }
938                         }
939                         if($store{irc_nick_requested} eq $alternates[@alternates-1]) # this will only happen once
940                         {
941                                 $store{irc_nick_requested} = $alternates[0];
942                                 # but don't set nextnick, so we edit it
943                         }
944                         if(defined $nextnick)
945                         {
946                                 $store{irc_nick_requested} = $nextnick;
947                         }
948                         else
949                         {
950                                 for(;;)
951                                 {
952                                         if(length $store{irc_nick_requested} < 9)
953                                         {
954                                                 $store{irc_nick_requested} .= '_';
955                                         }
956                                         else
957                                         {
958                                                 substr $store{irc_nick_requested}, int(rand length $store{irc_nick_requested}), 1, chr(97 + int rand 26);
959                                         }
960                                         last unless grep { $_ eq $store{irc_nick_requested} } @alternates;
961                                 }
962                         }
963                         out irc => 1, "NICK $store{irc_nick_requested}";
964                         return; # when it fails, we'll get here again, and when it succeeds, we will continue
965                 }
966         }
967
968         # we got a 001 or a NICK message, so $store{irc_nick} has been updated
969         if(length $config{irc_nickserv_password})
970         {
971                 if($store{irc_nick} eq $config{irc_nick})
972                 {
973                         # identify
974                         out irc => 1, sprintf($config{irc_nickserv_identify}, $config{irc_nick}, $config{irc_nickserv_password});
975                 }
976                 else
977                 {
978                         # ghost
979                         if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
980                         {
981                                 $store{irc_nick_requested} = $config{irc_nick};
982                                 out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
983                                 schedule sub {
984                                         out irc => 1, "NICK $config{irc_nick}";
985                                 } => 1;
986                                 return; # we'll get here again for the NICK success message, or for a 433 failure
987                         }
988                         # otherwise, we failed to ghost and will continue with the wrong
989                         # nick... also, no need to try to identify here
990                 }
991         }
992
993         # we are on Quakenet. Try to authenticate.
994         if(length $config{irc_quakenet_password} and length $config{irc_quakenet_authname})
995         {
996                 if(defined $store{irc_quakenet_challenge})
997                 {
998                         if($store{irc_quakenet_challenge} =~ /^([0-9a-f]*)\b.*\bHMAC-SHA-256\b/)
999                         {
1000                                 my $challenge = $1;
1001                                 my $hash1 = Digest::SHA::sha256_hex(substr $config{irc_quakenet_password}, 0, 10);
1002                                 my $key = Digest::SHA::sha256_hex("@{[lc $config{irc_quakenet_authname}]}:$hash1");
1003                                 my $digest = Digest::HMAC::hmac_hex($challenge, $key, \&Digest::SHA::sha256);
1004                                 out irc => 1, "$config{irc_quakenet_challengeauth} $config{irc_quakenet_authname} $digest HMAC-SHA-256";
1005                         }
1006                 }
1007                 else
1008                 {
1009                         out irc => 1, $config{irc_quakenet_getchallenge};
1010                         return;
1011                         # we get here again when Q asks us
1012                 }
1013         }
1014         
1015         # if we get here, we are on IRC
1016         $store{irc_joined_channel} = 1;
1017         schedule sub {
1018                 out irc => 1, "JOIN $config{irc_channel}";
1019         } => 1;
1020         return 0;
1021 }
1022
1023 my $RE_FAIL = qr/$ $/;
1024 my $RE_SUCCEED = qr//;
1025 sub cond($)
1026 {
1027         return $_[0] ? $RE_FAIL : $RE_SUCCEED;
1028 }
1029
1030
1031 # List of all handlers on the various sockets. Additional handlers can be added by a plugin.
1032 @handlers = (
1033         # detect a server restart and set it up again
1034         [ dp => q{ *(?:Warning: Could not expand \$|Unknown command ")(?:rcon2irc_[a-z0-9_]*)[" ]*} => sub {
1035                 out dp => 0,
1036                         'alias rcon2irc_eval "$*"',
1037                         'log_dest_udp',
1038                         'sv_logscores_console 0',
1039                         'sv_logscores_bots 1',
1040                         'sv_eventlog 1',
1041                         'sv_eventlog_console 1',
1042                         'alias rcon2irc_say_as "set say_as_restorenick \"$sv_adminnick\"; sv_adminnick \"$1^3\"; say \"^7$2\"; rcon2irc_say_as_restore"',
1043                         'alias rcon2irc_say_as_restore "set sv_adminnick \"$say_as_restorenick\""',
1044                         'alias rcon2irc_quit "echo \"quitting rcon2irc $1: log_dest_udp is $log_dest_udp\""'; # note: \\\\\\" ->perl \\\" ->console \"
1045                 return 0;
1046         } ],
1047
1048         # detect missing entry in log_dest_udp and fix it
1049         [ dp => q{"log_dest_udp" is "([^"]*)" \["[^"]*"\]} => sub {
1050                 my ($dest) = @_;
1051                 my @dests = split ' ', $dest;
1052                 return 0 if grep { $_ eq $config{dp_listen} } @dests;
1053                 out dp => 0, 'log_dest_udp "' . join(" ", @dests, $config{dp_listen}) . '"';
1054                 return 0;
1055         } ],
1056
1057         # retrieve list of banned hosts
1058         [ dp => q{#(\d+): (\S+) is still banned for (\S+) seconds} => sub {
1059                 return 0 unless $store{status_waiting} < 0;
1060                 my ($id, $ip, $time) = @_;
1061                 $store{bans_new} = [] if $id == 0;
1062                 $store{bans_new}[$id] = { ip => $ip, 'time' => $time };
1063                 return 0;
1064         } ],
1065
1066         # retrieve hostname from status replies
1067         [ dp => q{host:     (.*)} => sub {
1068                 return 0 unless $store{status_waiting} < 0;
1069                 my ($name) = @_;
1070                 $store{dp_hostname} = $name;
1071                 $store{bans} = $store{bans_new};
1072                 return 0;
1073         } ],
1074
1075         # retrieve version from status replies
1076         [ dp => q{version:  (.*)} => sub {
1077                 return 0 unless $store{status_waiting} < 0;
1078                 my ($version) = @_;
1079                 $store{dp_version} = $version;
1080                 return 0;
1081         } ],
1082
1083         # retrieve player names
1084         [ dp => q{players:  (\d+) active \((\d+) max\)} => sub {
1085                 return 0 unless $store{status_waiting} < 0;
1086                 my ($active, $max) = @_;
1087                 my $full = ($active >= $max);
1088                 $store{slots_max} = $max;
1089                 $store{slots_active} = $active;
1090                 $store{status_waiting} = $active;
1091                 $store{playerslots_active_new} = [];
1092                 if($store{status_waiting} == 0)
1093                 {
1094                         $store{playerslots_active} = $store{playerslots_active_new};
1095                 }
1096                 if($full != ($store{slots_full} || 0))
1097                 {
1098                         $store{slots_full} = $full;
1099                         return 0 if $store{lms_blocked};
1100                         return 0 if !$config{irc_announce_slotsfree};
1101                         if($full)
1102                         {
1103                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION is full!\001";
1104                         }
1105                         else
1106                         {
1107                                 my $slotsstr = xon_slotsstring();
1108                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can be joined again$slotsstr!\001";
1109                         }
1110                 }
1111                 return 0;
1112         } ],
1113
1114         # retrieve player names
1115         [ dp => q{\^\d(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(-?\d+)\s+\#(\d+)\s+\^\d(.*)} => sub {
1116                 return 0 unless $store{status_waiting} > 0;
1117                 my ($ip, $pl, $ping, $time, $frags, $no, $name) = ($1, $2, $3, $4, $5, $6, $7);
1118                 $store{"playerslot_$no"} = { ip => $ip, pl => $pl, ping => $ping, 'time' => $time, frags => $frags, no => $no, name => $name };
1119                 push @{$store{playerslots_active_new}}, $no;
1120                 if(--$store{status_waiting} == 0)
1121                 {
1122                         $store{playerslots_active} = $store{playerslots_active_new};
1123                 }
1124                 return 0;
1125         } ],
1126
1127         # IRC admin commands
1128         [ irc => q{:(([^! ]*)![^ ]*) (?i:PRIVMSG) [^&#%]\S* :(.*)} => sub {
1129                 return 0 unless ($config{irc_admin_password} ne '' || $store{irc_quakenet_users});
1130
1131                 my ($hostmask, $nick, $command) = @_;
1132                 my $dpnick = color_dpfix $nick;
1133
1134                 if($command eq "login $config{irc_admin_password}")
1135                 {
1136                         $store{logins}{$hostmask} = time() + $config{irc_admin_timeout};
1137                         out irc => 0, "PRIVMSG $nick :my wish is your command";
1138                         return -1;
1139                 }
1140
1141                 if($command =~ /^login /)
1142                 {
1143                         out irc => 0, "PRIVMSG $nick :invalid password";
1144                         return -1;
1145                 }
1146
1147                 if(($store{logins}{$hostmask} || 0) < time())
1148                 {
1149                         out irc => 0, "PRIVMSG $nick :authentication required";
1150                         return -1;
1151                 }
1152
1153                 if($command =~ /^status(?: (.*))?$/)
1154                 {
1155                         my ($match) = $1;
1156                         my $found = 0;
1157                         my $foundany = 0;
1158                         for my $slot(@{$store{playerslots_active} || []})
1159                         {
1160                                 my $s = $store{"playerslot_$slot"};
1161                                 next unless $s;
1162                                 if(not defined $match or index(color_dp2none($s->{name}), $match) >= 0)
1163                                 {
1164                                         out irc => 0, sprintf 'PRIVMSG %s :%-21s %2i %4i %8s %4i #%-3u %s', $nick, $s->{ip}, $s->{pl}, $s->{ping}, $s->{time}, $s->{frags}, $slot, color_dp2irc $s->{name};
1165                                         ++$found;
1166                                 }
1167                                 ++$foundany;
1168                         }
1169                         if(!$found)
1170                         {
1171                                 if(!$foundany)
1172                                 {
1173                                         out irc => 0, "PRIVMSG $nick :the server is empty";
1174                                 }
1175                                 else
1176                                 {
1177                                         out irc => 0, "PRIVMSG $nick :no nicknames match";
1178                                 }
1179                         }
1180                         return 0;
1181                 }
1182
1183                 if($command =~ /^kick # (\d+) (.*)$/)
1184                 {
1185                         my ($id, $reason) = ($1, $2);
1186                         my $dpreason = color_irc2dp $reason;
1187                         $dpreason =~ s/^(~?)(.*)/$1irc $dpnick: $2/g;
1188                         $dpreason =~ s/(["\\])/\\$1/g;
1189                         out dp => 0, "kick # $id $dpreason";
1190                         my $slotnik = "playerslot_$id";
1191                         out irc => 0, "PRIVMSG $nick :kicked #$id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip}) ($reason)";
1192                         return 0;
1193                 }
1194
1195                 if($command =~ /^kickban # (\d+) (\d+) (\d+) (.*)$/)
1196                 {
1197                         my ($id, $bantime, $mask, $reason) = ($1, $2, $3, $4);
1198                         my $dpreason = color_irc2dp $reason;
1199                         $dpreason =~ s/^(~?)(.*)/$1irc $dpnick: $2/g;
1200                         $dpreason =~ s/(["\\])/\\$1/g;
1201                         out dp => 0, "kickban # $id $bantime $mask $dpreason";
1202                         my $slotnik = "playerslot_$id";
1203                         out irc => 0, "PRIVMSG $nick :kickbanned #$id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip}), netmask $mask, for $bantime seconds ($reason)";
1204                         return 0;
1205                 }
1206
1207                 if($command eq "bans")
1208                 {
1209                         my $banlist =
1210                                 join ", ",
1211                                 map { "$_ ($store{bans}[$_]{ip}, $store{bans}[$_]{time}s)" }
1212                                 0..@{$store{bans} || []}-1;
1213                         $banlist = "no bans"
1214                                 if $banlist eq "";
1215                         out irc => 0, "PRIVMSG $nick :$banlist";
1216                         return 0;
1217                 }
1218
1219                 if($command =~ /^unban (\d+)$/)
1220                 {
1221                         my ($id) = ($1);
1222                         out dp => 0, "unban $id";
1223                         out irc => 0, "PRIVMSG $nick :removed ban $id ($store{bans}[$id]{ip})";
1224                         return 0;
1225                 }
1226
1227                 if($command =~ /^mute (\d+)$/)
1228                 {
1229                         my $id = $1;
1230                         out dp => 0, "mute $id";
1231                         my $slotnik = "playerslot_$id";
1232                         out irc => 0, "PRIVMSG $nick :muted $id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip})";
1233                         return 0;
1234                 }
1235
1236                 if($command =~ /^unmute (\d+)$/)
1237                 {
1238                         my ($id) = ($1);
1239                         out dp => 0, "unmute $id";
1240                         my $slotnik = "playerslot_$id";
1241                         out irc => 0, "PRIVMSG $nick :unmuted $id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip})";
1242                         return 0;
1243                 }
1244
1245                 if($command =~ /^quote (.*)$/)
1246                 {
1247                         my ($cmd) = ($1);
1248                         if($cmd =~ /^(??{$config{irc_admin_quote_re}})$/si)
1249                         {
1250                                 out irc => 0, $cmd;
1251                                 out irc => 0, "PRIVMSG $nick :executed your command";
1252                         }
1253                         else
1254                         {
1255                                 out irc => 0, "PRIVMSG $nick :permission denied";
1256                         }
1257                         return 0;
1258                 }
1259
1260                 out irc => 0, "PRIVMSG $nick :unknown command (supported: status [substring], kick # id reason, kickban # id bantime mask reason, bans, unban banid, mute id, unmute id)";
1261
1262                 return -1;
1263         } ],
1264
1265         # LMS: detect "no more lives" message
1266         [ dp => q{\^4.*\^4 has no more lives left} => sub {
1267                 if(!$store{lms_blocked})
1268                 {
1269                         $store{lms_blocked} = 1;
1270                         if(!$store{slots_full})
1271                         {
1272                                 schedule sub {
1273                                         if($store{lms_blocked})
1274                                         {
1275                                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can't be joined until next round (a player has no more lives left)\001";
1276                                         }
1277                                 } => 1;
1278                         }
1279                 }
1280         } ],
1281
1282         # detect IRC errors and reconnect
1283         [ irc => q{ERROR .*} => \&irc_error ],
1284         [ irc => q{:[^ ]* 404 .*} => \&irc_error ], # cannot send to channel
1285         [ system => q{error irc} => \&irc_error ],
1286
1287         # IRC nick in use
1288         [ irc => q{:[^ ]* 433 .*} => sub {
1289                 return irc_joinstage(433);
1290         } ],
1291
1292         # IRC welcome
1293         [ irc => q{:[^ ]* 001 .*} => sub {
1294                 $store{irc_seen_welcome} = 1;
1295                 $store{irc_nick} = $store{irc_nick_requested};
1296                 
1297                 # If users for quakenet are listed, parse them into a hash and schedule a sub to query information
1298                 if ($config{irc_quakenet_authusers} ne '') {
1299                         $store{irc_quakenet_users} = { map { $_ => 1 } split / /, $config{irc_quakenet_authusers} };
1300         
1301                         schedule sub {
1302                                 my ($timer) = @_;
1303                                 out irc => 0, "PRIVMSG Q :users " . $config{irc_channel};
1304                                 schedule $timer => 300;;
1305                         } => 1;
1306                 }
1307
1308                 return irc_joinstage(0);
1309         } ],
1310
1311         # IRC my nickname changed
1312         [ irc => q{:(?i:(??{$store{irc_nick}}))![^ ]* (?i:NICK) :(.*)} => sub {
1313                 my ($n) = @_;
1314                 $store{irc_nick} = $n;
1315                 return irc_joinstage(0);
1316         } ],
1317
1318         # Quakenet: challenge from Q
1319         [ irc => q{(??{$config{irc_quakenet_challengeprefix}}) (.*)} => sub {
1320                 $store{irc_quakenet_challenge} = $1;
1321                 return irc_joinstage(0);
1322         } ],
1323         
1324         # Catch joins of people in a channel the bot is in and catch our own joins of a channel,
1325         # detect channel join message and note hostname length to get the maximum allowed line length
1326         [ irc => q{:(([^! ]*)![^ ]*) JOIN (#.+)} => sub {
1327                 my ($hostmask, $nick, $chan) = @_;
1328
1329                 if ($nick eq $store{irc_nick}) {
1330                         $store{irc_maxlen} = 510 - length($hostmask);
1331                         $store{irc_joined_channel} = 1;
1332                         print "* detected maximum line length for channel messages: $store{irc_maxlen}\n";
1333                 }
1334
1335                 return 0 unless ($store{irc_quakenet_users});
1336                 
1337                 if ($nick eq $store{irc_nick}) {
1338                         out irc => 0, "PRIVMSG Q :users $chan"; # get auths for all users
1339                 } else {
1340                         $store{quakenet_hosts}->{$nick} = $hostmask;
1341                         out irc => 0, "PRIVMSG Q :whois $nick"; # get auth for single user
1342                 }
1343                 
1344                 return 0;
1345         } ],
1346         
1347         # Catch response of users request
1348         [ irc => q{:Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :[@\+\s]?(\S+)\s+(\S+)\s*(\S*)\s*\((.*)\)} => sub {
1349                 my ($nick, $username, $flags, $host) = @_;
1350                 return 0 unless ($store{irc_quakenet_users});
1351                 
1352                 $store{logins}{"$nick!$host"} = time() + 600 if ($store{irc_quakenet_users}->{$username});
1353                 
1354                 return 0;
1355         } ],
1356         
1357         # Catch response of whois request
1358         [ irc => q{:Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :-Information for user (.*) \(using account (.*)\):} => sub {
1359                 my ($nick, $username) = @_;
1360                 return 0 unless ($store{irc_quakenet_users});
1361                 
1362                 if ($store{irc_quakenet_users}->{$username}) {
1363                         my $hostmask = $store{quakenet_hosts}->{$nick};
1364                         $store{logins}{$hostmask} = time() + 600;
1365                 }
1366                 
1367                 return 0;
1368         } ],
1369
1370         # shut down everything on SIGINT
1371         [ system => q{quit (.*)} => sub {
1372                 my ($cause) = @_;
1373                 out irc => 1, "QUIT :$cause";
1374                 $store{quitcookie} = int rand 1000000000;
1375                 out dp => 0, "rcon2irc_quit $store{quitcookie}";
1376         } ],
1377
1378         # remove myself from the log destinations and exit everything
1379         [ dp => q{quitting rcon2irc (??{$store{quitcookie}}): log_dest_udp is (.*) *} => sub {
1380                 my ($dest) = @_;
1381                 my @dests = grep { $_ ne $config{dp_listen} } split ' ', $dest;
1382                 out dp => 0, 'log_dest_udp "' . join(" ", @dests) . '"';
1383                 exit 0;
1384                 return 0;
1385         } ],
1386
1387         # IRC PING
1388         [ irc => q{PING (.*)} => sub {
1389                 my ($data) = @_;
1390                 out irc => 1, "PONG $data";
1391                 return 1;
1392         } ],
1393
1394         # IRC PONG
1395         [ irc => q{:[^ ]* PONG .* :(.*)} => sub {
1396                 my ($data) = @_;
1397                 return 0
1398                         if not defined $store{irc_pingtime};
1399                 return 0
1400                         if $data ne $store{irc_pingtime};
1401                 print "* measured IRC line delay: @{[time() - $store{irc_pingtime}]}\n";
1402                 undef $store{irc_pingtime};
1403                 return 0;
1404         } ],
1405
1406         # chat: Xonotic server -> IRC channel
1407         [ dp => q{\001(.*?)\^7: (.*)} => sub {
1408                 my ($nick, $message) = map { color_dp2irc $_ } @_;
1409                 out irc => 0, "PRIVMSG $config{irc_channel} :<$nick\017> $message";
1410                 return 0;
1411         } ],
1412
1413         # chat: Xonotic server -> IRC channel, nick set
1414         [ dp => q{:join:(\d+):(\d+):([^:]*):(.*)} => sub {
1415                 my ($id, $slot, $ip, $nick) = @_;
1416                 $store{"playernickraw_byid_$id"} = $nick;
1417                 $nick = color_dp2irc $nick;
1418                 $store{"playernick_byid_$id"} = $nick;
1419                 $store{"playerip_byid_$id"} = $ip;
1420                 $store{"playerslot_byid_$id"} = $slot;
1421                 $store{"playerid_byslot_$slot"} = $id;
1422                 return 0;
1423         } ],
1424
1425         # chat: Xonotic server -> IRC channel, nick change/set
1426         [ dp => q{:name:(\d+):(.*)} => sub {
1427                 my ($id, $nick) = @_;
1428                 $store{"playernickraw_byid_$id"} = $nick;
1429                 $nick = color_dp2irc $nick;
1430                 my $oldnick = $store{"playernick_byid_$id"};
1431                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 is now known as $nick";
1432                 $store{"playernick_byid_$id"} = $nick;
1433                 return 0;
1434         } ],
1435
1436         # chat: Xonotic server -> IRC channel, vote call
1437         [ dp => q{:vote:vcall:(\d+):(.*)} => sub {
1438                 my ($id, $command) = @_;
1439                 $command = color_dp2irc $command;
1440                 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1441                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 calls a vote for \"$command\017\"";
1442                 return 0;
1443         } ],
1444
1445         # chat: Xonotic server -> IRC channel, vote stop
1446         [ dp => q{:vote:vstop:(\d+)} => sub {
1447                 my ($id) = @_;
1448                 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1449                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 stopped the vote";
1450                 return 0;
1451         } ],
1452
1453         # chat: Xonotic server -> IRC channel, master login
1454         [ dp => q{:vote:vlogin:(\d+)} => sub {
1455                 my ($id) = @_;
1456                 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1457                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 logged in as master";
1458                 return 0;
1459         } ],
1460
1461         # chat: Xonotic server -> IRC channel, master do
1462         [ dp => q{:vote:vdo:(\d+):(.*)} => sub {
1463                 my ($id, $command) = @_;
1464                 $command = color_dp2irc $command;
1465                 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1466                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 used his master status to do \"$command\017\"";
1467                 return 0;
1468         } ],
1469
1470         # chat: Xonotic server -> IRC channel, result
1471         [ dp => q{:vote:v(yes|no|timeout):(\d+):(\d+):(\d+):(\d+):(-?\d+)} => sub {
1472                 my ($result, $yes, $no, $abstain, $not, $min) = @_;
1473                 my $spam = "$yes:$no" . (($min >= 0) ? " ($min needed)" : "") . ", $abstain didn't care, $not didn't vote";
1474                 out irc => 0, "PRIVMSG $config{irc_channel} :* the vote ended with $result: $spam";
1475                 return 0;
1476         } ],
1477
1478         # chat: IRC channel -> Xonotic server
1479         [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$store{irc_nick}}))(?: |: ?|, ?)(.*)} => sub {
1480                 my ($nick, $message) = @_;
1481                 $nick = color_dpfix $nick;
1482                         # allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
1483                 $message = color_irc2dp $message;
1484                 $message =~ s/(["\\])/\\$1/g;
1485                 out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
1486                 return 0;
1487         } ],
1488
1489         (
1490                 length $config{irc_trigger}
1491                         ?
1492                                 [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$config{irc_trigger}}))(?: |: ?|, ?)(.*)} => sub {
1493                                         my ($nick, $message) = @_;
1494                                         $nick = color_dpfix $nick;
1495                                                 # allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
1496                                         $message = color_irc2dp $message;
1497                                         $message =~ s/(["\\])/\\$1/g;
1498                                         out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
1499                                         return 0;
1500                                 } ]
1501                         :
1502                                 ()
1503         ),
1504
1505         # irc: CTCP VERSION reply
1506         [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$store{irc_nick}})) :\001VERSION( .*)?\001} => sub {
1507                 my ($nick) = @_;
1508                 my $ver = $store{dp_version} or return 0;
1509                 $ver .= ", rcon2irc $VERSION";
1510                 out irc => 0, "NOTICE $nick :\001VERSION $ver\001";
1511         } ],
1512
1513         # on game start, notify the channel
1514         [ dp => q{:gamestart:(.*):[0-9.]*} => sub {
1515                 my ($map) = @_;
1516                 $store{playing} = 1;
1517                 $store{map} = $map;
1518                 $store{map_starttime} = time();
1519                 if ($config{irc_announce_mapchange} eq 'always' || ($config{irc_announce_mapchange} eq 'notempty' && $store{slots_active} > 0)) {
1520                         my $slotsstr = xon_slotsstring();
1521                         out irc => 0, "PRIVMSG $config{irc_channel} :\00304" . $map . "\017 has begun$slotsstr";
1522                 }
1523                 delete $store{lms_blocked};
1524                 return 0;
1525         } ],
1526
1527         # on game over, clear the current map
1528         [ dp => q{:gameover} => sub {
1529                 $store{playing} = 0;
1530                 return 0;
1531         } ],
1532
1533         # scores: Xonotic server -> IRC channel (start)
1534         [ dp => q{:scores:(.*):(\d+)} => sub {
1535                 my ($map, $time) = @_;
1536                 $store{scores} = {};
1537                 $store{scores}{map} = $map;
1538                 $store{scores}{time} = $time;
1539                 $store{scores}{players} = [];
1540                 delete $store{lms_blocked};
1541                 return 0;
1542         } ],
1543
1544         # scores: Xonotic server -> IRC channel, legacy format
1545         [ dp => q{:player:(-?\d+):(\d+):(\d+):(\d+):(\d+):(.*)} => sub {
1546                 my ($frags, $deaths, $time, $team, $id, $name) = @_;
1547                 return if not exists $store{scores};
1548                 push @{$store{scores}{players}}, [$frags, $team, $name]
1549                         unless $frags <= -666; # no spectators
1550                 return 0;
1551         } ],
1552
1553         # scores: Xonotic server -> IRC channel (CTF), legacy format
1554         [ dp => q{:teamscores:(\d+:-?\d*(?::\d+:-?\d*)*)} => sub {
1555                 my ($teams) = @_;
1556                 return if not exists $store{scores};
1557                 $store{scores}{teams} = {split /:/, $teams};
1558                 return 0;
1559         } ],
1560
1561         # scores: Xonotic server -> IRC channel, new format
1562         [ dp => q{:player:see-labels:(-?\d+)[-0-9,]*:(\d+):(\d+):(\d+):(.*)} => sub {
1563                 my ($frags, $time, $team, $id, $name) = @_;
1564                 return if not exists $store{scores};
1565                 push @{$store{scores}{players}}, [$frags, $team, $name];
1566                 return 0;
1567         } ],
1568
1569         # scores: Xonotic server -> IRC channel (CTF), new format
1570         [ dp => q{:teamscores:see-labels:(-?\d+)[-0-9,]*:(\d+)} => sub {
1571                 my ($frags, $team) = @_;
1572                 return if not exists $store{scores};
1573                 $store{scores}{teams}{$team} = $frags;
1574                 return 0;
1575         } ],
1576
1577         # scores: Xonotic server -> IRC channel
1578         [ dp => q{:end} => sub {
1579                 return if not exists $store{scores};
1580                 my $s = $store{scores};
1581                 delete $store{scores};
1582                 my $teams_matter = defined $s->{teams};
1583
1584                 my @t = ();
1585                 my @p = ();
1586
1587                 if($teams_matter)
1588                 {
1589                         # put players into teams
1590                         my %t = ();
1591                         for(@{$s->{players}})
1592                         {
1593                                 my $thisteam = ($t{$_->[1]} ||= {score => 0, team => $_->[1], players => []});
1594                                 push @{$thisteam->{players}}, [$_->[0], $_->[1], $_->[2]];
1595                                 if($s->{teams})
1596                                 {
1597                                         $thisteam->{score} = $s->{teams}{$_->[1]};
1598                                 }
1599                                 else
1600                                 {
1601                                         $thisteam->{score} += $_->[0];
1602                                 }
1603                         }
1604
1605                         # sort by team score
1606                         @t = sort { $b->{score} <=> $a->{score} } values %t;
1607
1608                         # sort by player score
1609                         @p = ();
1610                         for(@t)
1611                         {
1612                                 @{$_->{players}} = sort { $b->[0] <=> $a->[0] } @{$_->{players}};
1613                                 push @p, @{$_->{players}};
1614                         }
1615                 }
1616                 else
1617                 {
1618                         @p = sort { $b->[0] <=> $a->[0] } @{$s->{players}};
1619                 }
1620
1621                 # no display for empty server
1622                 return 0
1623                         if !@p;
1624
1625                 # make message fit somehow
1626                 for my $maxnamelen(reverse 3..64)
1627                 {
1628                         my $scores_string = "PRIVMSG $config{irc_channel} :\00304" . $s->{map} . "\017 ended:";
1629                         if($teams_matter)
1630                         {
1631                                 my $sep = ' ';
1632                                 for(@t)
1633                                 {
1634                                         $scores_string .= $sep . "\003" . $color_team2irc_table{$_->{team}}. "\002\002" . $_->{score} . "\017";
1635                                         $sep = ':';
1636                                 }
1637                         }
1638                         my $sep = '';
1639                         for(@p)
1640                         {
1641                                 my ($frags, $team, $name) = @$_;
1642                                 $name = color_dpfix substr($name, 0, $maxnamelen);
1643                                 if($teams_matter)
1644                                 {
1645                                         $name = "\003" . $color_team2irc_table{$team} . " " . color_dp2none $name;
1646                                 }
1647                                 else
1648                                 {
1649                                         $name = " " . color_dp2irc $name;
1650                                 }
1651                                 $scores_string .= "$sep$name\017 $frags";
1652                                 $sep = ',';
1653                         }
1654                         if(length($scores_string) <= ($store{irc_maxlen} || 256))
1655                         {
1656                                 out irc => 0, $scores_string;
1657                                 return 0;
1658                         }
1659                 }
1660                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION would have LIKED to put the scores here, but they wouldn't fit :(\001";
1661                 return 0;
1662         } ],
1663
1664         # complain when system load gets too high
1665         [ dp => q{timing:   (([0-9.]*)% CPU, ([0-9.]*)% lost, offset avg ([0-9.]*)ms, max ([0-9.]*)ms, sdev ([0-9.]*)ms)} => sub {
1666                 my ($all, $cpu, $lost, $avg, $max, $sdev) = @_;
1667                 return 0 # don't complain when just on the voting screen
1668                         if !$store{playing};
1669                 return 0 # don't complain if it was less than 0.5%
1670                         if $lost < 0.5;
1671                 return 0 # don't complain if nobody is looking
1672                         if $store{slots_active} == 0;
1673                 return 0 # don't complain in the first two minutes
1674                         if time() - $store{map_starttime} < 120;
1675                 return 0 # don't complain if it was already at least half as bad in this round
1676                         if $store{map_starttime} == $store{timingerror_map_starttime} and $lost <= 2 * $store{timingerror_lost};
1677                 $store{timingerror_map_starttime} = $store{map_starttime};
1678                 $store{timingerror_lost} = $lost;
1679                 out dp => 0, 'rcon2irc_say_as server "There are currently some severe system load problems. The admins have been notified."';
1680                 out irc => 1, "PRIVMSG $config{irc_channel} :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1681                 #out irc => 1, "PRIVMSG OpBaI :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1682                 return 0;
1683         } ],
1684 );
1685
1686
1687
1688 # Load plugins and add them to the handler list in the front.
1689 for my $p(split ' ', $config{plugins})
1690 {
1691         my @h = eval { do $p; }
1692                 or die "Invalid plugin $p: $@";
1693         for(reverse @h)
1694         {
1695                 ref $_ eq 'ARRAY' or die "Invalid plugin $p: did not return a list of arrays";
1696                 @$_ == 3 or die "Invalid plugin $p: did not return a list of three-element arrays";
1697                 !ref $_->[0] && !ref $_->[1] && ref $_->[2] eq 'CODE' or die "Invalid plugin $p: did not return a list of string-string-sub arrays";
1698                 unshift @handlers, $_;
1699         }
1700 }
1701
1702
1703
1704 # verify that the server is up by letting it echo back a string that causes
1705 # re-initialization of the required aliases
1706 out dp => 0, 'echo "Unknown command \"rcon2irc_eval\""'; # assume the server has been restarted
1707
1708
1709
1710 # regularily, query the server status and if it still is connected to us using
1711 # the log_dest_udp feature. If not, we will detect the response to this rcon
1712 # command and re-initialize the server's connection to us (either by log_dest_udp
1713 # not containing our own IP:port, or by rcon2irc_eval not being a defined command).
1714 schedule sub {
1715         my ($timer) = @_;
1716         out dp => 0, 'sv_cmd bans', 'status 1', 'log_dest_udp', 'rcon2irc_eval set dummy 1';
1717         $store{status_waiting} = -1;
1718         schedule $timer => (exists $store{dp_hostname} ? $config{dp_status_delay} : 1);;
1719 } => 1;
1720
1721
1722
1723 # Continue with connecting to IRC as soon as we get our first status reply from
1724 # the DP server (which contains the server's hostname that we'll use as
1725 # realname for IRC).
1726 schedule sub {
1727         my ($timer) = @_;
1728
1729         # log on to IRC when needed
1730         if(exists $store{dp_hostname} && !exists $store{irc_seen_welcome})
1731         {
1732                 $store{irc_nick_requested} = $config{irc_nick};
1733                 out irc => 1, "NICK $config{irc_nick}", "USER $config{irc_user} localhost localhost :$store{dp_hostname}";
1734                 $store{irc_logged_in} = 1;
1735                 undef $store{irc_maxlen};
1736                 undef $store{irc_pingtime};
1737         }
1738
1739         schedule $timer => 1;;
1740 } => 1;
1741
1742
1743
1744 # Regularily ping the IRC server to detect if the connection is down. If it is,
1745 # schedule an IRC error that will cause reconnection later.
1746 schedule sub {
1747         my ($timer) = @_;
1748
1749         if($store{irc_logged_in})
1750         {
1751                 if(defined $store{irc_pingtime})
1752                 {
1753                         # IRC connection apparently broke
1754                         # so... KILL IT WITH FIRE
1755                         $channels{system}->send("error irc", 0);
1756                 }
1757                 else
1758                 {
1759                         # everything is fine, send a new ping
1760                         $store{irc_pingtime} = time();
1761                         out irc => 1, "PING $store{irc_pingtime}";
1762                 }
1763         }
1764
1765         schedule $timer => $config{irc_ping_delay};;
1766 } => 1;
1767
1768
1769
1770 # Main loop.
1771 for(;;)
1772 {
1773         # Build up an IO::Select object for all our channels.
1774         my $s = IO::Select->new();
1775         for my $chan(values %channels)
1776         {
1777                 $s->add($_) for $chan->fds();
1778         }
1779
1780         # wait for something to happen on our sockets, or wait 2 seconds without anything happening there
1781         $s->can_read(2);
1782         my @errors = $s->has_exception(0);
1783
1784         # on every channel, look for incoming messages
1785         CHANNEL:
1786         for my $chanstr(keys %channels)
1787         {
1788                 my $chan = $channels{$chanstr};
1789                 my @chanfds = $chan->fds();
1790
1791                 for my $chanfd(@chanfds)
1792                 {
1793                         if(grep { $_ == $chanfd } @errors)
1794                         {
1795                                 # STOP! This channel errored!
1796                                 $channels{system}->send("error $chanstr", 0);
1797                                 next CHANNEL;
1798                         }
1799                 }
1800
1801                 eval
1802                 {
1803                         for my $line($chan->recv())
1804                         {
1805                                 # found one! Check if it matches the regular expression of one of
1806                                 # our handlers...
1807                                 my $handled = 0;
1808                                 my $private = 0;
1809                                 for my $h(@handlers)
1810                                 {
1811                                         my ($chanstr_wanted, $re, $sub) = @$h;
1812                                         next
1813                                                 if $chanstr_wanted ne $chanstr;
1814                                         use re 'eval';
1815                                         my @matches = ($line =~ /^$re$/s);
1816                                         no re 'eval';
1817                                         next
1818                                                 unless @matches;
1819                                         # and if it is a match, handle it.
1820                                         ++$handled;
1821                                         my $result = $sub->(@matches);
1822                                         $private = 1
1823                                                 if $result < 0;
1824                                         last
1825                                                 if $result;
1826                                 }
1827                                 # print the message, together with info on whether it has been handled or not
1828                                 if($private)
1829                                 {
1830                                         print "           $chanstr >> (private)\n";
1831                                 }
1832                                 elsif($handled)
1833                                 {
1834                                         print "           $chanstr >> $line\n";
1835                                 }
1836                                 else
1837                                 {
1838                                         print "unhandled: $chanstr >> $line\n";
1839                                 }
1840                         }
1841                         1;
1842                 } or do {
1843                         if($@ eq "read error\n")
1844                         {
1845                                 $channels{system}->send("error $chanstr", 0);
1846                                 next CHANNEL;
1847                         }
1848                         else
1849                         {
1850                                 # re-throw
1851                                 die $@;
1852                         }
1853                 };
1854         }
1855
1856         # handle scheduled tasks...
1857         my @t = @tasks;
1858         my $t = time();
1859         # by emptying the list of tasks...
1860         @tasks = ();
1861         for(@t)
1862         {
1863                 my ($time, $sub) = @$_;
1864                 if($t >= $time)
1865                 {
1866                         # calling them if they are schedled for the "past"...
1867                         $sub->($sub);
1868                 }
1869                 else
1870                 {
1871                         # or re-adding them to the task list if they still are scheduled for the "future"
1872                         push @tasks, [$time, $sub];
1873                 }
1874         }
1875 }