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