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