3 our $VERSION = '0.4.2 svn $Revision$';
5 # Copyright (c) 2008 Rudolf "divVerent" Polzer
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
16 # The above copyright notice and this permission notice shall be
17 # included in all copies or substantial portions of the Software.
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.
28 # MISC STRING UTILITY ROUTINES to convert between DarkPlaces and IRC conventions
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);
39 $message =~ s/\^/^^/g;
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;
50 $color = $color_irc2dp_table[$1];
51 $color = $oldcolor if not defined $color;
53 ($color == $oldcolor) ? '' : '^' . $color;
55 $message =~ s{[\000-\037]}{}gs; # kill bold etc. for now
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', '{', '|', '}', '~', '<'
96 $message = join '', map { $text_qfont_table[ord $_] } split //, $message;
99 sub color_dp_transform(&$)
101 my ($block, $message) = @_;
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) :
118 return color_dp_transform
120 my ($type, $data, $next) = @_;
122 ? $text_qfont_table[ord $data]
128 sub color_rgb2basic($)
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];
136 my $s = ($max == $min) ? 0 : 1 - $min/$max;
140 return 0 if $v < 0.5;
151 $h = (60 * ($G - $B) / ($max - $min)) % 360;
155 $h = (60 * ($B - $R) / ($max - $min)) + 120;
159 $h = (60 * ($R - $G) / ($max - $min)) + 240;
164 return 2 if $h < 150;
165 return 5 if $h < 200;
166 return 4 if $h < 270;
167 return 6 if $h < 330;
171 sub color_dp_rgb2basic($)
174 return color_dp_transform
176 my ($type, $data, $next) = @_;
177 $type eq 'char' ? ($data eq '^' ? '^^' : $data) :
178 $type eq 'color' ? "^$data" :
179 $type eq 'rgb' ? "^" . color_rgb2basic $data :
189 return color_dp_transform
191 my ($type, $data, $next) = @_;
196 $data = color_rgb2basic $data;
199 $type eq 'char' ? $text_qfont_table[ord $data] :
200 $type eq 'color' ? do {
201 my $oldcolor = $color;
202 $color = $color_dp2irc_table[$data];
204 $color == $oldcolor ? '' :
205 $color < 0 ? "\017" :
206 (index '0123456789,', $next) >= 0 ? "\003$color\002\002" :
218 return color_dp_transform
220 my ($type, $data, $next) = @_;
225 $data = color_rgb2basic $data;
228 $type eq 'char' ? $text_qfont_table[ord $data] :
229 $type eq 'color' ? do {
230 my $oldcolor = $color;
231 $color = $color_dp2ansi_table[$data];
233 $color eq $oldcolor ? '' :
244 # if the message ends with an odd number of ^, kill one
245 chop $message if $message =~ /(?:^|[^\^])\^(\^\^)*$/;
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
259 # $conn->fds() returns all file descriptors used by the connection, so one
260 # can use select() on them.
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.
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;
293 use IO::Socket::INET;
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).
302 my ($class, $proto, $local, $remote, $defaultport) = @_;
303 my $sock = IO::Socket::INET->new(
305 (length($local) ? (LocalAddr => $local) : ()),
307 PeerPort => $defaultport
308 ) or die "socket $proto/$local/$remote/$defaultport: $!";
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!
316 # My demonic powers have made me OMNIPOTENT! Bwahahahahahahaha!
319 bless $you, 'Connection::Socket';
322 # $sock->sockname() returns the local address of the socket.
326 my ($port, $addr) = sockaddr_in $self->{sock}->sockname();
327 return "@{[inet_ntoa $addr]}:$port";
330 # $sock->send($data) sends some data over the socket; on success, 1 is returned.
333 my ($self, $data) = @_;
336 if(not eval { $self->{sock}->send($data); })
344 # $sock->recv() receives as much as possible from the socket (or at most 32k). Returns "" if no data is available.
349 if(defined $self->{sock}->recv($data, 32768, 0))
363 # $sock->fds() returns the socket file descriptor.
367 return fileno $self->{sock};
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;
383 # my $chan = new Channel::FIFO();
391 bless $you, 'Channel::FIFO';
394 sub join_commands($@)
396 my ($self, @data) = @_;
402 my ($self, $line, $nothrottle) = @_;
403 push @{$self->{buffer}}, $line;
408 my ($self, $data) = @_;
415 my $r = $self->{buffer};
416 $self->{buffer} = [];
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.
443 # my $chan = new Channel::QW($connection, "password");
446 my ($class, $conn, $password, $secure, $timeout) = @_;
449 password => $password,
455 bless $you, 'Channel::QW';
458 # Note: multiple commands in one rcon packet is a DarkPlaces extension.
459 sub join_commands($@)
461 my ($self, @data) = @_;
462 return join "\0", @data;
467 my ($self, $line, $nothrottle) = @_;
468 if($self->{secure} > 1)
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");
476 elsif($self->{secure})
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");
484 return $self->{connector}->send("\377\377\377\377rcon $self->{password} $line");
488 # Note: backslash and quotation mark escaping is a DarkPlaces extension.
491 my ($self, $data) = @_;
492 $data =~ s/[\000-\037]//g;
493 $data =~ s/([\\"])/\\$1/g;
494 $data =~ s/\$/\$\$/g;
502 my $sel = IO::Select->new($self->fds());
503 my $endtime_max = Time::HiRes::time() + $self->{timeout};
504 my $endtime = $endtime_max;
506 while((my $dt = $endtime - Time::HiRes::time()) > 0)
508 if($sel->can_read($dt))
512 my $s = $self->{connector}->recv();
517 if($s =~ /^\377\377\377\377challenge (.*)$/s)
522 if $s !~ /^\377\377\377\377n(.*)$/s;
523 $self->{recvbuf} .= $1;
535 my $s = $self->{connector}->recv();
541 if $s !~ /^\377\377\377\377n(.*)$/s;
542 $self->{recvbuf} .= $1;
545 while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
555 return $self->{connector}->fds();
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;
572 use Time::HiRes qw/time/;
575 # my $chan = new Channel::Line($connection);
578 my ($class, $conn) = @_;
588 bless $you, 'Channel::Line';
591 sub join_commands($@)
593 my ($self, @data) = @_;
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.
605 my ($self, $linepersec, $maxlines) = @_;
606 $self->{linepersec} = $linepersec;
607 $self->{maxlines} = $maxlines;
608 $self->{capacity} = $maxlines;
613 my ($self, $line, $nothrottle) = @_;
615 if(defined $self->{capacity})
617 $self->{capacity} += ($t - $self->{lastsend}) * $self->{linepersec};
618 $self->{lastsend} = $t;
619 $self->{capacity} = $self->{maxlines}
620 if $self->{capacity} > $self->{maxlines};
624 if $self->{capacity} < 0;
626 $self->{capacity} -= 1;
629 return $self->{connector}->send("$line\r\n");
634 my ($self, $data) = @_;
635 $data =~ s/\r\n?/\n/g;
645 my $s = $self->{connector}->recv();
650 $self->{recvbuf} .= $s;
653 while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
663 return $self->{connector}->fds();
671 # main program... a gateway between IRC and DarkPlaces servers
679 use Time::HiRes qw/time/;
681 our @handlers = (); # list of [channel, expression, sub to handle result]
682 our @tasks = (); # list of [time, sub]
686 playernick_byid_0 => "(console)",
691 irc_nick_alternates => "",
693 irc_channel => undef,
694 irc_ping_delay => 120,
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,
702 irc_quakenet_authname => "",
703 irc_quakenet_password => "",
704 irc_quakenet_getchallenge => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGE',
705 irc_quakenet_challengeauth => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGEAUTH',
706 irc_quakenet_challengeprefix => ':Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :CHALLENGE',
708 irc_announce_slotsfree => 1,
709 irc_announce_mapchange => 'always',
713 dp_secure_challengetimeout => 1,
715 dp_password => undef,
716 dp_status_delay => 30,
717 dp_server_from_wan => "",
720 irc_admin_password => "",
721 irc_admin_timeout => 3600,
722 irc_admin_quote_re => "",
724 irc_reconnect_delay => 300,
731 # Nexuiz specific parsing of some server messages
733 sub nex_slotsstring()
736 if(defined $store{slots_max})
738 my $slots = $store{slots_max} - $store{slots_active};
739 my $slots_s = ($slots == 1) ? '' : 's';
740 $slotsstr = " ($slots free slot$slots_s)";
741 my $s = $config{dp_server_from_wan} || $config{dp_server};
742 $slotsstr .= "; join now: \002nexuiz +connect $s"
743 if $slots >= 1 and not $store{lms_blocked};
750 # Do we have a config file? If yes, read and parse it (syntax: key = value
751 # pairs, separated by newlines), if not, complain.
752 die "Usage: $0 configfile\n"
755 open my $fh, "<", $ARGV[0]
756 or die "open $ARGV[0]: $!";
761 /^(.*?)\s*=(?:\s*(.*))?$/ or next;
762 warn "Undefined config item: $1"
763 unless exists $config{$1};
764 $config{$1} = defined $2 ? $2 : "";
767 my @missing = grep { !defined $config{$_} } keys %config;
768 die "The following config items are missing: @missing"
773 # Create a channel for error messages and other internal status messages...
775 $channels{system} = new Channel::FIFO();
777 # for example, quit messages caused by signals (if SIGTERM or SIGINT is first
778 # received, try to shut down cleanly, and if such a signal is received a second
782 exit 1 if $quitting++;
783 $channels{system}->send("quit SIGINT");
786 exit 1 if $quitting++;
787 $channels{system}->send("quit SIGTERM");
792 # Create the two channels to gateway between...
794 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => $config{irc_local} => $config{irc_server} => 6667));
795 $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});
796 $config{dp_listen} = $dpsock->sockname();
797 print "Listening on $config{dp_listen}\n";
799 $channels{irc}->throttle(0.5, 5);
802 # Utility routine to write to a channel by name, also outputting what's been written and some status
806 my $nothrottle = shift;
807 my $chan = $channels{$chanstr};
810 print "UNDEFINED: $chanstr, ignoring message\n";
813 @_ = $chan->join_commands(@_);
816 my $result = $chan->send($_, $nothrottle);
819 print " $chanstr << $_\n";
823 print "FLOOD: $chanstr << $_\n";
827 print "ERROR: $chanstr << $_\n";
828 $channels{system}->send("error $chanstr", 0);
835 # Schedule a task for later execution by the main loop; usage: schedule sub {
836 # task... }, $time; When a scheduled task is run, a reference to the task's own
837 # sub is passed as first argument; that way, the task is able to re-schedule
838 # itself so it gets periodically executed.
841 my ($sub, $time) = @_;
842 push @tasks, [time() + $time, $sub];
845 # On IRC error, delete some data store variables of the connection, and
846 # reconnect to the IRC server soon (but only if someone is actually playing)
849 # prevent multiple instances of this timer
850 return if $store{irc_error_active};
851 $store{irc_error_active} = 1;
853 delete $channels{irc};
856 if(!defined $store{slots_active})
858 # DP is not running, then delay IRC reconnecting
859 #use Data::Dumper; print Dumper \$timer;
860 schedule $timer => 1;
862 # this will keep irc_error_active
864 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => "" => $config{irc_server} => 6667));
865 delete $store{$_} for grep { /^irc_/ } keys %store;
866 $store{irc_nick} = "";
869 out dp => 0, 'sv_cmd bans', 'status 1', 'log_dest_udp';
870 $store{status_waiting} = -1;
872 # this will clear irc_error_active
873 } => $config{irc_reconnect_delay};
883 next if $found{$_}++;
889 # IRC joining (if this is called as response to a nick name collision, $is433 is set);
890 # among other stuff, it performs NickServ or Quakenet authentication. This is to be called
891 # until the channel has been joined for every message that may be "interesting" (basically,
892 # IRC 001 hello messages, 443 nick collision messages and some notices by services).
898 if $store{irc_joined_channel};
900 #use Data::Dumper; print Dumper \%store;
904 if(length $store{irc_nick})
906 # we already have another nick, but couldn't change to the new one
907 # try ghosting and then get the nick again
908 if(length $config{irc_nickserv_password})
910 if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
912 $store{irc_nick_requested} = $config{irc_nick};
913 out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
915 out irc => 1, "NICK $config{irc_nick}";
917 return; # we'll get here again for the NICK success message, or for a 433 failure
919 # otherwise, we failed to ghost and will continue with the wrong
920 # nick... also, no need to try to identify here
922 # otherwise, we can't handle this and will continue with our wrong nick
926 # we failed to get an initial nickname
927 # change ours a bit and try again
929 my @alternates = uniq ($config{irc_nick}, grep { $_ ne "" } split /\s+/, $config{irc_nick_alternates});
930 my $nextnick = undef;
931 for(0..@alternates-2)
933 if($store{irc_nick_requested} eq $alternates[$_])
935 $nextnick = $alternates[$_+1];
938 if($store{irc_nick_requested} eq $alternates[@alternates-1]) # this will only happen once
940 $store{irc_nick_requested} = $alternates[0];
941 # but don't set nextnick, so we edit it
943 if(defined $nextnick)
945 $store{irc_nick_requested} = $nextnick;
951 if(length $store{irc_nick_requested} < 9)
953 $store{irc_nick_requested} .= '_';
957 substr $store{irc_nick_requested}, int(rand length $store{irc_nick_requested}), 1, chr(97 + int rand 26);
959 last unless grep { $_ eq $store{irc_nick_requested} } @alternates;
962 out irc => 1, "NICK $store{irc_nick_requested}";
963 return; # when it fails, we'll get here again, and when it succeeds, we will continue
967 # we got a 001 or a NICK message, so $store{irc_nick} has been updated
968 if(length $config{irc_nickserv_password})
970 if($store{irc_nick} eq $config{irc_nick})
973 out irc => 1, sprintf($config{irc_nickserv_identify}, $config{irc_nick}, $config{irc_nickserv_password});
978 if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
980 $store{irc_nick_requested} = $config{irc_nick};
981 out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
983 out irc => 1, "NICK $config{irc_nick}";
985 return; # we'll get here again for the NICK success message, or for a 433 failure
987 # otherwise, we failed to ghost and will continue with the wrong
988 # nick... also, no need to try to identify here
992 # we are on Quakenet. Try to authenticate.
993 if(length $config{irc_quakenet_password} and length $config{irc_quakenet_authname})
995 if(defined $store{irc_quakenet_challenge})
997 if($store{irc_quakenet_challenge} =~ /^([0-9a-f]*)\b.*\bHMAC-SHA-256\b/)
1000 my $hash1 = Digest::SHA::sha256_hex(substr $config{irc_quakenet_password}, 0, 10);
1001 my $key = Digest::SHA::sha256_hex("@{[lc $config{irc_quakenet_authname}]}:$hash1");
1002 my $digest = Digest::HMAC::hmac_hex($challenge, $key, \&Digest::SHA::sha256);
1003 out irc => 1, "$config{irc_quakenet_challengeauth} $config{irc_quakenet_authname} $digest HMAC-SHA-256";
1008 out irc => 1, $config{irc_quakenet_getchallenge};
1010 # we get here again when Q asks us
1014 # if we get here, we are on IRC
1015 $store{irc_joined_channel} = 1;
1017 out irc => 1, "JOIN $config{irc_channel}";
1022 my $RE_FAIL = qr/$ $/;
1023 my $RE_SUCCEED = qr//;
1026 return $_[0] ? $RE_FAIL : $RE_SUCCEED;
1030 # List of all handlers on the various sockets. Additional handlers can be added by a plugin.
1032 # detect a server restart and set it up again
1033 [ dp => q{ *(?:Warning: Could not expand \$|Unknown command ")(?:rcon2irc_[a-z0-9_]*)[" ]*} => sub {
1035 'alias rcon2irc_eval "$*"',
1037 'sv_logscores_console 0',
1038 'sv_logscores_bots 1',
1040 'sv_eventlog_console 1',
1041 'alias rcon2irc_say_as "set say_as_restorenick \"$sv_adminnick\"; sv_adminnick \"$1^3\"; say \"^7$2\"; rcon2irc_say_as_restore"',
1042 'alias rcon2irc_say_as_restore "set sv_adminnick \"$say_as_restorenick\""',
1043 'alias rcon2irc_quit "echo \"quitting rcon2irc $1: log_dest_udp is $log_dest_udp\""'; # note: \\\\\\" ->perl \\\" ->console \"
1047 # detect missing entry in log_dest_udp and fix it
1048 [ dp => q{"log_dest_udp" is "([^"]*)" \["[^"]*"\]} => sub {
1050 my @dests = split ' ', $dest;
1051 return 0 if grep { $_ eq $config{dp_listen} } @dests;
1052 out dp => 0, 'log_dest_udp "' . join(" ", @dests, $config{dp_listen}) . '"';
1056 # retrieve list of banned hosts
1057 [ dp => q{#(\d+): (\S+) is still banned for (\S+) seconds} => sub {
1058 return 0 unless $store{status_waiting} < 0;
1059 my ($id, $ip, $time) = @_;
1060 $store{bans_new} = [] if $id == 0;
1061 $store{bans_new}[$id] = { ip => $ip, 'time' => $time };
1065 # retrieve hostname from status replies
1066 [ dp => q{host: (.*)} => sub {
1067 return 0 unless $store{status_waiting} < 0;
1069 $store{dp_hostname} = $name;
1070 $store{bans} = $store{bans_new};
1074 # retrieve version from status replies
1075 [ dp => q{version: (.*)} => sub {
1076 return 0 unless $store{status_waiting} < 0;
1078 $store{dp_version} = $version;
1082 # retrieve player names
1083 [ dp => q{players: (\d+) active \((\d+) max\)} => sub {
1084 return 0 unless $store{status_waiting} < 0;
1085 my ($active, $max) = @_;
1086 my $full = ($active >= $max);
1087 $store{slots_max} = $max;
1088 $store{slots_active} = $active;
1089 $store{status_waiting} = $active;
1090 $store{playerslots_active_new} = [];
1091 if($store{status_waiting} == 0)
1093 $store{playerslots_active} = $store{playerslots_active_new};
1095 if($full != ($store{slots_full} || 0))
1097 $store{slots_full} = $full;
1098 return 0 if $store{lms_blocked};
1099 return 0 if !$config{irc_announce_slotsfree};
1102 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION is full!\001";
1106 my $slotsstr = nex_slotsstring();
1107 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can be joined again$slotsstr!\001";
1113 # retrieve player names
1114 [ dp => q{\^\d(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(-?\d+)\s+\#(\d+)\s+\^\d(.*)} => sub {
1115 return 0 unless $store{status_waiting} > 0;
1116 my ($ip, $pl, $ping, $time, $frags, $no, $name) = ($1, $2, $3, $4, $5, $6, $7);
1117 $store{"playerslot_$no"} = { ip => $ip, pl => $pl, ping => $ping, 'time' => $time, frags => $frags, no => $no, name => $name };
1118 push @{$store{playerslots_active_new}}, $no;
1119 if(--$store{status_waiting} == 0)
1121 $store{playerslots_active} = $store{playerslots_active_new};
1126 # IRC admin commands
1127 [ irc => q{:(([^! ]*)![^ ]*) (?i:PRIVMSG) [^&#%]\S* :(.*)} => sub {
1128 return 0 unless $config{irc_admin_password} ne '';
1130 my ($hostmask, $nick, $command) = @_;
1131 my $dpnick = color_dpfix $nick;
1133 if($command eq "login $config{irc_admin_password}")
1135 $store{logins}{$hostmask} = time() + $config{irc_admin_timeout};
1136 out irc => 0, "PRIVMSG $nick :my wish is your command";
1140 if($command =~ /^login /)
1142 out irc => 0, "PRIVMSG $nick :invalid password";
1146 if(($store{logins}{$hostmask} || 0) < time())
1148 out irc => 0, "PRIVMSG $nick :authentication required";
1152 if($command =~ /^status(?: (.*))?$/)
1157 for my $slot(@{$store{playerslots_active} || []})
1159 my $s = $store{"playerslot_$slot"};
1161 if(not defined $match or index(color_dp2none($s->{name}), $match) >= 0)
1163 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};
1172 out irc => 0, "PRIVMSG $nick :the server is empty";
1176 out irc => 0, "PRIVMSG $nick :no nicknames match";
1182 if($command =~ /^kick # (\d+) (.*)$/)
1184 my ($id, $reason) = ($1, $2);
1185 my $dpreason = color_irc2dp $reason;
1186 $dpreason =~ s/^(~?)(.*)/$1irc $dpnick: $2/g;
1187 $dpreason =~ s/(["\\])/\\$1/g;
1188 out dp => 0, "kick # $id $dpreason";
1189 my $slotnik = "playerslot_$id";
1190 out irc => 0, "PRIVMSG $nick :kicked #$id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip}) ($reason)";
1194 if($command =~ /^kickban # (\d+) (\d+) (\d+) (.*)$/)
1196 my ($id, $bantime, $mask, $reason) = ($1, $2, $3, $4);
1197 my $dpreason = color_irc2dp $reason;
1198 $dpreason =~ s/^(~?)(.*)/$1irc $dpnick: $2/g;
1199 $dpreason =~ s/(["\\])/\\$1/g;
1200 out dp => 0, "kickban # $id $bantime $mask $dpreason";
1201 my $slotnik = "playerslot_$id";
1202 out irc => 0, "PRIVMSG $nick :kickbanned #$id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip}), netmask $mask, for $bantime seconds ($reason)";
1206 if($command eq "bans")
1210 map { "$_ ($store{bans}[$_]{ip}, $store{bans}[$_]{time}s)" }
1211 0..@{$store{bans} || []}-1;
1212 $banlist = "no bans"
1214 out irc => 0, "PRIVMSG $nick :$banlist";
1218 if($command =~ /^unban (\d+)$/)
1221 out dp => 0, "unban $id";
1222 out irc => 0, "PRIVMSG $nick :removed ban $id ($store{bans}[$id]{ip})";
1226 if($command =~ /^mute (\d+)$/)
1229 out dp => 0, "mute $id";
1230 my $slotnik = "playerslot_$id";
1231 out irc => 0, "PRIVMSG $nick :muted $id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip})";
1235 if($command =~ /^unmute (\d+)$/)
1238 out dp => 0, "unmute $id";
1239 my $slotnik = "playerslot_$id";
1240 out irc => 0, "PRIVMSG $nick :unmuted $id (@{[color_dp2irc $store{$slotnik}{name}]}\017 @ $store{$slotnik}{ip})";
1244 if($command =~ /^quote (.*)$/)
1247 if($cmd =~ /^(??{$config{irc_admin_quote_re}})$/si)
1250 out irc => 0, "PRIVMSG $nick :executed your command";
1254 out irc => 0, "PRIVMSG $nick :permission denied";
1259 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)";
1264 # LMS: detect "no more lives" message
1265 [ dp => q{\^4.*\^4 has no more lives left} => sub {
1266 if(!$store{lms_blocked})
1268 $store{lms_blocked} = 1;
1269 if(!$store{slots_full})
1272 if($store{lms_blocked})
1274 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can't be joined until next round (a player has no more lives left)\001";
1281 # detect IRC errors and reconnect
1282 [ irc => q{ERROR .*} => \&irc_error ],
1283 [ irc => q{:[^ ]* 404 .*} => \&irc_error ], # cannot send to channel
1284 [ system => q{error irc} => \&irc_error ],
1287 [ irc => q{:[^ ]* 433 .*} => sub {
1288 return irc_joinstage(433);
1292 [ irc => q{:[^ ]* 001 .*} => sub {
1293 $store{irc_seen_welcome} = 1;
1294 $store{irc_nick} = $store{irc_nick_requested};
1295 return irc_joinstage(0);
1298 # IRC my nickname changed
1299 [ irc => q{:(?i:(??{$store{irc_nick}}))![^ ]* (?i:NICK) :(.*)} => sub {
1301 $store{irc_nick} = $n;
1302 return irc_joinstage(0);
1305 # Quakenet: challenge from Q
1306 [ irc => q{(??{$config{irc_quakenet_challengeprefix}}) (.*)} => sub {
1307 $store{irc_quakenet_challenge} = $1;
1308 return irc_joinstage(0);
1311 # shut down everything on SIGINT
1312 [ system => q{quit (.*)} => sub {
1314 out irc => 1, "QUIT :$cause";
1315 $store{quitcookie} = int rand 1000000000;
1316 out dp => 0, "rcon2irc_quit $store{quitcookie}";
1319 # remove myself from the log destinations and exit everything
1320 [ dp => q{quitting rcon2irc (??{$store{quitcookie}}): log_dest_udp is (.*) *} => sub {
1322 my @dests = grep { $_ ne $config{dp_listen} } split ' ', $dest;
1323 out dp => 0, 'log_dest_udp "' . join(" ", @dests) . '"';
1329 [ irc => q{PING (.*)} => sub {
1331 out irc => 1, "PONG $data";
1336 [ irc => q{:[^ ]* PONG .* :(.*)} => sub {
1339 if not defined $store{irc_pingtime};
1341 if $data ne $store{irc_pingtime};
1342 print "* measured IRC line delay: @{[time() - $store{irc_pingtime}]}\n";
1343 undef $store{irc_pingtime};
1347 # detect channel join message and note hostname length to get the maximum allowed line length
1348 [ irc => q{(:(?i:(??{$store{irc_nick}}))![^ ]* )(?i:JOIN) :(?i:(??{$config{irc_channel}}))} => sub {
1349 $store{irc_maxlen} = 510 - length($1);
1350 $store{irc_joined_channel} = 1;
1351 print "* detected maximum line length for channel messages: $store{irc_maxlen}\n";
1355 # chat: Nexuiz server -> IRC channel
1356 [ dp => q{\001(.*?)\^7: (.*)} => sub {
1357 my ($nick, $message) = map { color_dp2irc $_ } @_;
1358 out irc => 0, "PRIVMSG $config{irc_channel} :<$nick\017> $message";
1362 # chat: Nexuiz server -> IRC channel, nick set
1363 [ dp => q{:join:(\d+):(\d+):([^:]*):(.*)} => sub {
1364 my ($id, $slot, $ip, $nick) = @_;
1365 $store{"playernickraw_byid_$id"} = $nick;
1366 $nick = color_dp2irc $nick;
1367 $store{"playernick_byid_$id"} = $nick;
1368 $store{"playerip_byid_$id"} = $ip;
1369 $store{"playerslot_byid_$id"} = $slot;
1370 $store{"playerid_byslot_$slot"} = $id;
1374 # chat: Nexuiz server -> IRC channel, nick change/set
1375 [ dp => q{:name:(\d+):(.*)} => sub {
1376 my ($id, $nick) = @_;
1377 $store{"playernickraw_byid_$id"} = $nick;
1378 $nick = color_dp2irc $nick;
1379 my $oldnick = $store{"playernick_byid_$id"};
1380 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 is now known as $nick";
1381 $store{"playernick_byid_$id"} = $nick;
1385 # chat: Nexuiz server -> IRC channel, vote call
1386 [ dp => q{:vote:vcall:(\d+):(.*)} => sub {
1387 my ($id, $command) = @_;
1388 $command = color_dp2irc $command;
1389 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1390 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 calls a vote for \"$command\017\"";
1394 # chat: Nexuiz server -> IRC channel, vote stop
1395 [ dp => q{:vote:vstop:(\d+)} => sub {
1397 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1398 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 stopped the vote";
1402 # chat: Nexuiz server -> IRC channel, master login
1403 [ dp => q{:vote:vlogin:(\d+)} => sub {
1405 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1406 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 logged in as master";
1410 # chat: Nexuiz server -> IRC channel, master do
1411 [ dp => q{:vote:vdo:(\d+):(.*)} => sub {
1412 my ($id, $command) = @_;
1413 $command = color_dp2irc $command;
1414 my $oldnick = $id ? $store{"playernick_byid_$id"} : "(console)";
1415 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 used his master status to do \"$command\017\"";
1419 # chat: Nexuiz server -> IRC channel, result
1420 [ dp => q{:vote:v(yes|no|timeout):(\d+):(\d+):(\d+):(\d+):(-?\d+)} => sub {
1421 my ($result, $yes, $no, $abstain, $not, $min) = @_;
1422 my $spam = "$yes:$no" . (($min >= 0) ? " ($min needed)" : "") . ", $abstain didn't care, $not didn't vote";
1423 out irc => 0, "PRIVMSG $config{irc_channel} :* the vote ended with $result: $spam";
1427 # chat: IRC channel -> Nexuiz server
1428 [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$store{irc_nick}}))(?: |: ?|, ?)(.*)} => sub {
1429 my ($nick, $message) = @_;
1430 $nick = color_dpfix $nick;
1431 # allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
1432 $message = color_irc2dp $message;
1433 $message =~ s/(["\\])/\\$1/g;
1434 out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
1439 length $config{irc_trigger}
1441 [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$config{irc_trigger}}))(?: |: ?|, ?)(.*)} => sub {
1442 my ($nick, $message) = @_;
1443 $nick = color_dpfix $nick;
1444 # allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
1445 $message = color_irc2dp $message;
1446 $message =~ s/(["\\])/\\$1/g;
1447 out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
1454 # irc: CTCP VERSION reply
1455 [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$store{irc_nick}})) :\001VERSION( .*)?\001} => sub {
1457 my $ver = $store{dp_version} or return 0;
1458 $ver .= ", rcon2irc $VERSION";
1459 out irc => 0, "NOTICE $nick :\001VERSION $ver\001";
1462 # on game start, notify the channel
1463 [ dp => q{:gamestart:(.*):[0-9.]*} => sub {
1465 $store{playing} = 1;
1467 $store{map_starttime} = time();
1468 if ($config{irc_announce_mapchange} eq 'always' || ($config{irc_announce_mapchange} eq 'notempty' && $store{slots_active} > 0)) {
1469 my $slotsstr = nex_slotsstring();
1470 out irc => 0, "PRIVMSG $config{irc_channel} :\00304" . $map . "\017 has begun$slotsstr";
1472 delete $store{lms_blocked};
1476 # on game over, clear the current map
1477 [ dp => q{:gameover} => sub {
1478 $store{playing} = 0;
1482 # scores: Nexuiz server -> IRC channel (start)
1483 [ dp => q{:scores:(.*):(\d+)} => sub {
1484 my ($map, $time) = @_;
1485 $store{scores} = {};
1486 $store{scores}{map} = $map;
1487 $store{scores}{time} = $time;
1488 $store{scores}{players} = [];
1489 delete $store{lms_blocked};
1493 # scores: Nexuiz server -> IRC channel, legacy format
1494 [ dp => q{:player:(-?\d+):(\d+):(\d+):(\d+):(\d+):(.*)} => sub {
1495 my ($frags, $deaths, $time, $team, $id, $name) = @_;
1496 return if not exists $store{scores};
1497 push @{$store{scores}{players}}, [$frags, $team, $name]
1498 unless $frags <= -666; # no spectators
1502 # scores: Nexuiz server -> IRC channel (CTF), legacy format
1503 [ dp => q{:teamscores:(\d+:-?\d*(?::\d+:-?\d*)*)} => sub {
1505 return if not exists $store{scores};
1506 $store{scores}{teams} = {split /:/, $teams};
1510 # scores: Nexuiz server -> IRC channel, new format
1511 [ dp => q{:player:see-labels:(-?\d+)[-0-9,]*:(\d+):(\d+):(\d+):(.*)} => sub {
1512 my ($frags, $time, $team, $id, $name) = @_;
1513 return if not exists $store{scores};
1514 push @{$store{scores}{players}}, [$frags, $team, $name];
1518 # scores: Nexuiz server -> IRC channel (CTF), new format
1519 [ dp => q{:teamscores:see-labels:(-?\d+)[-0-9,]*:(\d+)} => sub {
1520 my ($frags, $team) = @_;
1521 return if not exists $store{scores};
1522 $store{scores}{teams}{$team} = $frags;
1526 # scores: Nexuiz server -> IRC channel
1527 [ dp => q{:end} => sub {
1528 return if not exists $store{scores};
1529 my $s = $store{scores};
1530 delete $store{scores};
1531 my $teams_matter = defined $s->{teams};
1538 # put players into teams
1540 for(@{$s->{players}})
1542 my $thisteam = ($t{$_->[1]} ||= {score => 0, team => $_->[1], players => []});
1543 push @{$thisteam->{players}}, [$_->[0], $_->[1], $_->[2]];
1546 $thisteam->{score} = $s->{teams}{$_->[1]};
1550 $thisteam->{score} += $_->[0];
1554 # sort by team score
1555 @t = sort { $b->{score} <=> $a->{score} } values %t;
1557 # sort by player score
1561 @{$_->{players}} = sort { $b->[0] <=> $a->[0] } @{$_->{players}};
1562 push @p, @{$_->{players}};
1567 @p = sort { $b->[0] <=> $a->[0] } @{$s->{players}};
1570 # no display for empty server
1574 # make message fit somehow
1575 for my $maxnamelen(reverse 3..64)
1577 my $scores_string = "PRIVMSG $config{irc_channel} :\00304" . $s->{map} . "\017 ended:";
1583 $scores_string .= $sep . "\003" . $color_team2irc_table{$_->{team}}. "\002\002" . $_->{score} . "\017";
1590 my ($frags, $team, $name) = @$_;
1591 $name = color_dpfix substr($name, 0, $maxnamelen);
1594 $name = "\003" . $color_team2irc_table{$team} . " " . color_dp2none $name;
1598 $name = " " . color_dp2irc $name;
1600 $scores_string .= "$sep$name\017 $frags";
1603 if(length($scores_string) <= ($store{irc_maxlen} || 256))
1605 out irc => 0, $scores_string;
1609 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION would have LIKED to put the scores here, but they wouldn't fit :(\001";
1613 # complain when system load gets too high
1614 [ dp => q{timing: (([0-9.]*)% CPU, ([0-9.]*)% lost, offset avg ([0-9.]*)ms, max ([0-9.]*)ms, sdev ([0-9.]*)ms)} => sub {
1615 my ($all, $cpu, $lost, $avg, $max, $sdev) = @_;
1616 return 0 # don't complain when just on the voting screen
1617 if !$store{playing};
1618 return 0 # don't complain if it was less than 0.5%
1620 return 0 # don't complain if nobody is looking
1621 if $store{slots_active} == 0;
1622 return 0 # don't complain in the first two minutes
1623 if time() - $store{map_starttime} < 120;
1624 return 0 # don't complain if it was already at least half as bad in this round
1625 if $store{map_starttime} == $store{timingerror_map_starttime} and $lost <= 2 * $store{timingerror_lost};
1626 $store{timingerror_map_starttime} = $store{map_starttime};
1627 $store{timingerror_lost} = $lost;
1628 out dp => 0, 'rcon2irc_say_as server "There are currently some severe system load problems. The admins have been notified."';
1629 out irc => 1, "PRIVMSG $config{irc_channel} :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1630 #out irc => 1, "PRIVMSG OpBaI :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1637 # Load plugins and add them to the handler list in the front.
1638 for my $p(split ' ', $config{plugins})
1640 my @h = eval { do $p; }
1641 or die "Invalid plugin $p: $@";
1644 ref $_ eq 'ARRAY' or die "Invalid plugin $p: did not return a list of arrays";
1645 @$_ == 3 or die "Invalid plugin $p: did not return a list of three-element arrays";
1646 !ref $_->[0] && !ref $_->[1] && ref $_->[2] eq 'CODE' or die "Invalid plugin $p: did not return a list of string-string-sub arrays";
1647 unshift @handlers, $_;
1653 # verify that the server is up by letting it echo back a string that causes
1654 # re-initialization of the required aliases
1655 out dp => 0, 'echo "Unknown command \"rcon2irc_eval\""'; # assume the server has been restarted
1659 # regularily, query the server status and if it still is connected to us using
1660 # the log_dest_udp feature. If not, we will detect the response to this rcon
1661 # command and re-initialize the server's connection to us (either by log_dest_udp
1662 # not containing our own IP:port, or by rcon2irc_eval not being a defined command).
1665 out dp => 0, 'sv_cmd bans', 'status 1', 'log_dest_udp', 'rcon2irc_eval set dummy 1';
1666 $store{status_waiting} = -1;
1667 schedule $timer => (exists $store{dp_hostname} ? $config{dp_status_delay} : 1);;
1672 # Continue with connecting to IRC as soon as we get our first status reply from
1673 # the DP server (which contains the server's hostname that we'll use as
1674 # realname for IRC).
1678 # log on to IRC when needed
1679 if(exists $store{dp_hostname} && !exists $store{irc_logged_in})
1681 $store{irc_nick_requested} = $config{irc_nick};
1682 out irc => 1, "NICK $config{irc_nick}", "USER $config{irc_user} localhost localhost :$store{dp_hostname}";
1683 $store{irc_logged_in} = 1;
1684 undef $store{irc_maxlen};
1685 undef $store{irc_pingtime};
1688 schedule $timer => 1;;
1693 # Regularily ping the IRC server to detect if the connection is down. If it is,
1694 # schedule an IRC error that will cause reconnection later.
1698 if($store{irc_logged_in})
1700 if(defined $store{irc_pingtime})
1702 # IRC connection apparently broke
1703 # so... KILL IT WITH FIRE
1704 $channels{system}->send("error irc", 0);
1708 # everything is fine, send a new ping
1709 $store{irc_pingtime} = time();
1710 out irc => 1, "PING $store{irc_pingtime}";
1714 schedule $timer => $config{irc_ping_delay};;
1722 # Build up an IO::Select object for all our channels.
1723 my $s = IO::Select->new();
1724 for my $chan(values %channels)
1726 $s->add($_) for $chan->fds();
1729 # wait for something to happen on our sockets, or wait 2 seconds without anything happening there
1731 my @errors = $s->has_exception(0);
1733 # on every channel, look for incoming messages
1735 for my $chanstr(keys %channels)
1737 my $chan = $channels{$chanstr};
1738 my @chanfds = $chan->fds();
1740 for my $chanfd(@chanfds)
1742 if(grep { $_ == $chanfd } @errors)
1744 # STOP! This channel errored!
1745 $channels{system}->send("error $chanstr", 0);
1752 for my $line($chan->recv())
1754 # found one! Check if it matches the regular expression of one of
1758 for my $h(@handlers)
1760 my ($chanstr_wanted, $re, $sub) = @$h;
1762 if $chanstr_wanted ne $chanstr;
1764 my @matches = ($line =~ /^$re$/s);
1768 # and if it is a match, handle it.
1770 my $result = $sub->(@matches);
1776 # print the message, together with info on whether it has been handled or not
1779 print " $chanstr >> (private)\n";
1783 print " $chanstr >> $line\n";
1787 print "unhandled: $chanstr >> $line\n";
1792 if($@ eq "read error\n")
1794 $channels{system}->send("error $chanstr", 0);
1805 # handle scheduled tasks...
1808 # by emptying the list of tasks...
1812 my ($time, $sub) = @$_;
1815 # calling them if they are schedled for the "past"...
1820 # or re-adding them to the task list if they still are scheduled for the "future"
1821 push @tasks, [$time, $sub];