=head1 NAME Net::Knuddels - www.knuddels.de protocol implementation. =head1 SYNOPSIS use Net::Knuddels; =head1 DESCRIPTION RTSL. =cut #TODO (Singles 15-17) headbanger16 >> LINK:Net-Knuddels (privat): hä was ins arschloch?? package Net::Knuddels; use Net::Knuddels::Dictionary; use strict; use utf8; use Carp; use Math::BigInt; use Time::HiRes; sub _to32($) { unpack "l", pack "L", (new Math::BigInt $_[0]) & 0xffffffff } sub hash_pw($$) { my ($challenge, $pw) = @_; my $l1 = length $pw; my $l2 = length $challenge; my $k = chr ($l1 ^ ($l2 << 4)); my $l = $l1 < $l2 ? $l2 : $l1; my $xor = substr +($pw x 100) ^ ($challenge x 100) ^ ($k x 100), 0, $l; my ($i, $j); --$l; if ($l <= 17) { for (0 .. $l) { $i = _to32 $i * 3 + ord substr $xor, $l - $_; $j = _to32 $j * 5 + ord substr $xor, $_; } } else { for ($_ = $l; $_ >= 0; $_ -= int $_/19) { $i = _to32 $i * 5 + ord substr $xor, $_; $j = _to32 $j * 3 + ord substr $xor, $l - $_; } } $i ^= $j; _to32 (($i & 0xffffff) ^ ($i >> 24)) } my $RE_dec = join "|", keys %$Net::Knuddels::Dictionary; sub decode { my $bin = unpack "b*", $_[0]; my $res = ""; while ($bin =~ /\G($RE_dec)/cog) { my $frag = $Net::Knuddels::Dictionary->{$1}; $frag = chr unpack "v", pack "b*", $bin =~ /\G(.{16})/cg && $1 if $frag eq "\\\\\\"; $res .= $frag; } $bin =~ /\G(.*[^0].*)$/ and die "Net::Knuddels::Receiver: undecodable message tail '$1'"; $res } my %encode = reverse %$Net::Knuddels::Dictionary; my $RE_enc = join "|", map quotemeta, sort { (length $b) <=> (length $a) } keys %encode; sub encode($) { my ($msg) = @_; my $data = ""; while () { $data .= $encode{$1} while $msg =~ /\G($RE_enc)/cog; $msg =~ /\G./csog or last; $data .= $encode{"\\\\\\"} . unpack "b*", pack "v", ord $1; } pack "b*", $data } =head2 CLASS Net::Knuddels::Protocol You B call the C method of this class when you no longer use it, as circular references will keep the object alive otherwise. =over 4 =cut package Net::Knuddels::Protocol; =item new Create a new C object. =cut sub new { my $class = shift; my %data; my $self = bless { @_ }, $class; $self; } =item $protocol->feed_data ($octets) Feed raw protocol data into the decoder. =cut sub feed_data($$) { my ($self, $data) = @_; # split data stream into packets $data = "$self->{rbuf}$data"; while () { 1 <= length $data or last; my $len = ord substr $data, 0, 1; my $skip; if ($len & 0x80) { my $tail = (($len >> 5) & 3) - 1; $len = ($len & 0x1f) + 1; $tail < length $data or last; $len += (ord substr $data, $_ + 1, 1) << ($_ * 8 + 5) for 0 .. $tail; $skip = 2 + $tail; } else { $skip = 1; $len++; } $len + $skip <= length $data or last; substr $data, 0, $skip, ""; my $msg = substr $data, 0, $len, ""; $self->feed_msg ($msg); } $self->{rbuf} = $data; } sub feed_msg($$) { my ($self, $msg) = @_; $self->feed_event (split /\0/, Net::Knuddels::decode $msg); } sub feed_event($@) { my ($self, @cmd) = @_; my $ev = $self->{cb}{ALL}; $_->(@cmd) for values %$ev; unless ($self->{cb}{$cmd[0]}) { my $ev = $self->{cb}{UNHANDLED}; $_->(@cmd) for values %$ev; } my $ev = $self->{cb}{shift @cmd}; $_->(@cmd) for values %$ev; } =item $msg = $protocol->encode_msg (@strings) Join the strings with C<\0>, encode the result into a protocol packet and return it. =cut sub encode_msg($@) { my ($self, @args) = @_; my $msg = Net::Knuddels::encode join "\0", @args; my $len = (length $msg) - 1; if ($len < 0x80) { (chr $len) . $msg } else { (chr 0x80 | 0x40 | ($len & 0x1f)) . (chr +($len >> 5) % 0xff) . (chr +($len >> 13) % 0xff) . $msg } } =item $protocol->register ($type => $callback) Register a callback for events of type C<$type>, which is either the name of a low-level event sent by the server (such as "k" for dialog box) or the name of a generated event, such as C. =cut sub register { my ($self, $type, $cb) = @_; $self->{cb}{$type}{$cb} = $cb; } =item $protocol->destroy I be called to destroy the object, otherwise it will leak (no automatic cleanup). =cut sub destroy { my ($self) = @_; delete $self->{cb}; } =back =head2 CLASS Net::Knuddels::Client Implement a Knuddels client connection. =over 4 =cut package Net::Knuddels::Client; sub handle_room { my ($self, $room) = @_; if ($room eq "-") { if (scalar (keys %{$self->{room}}) == 1) { return (keys %{$self->{room}})[0]; } else { warn "Couldn't assign '-' room to a room!"; return '#nosuchroom'; } } else { return $room; } } sub update_user_stats { my ($user) = @_; if ($user->{name} =~ s/\cJ(\d+)$//) { $user->{age} = $1 } if ($user->{picture} =~ m/\bmale/) { $user->{gender} = 'm'; } elsif ($user->{picture} =~ m/female/) { $user->{gender} = 'f'; } return $user; } sub del1 { my ($str) = @_; my $s = substr ($$str, 0, 1); $$str = substr ($$str, 1); $s } sub del2 { my ($str) = @_; my $s = substr ($$str, 0, 2); $$str = substr ($$str, 2); $s } sub todelim { my ($str) = @_; $$str =~ s/^(.*?)\365//; $1; } sub chk_flag { my ($str) = @_; if ($$str =~ s/^\343//) { return 1; } return 0; } =item new Net::Knuddels::Client [IO::Socket::new arguments] Create a new client connection. =cut use IO::Socket::INET; sub new { my ($class, @arg) = @_; my $fh = new IO::Socket::INET @arg or Carp::croak "Net::Knuddels::Client::new: $!"; my $self = bless { queue => [], fh => $fh, proto => (new Net::Knuddels::Protocol), rate => 1, # commands/s @arg, }, $class; syswrite $fh, "\0"; $self->register ("(" => sub { $self->{login_challenge} = $_[0]; $self->{login_room} = $_[1]; $self->{proto}->feed_event (login => $_[1]); }); $self->register (k => sub { my @str = map { s/[\356\343]//; $_ } @_; my @out; push @out, split /#/, $_ for @str; $self->{proto}->feed_event (dialog => \@out); }); $self->register (t => sub { my $src = $_[0]; if ($src eq '-') { $_[2] = $self->{nick} . " " . $_[2]; } else { $_[2] = $src . " " . $_[2]; } $self->{proto}->feed_event (action_room => $self->handle_room ($_[1]), $_[2]); }); my %last_msg; # the last message of a user, to avoid duplicates $self->register (r => sub { my $src = $_[0]; $src = $self->{nick} if $src eq "-"; $self->{proto}->feed_event (msg_priv => $self->handle_room ($_[2]), $src, $_[1], $_[3]); if ($src eq "James") { $self->{proto}->feed_event (msg_priv_james => $self->handle_room ($_[2]), $src, $_[1], $_[3]); } elsif ($src eq $self->{nick}) { $self->{proto}->feed_event (msg_priv_echo => $self->handle_room ($_[2]), $src, $_[1], $_[3]); } else { $self->{proto}->feed_event (msg_priv_nondup => $self->handle_room ($_[2]), $src, $_[1], $_[3]) if $last_msg{$src} ne $_[3]; $last_msg{$src} = $_[3]; } }); $self->register (e => sub { $self->{proto}->feed_event (msg_room => $self->handle_room ($_[1]), $_[0], $_[2]); }); $self->register (l => sub { my $room = $self->handle_room ($_[0]); return if $room eq "-"; # things that shouln't happen my $user = { name => $_[1], flag => $_[2], color => $_[3], picture => $_[4] }; update_user_stats ($user); my $rl = $self->{user_lists}->{lc $room}->{lc $user->{name}} = $user; $self->{proto}->feed_event (join_room => $room, $user); }); $self->register (w => sub { my $room = $self->handle_room ($_[1]); return if $room eq "-"; # things that shouln't happen my $username = $_[0]; my $u = delete $self->{user_lists}->{lc $room}->{lc $username}; if (not defined $u) { warn "User $username wasn't in room $room, trying to fix... but be careful!!!\n"; $u = { name => $username }; } $self->{proto}->feed_event (part_room => $room, $u); }); $self->register (b => sub { my @arg = @_; my $cc = {}; $self->{knuddels_rooms}->{$cc->{name}} = {}; my $last = $cc; my $chan_cnt = 2; while (@arg) { $cc->{name} = shift @arg; if ($cc->{name} =~ s/\cJ(\d+)$//) { $cc->{user_count} = $1; } if ($cc->{name} =~ m/^"/) { $cc->{name} = "$last->{name} $chan_cnt"; $chan_cnt++; } else { $last = $cc; $chan_cnt = 2; } $cc->{flag1} = shift @arg; $cc->{flag2} = shift @arg; my $i = 0; for (my $a = shift @arg; $a ne "-"; $a = shift @arg) { if ($i == 0) { $cc->{picture} = $a; $cc->{full_flag} = 1 if $cc->{picture} =~ m/full/i; } $i++; } $self->{knuddels_rooms}->{$cc->{name}} = $cc; $cc = {}; } $self->{proto}->feed_event (room_list => $self->{knuddels_rooms}); }); $self->register (d => sub { my $room = $self->handle_room ($_[0]); delete $self->{room}->{lc $room}; $self->{room}->{lc $_[1]} = { name => $_[1] }; $self->{proto}->feed_event (change_room => $room, $_[1]); }); $self->register ('6' => sub { # i have no exact clue what this message does, # but java-code seems to say i should do this: warn "*********************** SIX MESSAGE GOT!!! CHECK PROTOCOL FOR OCCURENCE!!"; # delete $self->{room}->{lc ($self->handle_room ($_[0]))}; }); $self->register (a => sub { $self->{my_nick} = $_[1]; # i'm really _not_ shure about this my $ri = $self->{room}->{lc $_[0]} = { name => $_[0], picture => $_[7], }; $self->{proto}->feed_event (room_info => $_[0], $ri); }); $self->register (u => sub { my $room = shift; my $rl = $self->{user_lists}->{lc $room} = {}; my $cur_u = {}; while (@_) { $cur_u->{name} = shift; $cur_u->{flag} = shift; $cur_u->{color} = shift; my $i = 0; while ((my $nxt = shift) ne "-") { if ($i == 0) { $cur_u->{picture} = $nxt; } $i++; } update_user_stats ($cur_u); $rl->{lc $cur_u->{name}} = $cur_u; $cur_u = {}; } $self->{proto}->feed_event (user_list => $room, $rl); }); $self } =item $client->fh Return the fh used for communications. You are responsible for calling C<< $client->ready >> whenever the fh becomes ready for reading. =cut sub fh { $_[0]->{fh} } =item $client->ready To be called then the filehandle is ready for reading. Returns false if the server closed the connection, true otherwise. =cut sub ready { my ($self) = @_; sysread $self->{fh}, my $buf, 8192 or return; $self->{proto}->feed_data ($buf); 1; } =item $client->command ($type => @args) Send a message of type C<$type> and the given arguments to the server, ensures a proper rate-limit. =cut sub command { my ($self, $type, @args) = @_; my $wait = $self->{next_command} - Time::HiRes::time; select undef, undef, undef, $wait if $wait > 0; $self->{next_command} = Time::HiRes::time + $self->{rate}; #use Dumpvalue; Dumpvalue->new (compactDump => 1, veryCompact => 1, quoteHighBit => 1, tick => '"')->dumpValue ([$type, @args]); syswrite $self->{fh}, $self->{proto}->encode_msg ($type, @args); } =item $client->login ($url, $unknown) Send a 't' message. The default for C<$url> is C and C<$unknown> is C<6>. =cut sub login { my ($self, $url, $unknown) = @_; $self->command ("t", "V8.6a", $url || "http://www.knuddels.de/applet.html?v=86a&c=0", $unknown || 3); } =item $client->enter_room ($room, $nick, $password) Enters a room C<$room> with C<$nick> and C<$password>. (for joining multiple rooms call this multiple times) NOTE: i won't allow joins to multiple rooms with different nick/password's, the java client reacted very confused.. don't know whether the server supports this. =cut sub enter_room { my ($self, $room, $nick, $password) = @_; if (defined $self->{nick} and $self->{nick} ne $nick) { return # i don't think knuddels-server will be happy if # we join multiple rooms on multiple accounts over 1 connection } exists $self->{login_challenge} or Carp::croak "set_nick can only be called after a login event"; $self->{nick} = $nick; $self->command ("n", $room, $nick, Net::Knuddels::hash_pw $self->{login_challenge}, $password); } =item $client->send_whois ($nick) Sends a whois-request for $nick. =cut sub send_whois { my ($self, $room, $nick) = @_; $self->command ("e", $room, "/w $nick"); } =item $client->send_room_msg ($nick, $room, $message) Sends a private C<$message> to C<$nick> over C<$room>. =cut sub send_room_msg { my ($self, $room, $message) = @_; $self->command ("e", $room, $message); } =item $client->send_priv_msg ($nick, $room, $message) Sends a private C<$message> to C<$nick> over C<$room>. =cut sub send_priv_msg { my ($self, $nick, $room, $message) = @_; $self->command ("e", $room, "/p $nick:$message"); } =item $client->send_join_room ($oldroom, $room) Sends the server a join command for C<$room>. This will result in a room change from C<$oldroom> to C<$room>. =cut sub send_join_room { my ($self, $old_room, $room) = @_; $self->command ("e", $old_room, "/go $room"); } =item $client->send_exit_room ($room) Exits C<$room> completly. (can be seen as counter method for C) =cut sub send_exit_room { my ($self, $room) = @_; $self->command ("w", $room, "\0", "\0"); delete $self->{room}->{lc $room}; } =item $client->register ($type => $cb) See L. The following extra events will be generated by this class: login set_nick can only be called _after_ a login event has occured. msg_room => $room, $user, $msg produced when a public message is uttered :) msg_priv => $room, $src, $dst, $msg personal message from $src to $dst. better use msg_priv_nondup, msg_priv_james or msg_priv_echo msg_priv_echo => $room, $src, $dst, $msg like msg_priv, but only for echoed messages msg_priv_james => $room, $src, $dst, $msg like msg_priv, but only for messages from James msg_priv_nondup => $room, $src, $dst, $msg like msg_priv, but avoids duplicate messages, echos and james. user_list => $room, $list the userlist of a channel named $room, a elmement of the list (a user) looks like: { name => , flag => , color => like /\d+.\d+.\d+/, age => /\d+/, gender => /(f|m)/, picture => } room_info => $room, $room_info some information about the $room: $room_info = { picture => } join_room => $room, $user join message of $user joined the room $room $user contains the user structure (see user_list). part_room => $room, $user part message of $user who left the room $room $user contains the user structure (see user_list). =cut sub register { my ($self, $type, $cb) = @_; $self->{proto}->register ($type, $cb); } =back =head1 AUTHOR Marc Lehmann http://home.schmorp.de/ =cut 1;