ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-Knuddels/Net/Knuddels.pm
Revision: 1.28
Committed: Sat Jan 29 03:20:32 2005 UTC (19 years, 4 months ago) by root
Branch: MAIN
Changes since 1.27: +3 -4 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.8 =head1 NAME
2    
3     Net::Knuddels - www.knuddels.de protocol implementation.
4    
5     =head1 SYNOPSIS
6    
7     use Net::Knuddels;
8    
9     =head1 DESCRIPTION
10    
11     RTSL.
12    
13     =cut
14    
15 root 1.1 package Net::Knuddels;
16    
17 root 1.3 use Net::Knuddels::Dictionary;
18    
19 root 1.2 use strict;
20     use utf8;
21    
22 root 1.4 use Carp;
23     use Math::BigInt;
24 root 1.27 use Time::HiRes;
25 root 1.4
26 root 1.14 sub _to32($) {
27     unpack "l", pack "L", (new Math::BigInt $_[0]) & 0xffffffff
28     }
29    
30 root 1.4 sub hash_pw($$) {
31     my ($challenge, $pw) = @_;
32    
33     my $l1 = length $pw;
34     my $l2 = length $challenge;
35    
36     my $k = chr ($l1 ^ ($l2 << 4));
37    
38     my $l = $l1 < $l2 ? $l2 : $l1;
39    
40     my $xor = substr +($pw x 100) ^ ($challenge x 100) ^ ($k x 100), 0, $l;
41    
42     my ($i, $j);
43    
44     --$l;
45    
46     if ($l <= 17) {
47     for (0 .. $l) {
48 root 1.25 $i = _to32 $i * 3 + ord substr $xor, $l - $_;
49     $j = _to32 $j * 5 + ord substr $xor, $_;
50 root 1.4 }
51     } else {
52     for ($_ = $l; $_ >= 0; $_ -= int $_/19) {
53 root 1.25 $i = _to32 $i * 5 + ord substr $xor, $_;
54     $j = _to32 $j * 3 + ord substr $xor, $l - $_;
55 root 1.4 }
56     }
57    
58     $i ^= $j;
59 root 1.14 _to32 (($i & 0xffffff) ^ ($i >> 24))
60 root 1.4 }
61    
62 root 1.10 my $RE_dec = join "|", keys %$Net::Knuddels::Dictionary;
63    
64     sub decode {
65     my $bin = unpack "b*", $_[0];
66     my $res = "";
67    
68     while ($bin =~ /\G($RE_dec)/cog) {
69     my $frag = $Net::Knuddels::Dictionary->{$1};
70 root 1.14 $frag = chr unpack "v", pack "b*", $bin =~ /\G(.{16})/cg && $1 if $frag eq "\\\\\\";
71 root 1.10 $res .= $frag;
72     }
73     $bin =~ /\G(.*[^0].*)$/ and die "Net::Knuddels::Receiver: undecodable message tail '$1'";
74    
75     $res
76     }
77    
78 root 1.9 my %encode = reverse %$Net::Knuddels::Dictionary;
79    
80 root 1.10 my $RE_enc = join "|", map quotemeta, sort { (length $b) <=> (length $a) } keys %encode;
81 root 1.9
82 root 1.10 sub encode($) {
83 root 1.9 my ($msg) = @_;
84    
85     my $data = "";
86    
87     while () {
88     $data .= $encode{$1} while $msg =~ /\G($RE_enc)/cog;
89    
90     $msg =~ /\G./csog
91     or last;
92    
93     $data .= $encode{"\\\\\\"} . unpack "b*", pack "v", ord $1;
94     }
95    
96     pack "b*", $data
97     }
98    
99 root 1.5 =head2 CLASS Net::Knuddels::Protocol
100    
101     You B<must> call the C<destroy> method of this class when you no longer
102     use it, as circular references will keep the object alive otherwise.
103    
104 root 1.6 =over 4
105    
106 root 1.8 =cut
107    
108     package Net::Knuddels::Protocol;
109    
110 root 1.6 =item new
111    
112     Create a new C<Net::Knuddels::Protocol> object.
113    
114 root 1.5 =cut
115 root 1.2
116     sub new {
117     my $class = shift;
118    
119 root 1.4 my %data;
120    
121     my $self = bless {
122     @_
123     }, $class;
124    
125     $self;
126 root 1.2 }
127    
128 root 1.6 =item $protocol->feed_data ($octets)
129    
130     Feed raw protocol data into the decoder.
131    
132     =cut
133    
134 root 1.2 sub feed_data($$) {
135     my ($self, $data) = @_;
136    
137     # split data stream into packets
138    
139     $data = "$self->{rbuf}$data";
140    
141     while () {
142     1 <= length $data or last;
143     my $len = ord substr $data, 0, 1;
144    
145     my $skip;
146     if ($len & 0x80) {
147     my $tail = (($len >> 5) & 3) - 1;
148     $len = ($len & 0x1f) + 1;
149    
150     $tail < length $data or last;
151     $len += (ord substr $data, $_ + 1, 1) << ($_ * 8 + 5)
152     for 0 .. $tail;
153    
154     $skip = 2 + $tail;
155     } else {
156     $skip = 1;
157     $len++;
158     }
159    
160     $len + $skip <= length $data or last;
161     substr $data, 0, $skip, "";
162     my $msg = substr $data, 0, $len, "";
163    
164     $self->feed_msg ($msg);
165     }
166    
167     $self->{rbuf} = $data;
168     }
169    
170     sub feed_msg($$) {
171     my ($self, $msg) = @_;
172 root 1.1
173 root 1.10 $self->feed_event (split /\0/, Net::Knuddels::decode $msg);
174 root 1.1 }
175    
176 root 1.2 sub feed_event($@) {
177 root 1.14 my ($self, @cmd) = @_;
178 root 1.1
179 root 1.14 my $ev = $self->{cb}{ALL};
180     $_->(@cmd) for values %$ev;
181    
182     unless ($self->{cb}{$cmd[0]}) {
183     my $ev = $self->{cb}{UNHANDLED};
184     $_->(@cmd) for values %$ev;
185 root 1.1 }
186 root 1.14
187     my $ev = $self->{cb}{shift @cmd};
188     $_->(@cmd) for values %$ev;
189 root 1.2 }
190 root 1.1
191 root 1.11 =item $msg = $protocol->encode_msg (@strings)
192    
193     Join the strings with C<\0>, encode the result into a protocol packet and
194     return it.
195    
196     =cut
197    
198     sub encode_msg($@) {
199     my ($self, @args) = @_;
200     my $msg = Net::Knuddels::encode join "\0", @args;
201 root 1.12 my $len = (length $msg) - 1;
202    
203     if ($len < 0x80) {
204     (chr $len) . $msg
205     } else {
206     (chr 0x80 | 0x40 | ($len & 0x1f))
207     . (chr +($len >> 5) % 0xff)
208     . (chr +($len >> 13) % 0xff)
209     . $msg
210     }
211 root 1.11 }
212    
213 root 1.6 =item $protocol->register ($type => $callback)
214    
215     Register a callback for events of type C<$type>, which is either the name
216     of a low-level event sent by the server (such as "k" for dialog box) or
217 root 1.16 the name of a generated event, such as C<login>.
218 root 1.6
219     =cut
220    
221 root 1.2 sub register {
222     my ($self, $type, $cb) = @_;
223 root 1.1
224 root 1.2 $self->{cb}{$type}{$cb} = $cb;
225 root 1.1 }
226    
227 root 1.8 =item $protocol->destroy
228    
229 root 1.9 I<MUST> be called to destroy the object, otherwise it will leak (no automatic cleanup).
230 root 1.8
231     =cut
232    
233 root 1.5 sub destroy {
234     my ($self) = @_;
235    
236     delete $self->{cb};
237     }
238    
239 root 1.6 =back
240    
241 root 1.8 =head2 CLASS Net::Knuddels::Client
242    
243 root 1.9 Implement a Knuddels client connection.
244    
245 root 1.8 =over 4
246    
247     =cut
248    
249     package Net::Knuddels::Client;
250    
251 root 1.16 sub handle_room {
252     my ($self, $room) = @_;
253    
254     if ($room eq "-") {
255 elmex 1.23 if (scalar (keys %{$self->{room}}) == 1) {
256     return (keys %{$self->{room}})[0];
257 root 1.16 } else {
258     warn "Couldn't assign '-' room to a room!";
259 elmex 1.23 return '#nosuchroom';
260 root 1.16 }
261     } else {
262     return $room;
263     }
264     }
265    
266 elmex 1.20 sub update_user_stats {
267     my ($user) = @_;
268 root 1.16
269     if ($user->{name} =~ s/\cJ(\d+)$//) {
270     $user->{age} = $1
271     }
272    
273     if ($user->{picture} =~ m/\bmale/) {
274     $user->{gender} = 'm';
275 elmex 1.20
276 root 1.16 } elsif ($user->{picture} =~ m/female/) {
277     $user->{gender} = 'f';
278     }
279 root 1.19
280 root 1.16 return $user;
281     }
282    
283 elmex 1.22 sub del1 {
284     my ($str) = @_;
285     my $s = substr ($$str, 0, 1);
286     $$str = substr ($$str, 1);
287     $s
288     }
289    
290     sub del2 {
291     my ($str) = @_;
292     my $s = substr ($$str, 0, 2);
293     $$str = substr ($$str, 2);
294     $s
295     }
296    
297     sub todelim {
298     my ($str) = @_;
299     $$str =~ s/^(.*?)\365//;
300     $1;
301     }
302    
303     sub chk_flag {
304     my ($str) = @_;
305     if ($$str =~ s/^\343//) {
306     return 1;
307     }
308     return 0;
309     }
310    
311 root 1.9 =item new Net::Knuddels::Client [IO::Socket::new arguments]
312    
313     Create a new client connection.
314    
315     =cut
316    
317     use IO::Socket::INET;
318    
319     sub new {
320     my ($class, @arg) = @_;
321    
322     my $fh = new IO::Socket::INET @arg
323     or Carp::croak "Net::Knuddels::Client::new: $!";
324    
325     my $self = bless {
326 root 1.27 queue => [],
327 root 1.9 fh => $fh,
328     proto => (new Net::Knuddels::Protocol),
329 root 1.28 rate => 1, # commands/s
330 root 1.27 @arg,
331 root 1.9 }, $class;
332    
333     syswrite $fh, "\0";
334    
335 root 1.16 $self->register ("(" => sub {
336     $self->{login_challenge} = $_[0];
337     $self->{login_room} = $_[1];
338 elmex 1.23 $self->{proto}->feed_event (login => $_[1]);
339 root 1.16 });
340 elmex 1.23
341 elmex 1.22 $self->register (k => sub {
342     my @str = map { s/[\356\343]//; $_ } @_;
343     my @out;
344     push @out, split /#/, $_ for @str;
345     $self->{proto}->feed_event (dialog => \@out);
346     });
347 elmex 1.23
348 elmex 1.21 $self->register (t => sub {
349     my $src = $_[0];
350 elmex 1.23
351 elmex 1.21 if ($src eq '-') {
352 root 1.27 $_[2] = $self->{nick} . " " . $_[2];
353 elmex 1.23
354 elmex 1.21 } else {
355     $_[2] = $src . " " . $_[2];
356     }
357     $self->{proto}->feed_event (action_room => $self->handle_room ($_[1]), $_[2]);
358     });
359 elmex 1.23
360 root 1.27 my %last_msg; # the last message of a user, to avoid duplicates
361    
362 root 1.16 $self->register (r => sub {
363 elmex 1.21 my $src = $_[0];
364 root 1.27 $src = $self->{nick} if $src eq "-";
365    
366 elmex 1.21 $self->{proto}->feed_event (msg_priv => $self->handle_room ($_[2]), $src, $_[1], $_[3]);
367 root 1.27
368     if ($src eq "James") {
369     $self->{proto}->feed_event (msg_priv_james => $self->handle_room ($_[2]), $src, $_[1], $_[3]);
370     } elsif ($src eq $self->{nick}) {
371     $self->{proto}->feed_event (msg_priv_echo => $self->handle_room ($_[2]), $src, $_[1], $_[3]);
372     } else {
373     $self->{proto}->feed_event (msg_priv_nondup => $self->handle_room ($_[2]), $src, $_[1], $_[3])
374     if $last_msg{$src} ne $_[3];
375     $last_msg{$src} = $_[3];
376     }
377 root 1.16 });
378 root 1.27
379 root 1.16 $self->register (e => sub {
380     $self->{proto}->feed_event (msg_room => $self->handle_room ($_[1]), $_[0], $_[2]);
381     });
382 elmex 1.23
383     $self->register (l => sub {
384 root 1.16 my $room = $self->handle_room ($_[0]);
385     return if $room eq "-"; # things that shouln't happen
386    
387     my $user = {
388     name => $_[1],
389     flag => $_[2],
390     color => $_[3],
391     picture => $_[4]
392     };
393    
394 elmex 1.20 update_user_stats ($user);
395 root 1.16
396     my $rl = $self->{user_lists}->{lc $room}->{lc $user->{name}} = $user;
397    
398     $self->{proto}->feed_event (join_room => $room, $user);
399     });
400 root 1.27
401 root 1.16 $self->register (w => sub {
402     my $room = $self->handle_room ($_[1]);
403     return if $room eq "-"; # things that shouln't happen
404    
405     my $username = $_[0];
406    
407     my $u = delete $self->{user_lists}->{lc $room}->{lc $username};
408    
409     if (not defined $u) {
410     warn "User $username wasn't in room $room, trying to fix... but be careful!!!\n";
411     $u = { name => $username };
412     }
413    
414     $self->{proto}->feed_event (part_room => $room, $u);
415     });
416 elmex 1.23
417     $self->register (b => sub {
418     my @arg = @_;
419     my $cc = {};
420     $self->{knuddels_rooms}->{$cc->{name}} = {};
421    
422     my $last = $cc;
423     my $chan_cnt = 2;
424    
425     while (@arg) {
426     $cc->{name} = shift @arg;
427    
428     if ($cc->{name} =~ s/\cJ(\d+)$//) {
429     $cc->{user_count} = $1;
430     }
431 root 1.27
432 elmex 1.23 if ($cc->{name} =~ m/^"/) {
433     $cc->{name} = "$last->{name} $chan_cnt";
434     $chan_cnt++;
435    
436     } else {
437     $last = $cc;
438     $chan_cnt = 2;
439     }
440    
441     $cc->{flag1} = shift @arg;
442     $cc->{flag2} = shift @arg;
443    
444     my $i = 0;
445    
446     for (my $a = shift @arg; $a ne "-"; $a = shift @arg) {
447    
448     if ($i == 0) {
449     $cc->{picture} = $a;
450     $cc->{full_flag} = 1 if $cc->{picture} =~ m/full/i;
451     }
452     $i++;
453     }
454    
455     $self->{knuddels_rooms}->{$cc->{name}} = $cc;
456     $cc = {};
457 root 1.16 }
458    
459 elmex 1.23 $self->{proto}->feed_event (room_list => $self->{knuddels_rooms});
460     });
461    
462     $self->register (d => sub {
463     my $room = $self->handle_room ($_[0]);
464    
465     delete $self->{room}->{lc $room};
466     $self->{room}->{lc $_[1]} = { name => $_[1] };
467    
468     $self->{proto}->feed_event (change_room => $room, $_[1]);
469     });
470    
471     $self->register ('6' => sub {
472     # i have no exact clue what this message does,
473     # but java-code seems to say i should do this:
474    
475     warn "*********************** SIX MESSAGE GOT!!! CHECK PROTOCOL FOR OCCURENCE!!";
476     # delete $self->{room}->{lc ($self->handle_room ($_[0]))};
477     });
478    
479     $self->register (a => sub {
480 root 1.16 $self->{my_nick} = $_[1]; # i'm really _not_ shure about this
481    
482     my $ri = $self->{room}->{lc $_[0]} = {
483 elmex 1.23 name => $_[0],
484 root 1.16 picture => $_[7],
485     };
486    
487     $self->{proto}->feed_event (room_info => $_[0], $ri);
488     });
489 elmex 1.23
490 root 1.16 $self->register (u => sub {
491     my $room = shift;
492     my $rl = $self->{user_lists}->{lc $room} = {};
493     my $cur_u = {};
494    
495     while (@_) {
496     $cur_u->{name} = shift;
497     $cur_u->{flag} = shift;
498     $cur_u->{color} = shift;
499    
500     my $i = 0;
501    
502     while ((my $nxt = shift) ne "-") {
503     if ($i == 0) {
504     $cur_u->{picture} = $nxt;
505     }
506     $i++;
507     }
508    
509 elmex 1.20 update_user_stats ($cur_u);
510 root 1.16 $rl->{lc $cur_u->{name}} = $cur_u;
511     $cur_u = {};
512     }
513     $self->{proto}->feed_event (user_list => $room, $rl);
514     });
515    
516 root 1.9 $self
517     }
518    
519     =item $client->fh
520    
521     Return the fh used for communications. You are responsible for calling C<<
522 root 1.13 $client->ready >> whenever the fh becomes ready for reading.
523 root 1.9
524     =cut
525    
526     sub fh {
527     $_[0]->{fh}
528     }
529    
530 root 1.13 =item $client->ready
531    
532     To be called then the filehandle is ready for reading. Returns false if
533     the server closed the connection, true otherwise.
534    
535     =cut
536    
537     sub ready {
538     my ($self) = @_;
539    
540     sysread $self->{fh}, my $buf, 8192
541     or return;
542    
543     $self->{proto}->feed_data ($buf);
544    
545     1;
546     }
547    
548 root 1.9 =item $client->command ($type => @args)
549    
550 root 1.27 Send a message of type C<$type> and the given arguments to the server,
551     ensures a proper rate-limit.
552 root 1.9
553     =cut
554    
555     sub command {
556     my ($self, $type, @args) = @_;
557    
558 root 1.28 my $wait = $self->{next_command} - Time::HiRes::time;
559 root 1.27 select undef, undef, undef, $wait if $wait > 0;
560 root 1.28 $self->{next_command} = Time::HiRes::time + $self->{rate};
561 root 1.27
562 root 1.14 #use Dumpvalue; Dumpvalue->new (compactDump => 1, veryCompact => 1, quoteHighBit => 1, tick => '"')->dumpValue ([$type, @args]);
563 root 1.13 syswrite $self->{fh}, $self->{proto}->encode_msg ($type, @args);
564 root 1.9 }
565    
566     =item $client->login ($url, $unknown)
567    
568     Send a 't' message. The default for C<$url> is
569     C<http://www.knuddels.de/applet.html?v=86a&c=0> and C<$unknown> is C<6>.
570    
571     =cut
572    
573     sub login {
574 root 1.13 my ($self, $url, $unknown) = @_;
575    
576     $self->command ("t", "V8.6a", $url || "http://www.knuddels.de/applet.html?v=86a&c=0", $unknown || 3);
577     }
578    
579 elmex 1.23 =item $client->enter_room ($room, $nick, $password)
580    
581 root 1.27 Enters a room C<$room> with C<$nick> and C<$password>. (for joining
582     multiple rooms call this multiple times)
583 root 1.13
584 root 1.27 NOTE: i won't allow joins to multiple rooms with different
585     nick/password's, the java client reacted very confused.. don't know
586     whether the server supports this.
587 root 1.13
588     =cut
589    
590 elmex 1.23 sub enter_room {
591 root 1.13 my ($self, $room, $nick, $password) = @_;
592    
593 root 1.27 if (defined $self->{nick} and $self->{nick} ne $nick) {
594 elmex 1.23 return # i don't think knuddels-server will be happy if
595     # we join multiple rooms on multiple accounts over 1 connection
596     }
597    
598 root 1.16 exists $self->{login_challenge} or Carp::croak "set_nick can only be called after a login event";
599 root 1.13
600 root 1.27 $self->{nick} = $nick;
601 root 1.16 $self->command ("n", $room, $nick, Net::Knuddels::hash_pw $self->{login_challenge}, $password);
602 root 1.9 }
603    
604 elmex 1.22 =item $client->send_whois ($nick)
605    
606 root 1.27 Sends a whois-request for $nick.
607 elmex 1.22
608     =cut
609 root 1.27
610 elmex 1.22 sub send_whois {
611     my ($self, $room, $nick) = @_;
612    
613     $self->command ("e", $room, "/w $nick");
614     }
615    
616    
617 elmex 1.21 =item $client->send_room_msg ($nick, $room, $message)
618    
619 root 1.27 Sends a private C<$message> to C<$nick> over C<$room>.
620 elmex 1.21
621     =cut
622 root 1.27
623 elmex 1.21 sub send_room_msg {
624     my ($self, $room, $message) = @_;
625    
626     $self->command ("e", $room, $message);
627     }
628    
629    
630     =item $client->send_priv_msg ($nick, $room, $message)
631    
632 root 1.27 Sends a private C<$message> to C<$nick> over C<$room>.
633 elmex 1.21
634     =cut
635 root 1.27
636 elmex 1.21 sub send_priv_msg {
637     my ($self, $nick, $room, $message) = @_;
638    
639     $self->command ("e", $room, "/p $nick:$message");
640     }
641    
642 elmex 1.23 =item $client->send_join_room ($oldroom, $room)
643    
644 root 1.27 Sends the server a join command for C<$room>. This will result in a room
645     change from C<$oldroom> to C<$room>.
646 elmex 1.23
647     =cut
648 root 1.27
649 elmex 1.23 sub send_join_room {
650     my ($self, $old_room, $room) = @_;
651    
652     $self->command ("e", $old_room, "/go $room");
653     }
654    
655     =item $client->send_exit_room ($room)
656    
657 root 1.27 Exits C<$room> completly. (can be seen as counter method for C<enter_room ()>)
658 elmex 1.23
659     =cut
660 root 1.27
661 elmex 1.23 sub send_exit_room {
662     my ($self, $room) = @_;
663     $self->command ("w", $room, "\0", "\0");
664     delete $self->{room}->{lc $room};
665     }
666    
667 root 1.9 =item $client->register ($type => $cb)
668    
669 root 1.16 See L<Net::Knuddels::Protocol::register>. The following extra events will
670     be generated by this class:
671 root 1.9
672 root 1.16 login
673     set_nick can only be called _after_ a login event has occured.
674    
675     msg_room => $room, $user, $msg
676     produced when a public message is uttered :)
677    
678 root 1.27 msg_priv => $room, $src, $dst, $msg
679     personal message from $src to $dst. better use msg_priv_nondup,
680     msg_priv_james or msg_priv_echo
681    
682     msg_priv_echo => $room, $src, $dst, $msg
683     like msg_priv, but only for echoed messages
684    
685     msg_priv_james => $room, $src, $dst, $msg
686     like msg_priv, but only for messages from James
687    
688     msg_priv_nondup => $room, $src, $dst, $msg
689     like msg_priv, but avoids duplicate messages, echos and james.
690 root 1.16
691     user_list => $room, $list
692     the userlist of a channel named $room, a elmement of the list (a user)
693     looks like:
694     {
695     name => <name>,
696     flag => <some flag i don't know what it means>,
697     color => like /\d+.\d+.\d+/,
698     age => /\d+/,
699     gender => /(f|m)/,
700     picture => <the picture file to put behind the nick>
701     }
702    
703     room_info => $room, $room_info
704     some information about the $room:
705     $room_info =
706     {
707     picture => <some picturefile>
708     }
709    
710     join_room => $room, $user
711     join message of $user joined the room $room
712     $user contains the user structure (see user_list).
713    
714     part_room => $room, $user
715     part message of $user who left the room $room
716     $user contains the user structure (see user_list).
717 root 1.9 =cut
718    
719     sub register {
720     my ($self, $type, $cb) = @_;
721    
722 root 1.13 $self->{proto}->register ($type, $cb);
723 root 1.9 }
724    
725 root 1.8 =back
726    
727     =head1 AUTHOR
728    
729     Marc Lehmann <pcg@goof.com>
730     http://home.schmorp.de/
731    
732 root 1.6 =cut
733    
734 root 1.2 1;
735