ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-Knuddels/Net/Knuddels.pm
Revision: 1.15
Committed: Thu Jan 13 14:52:37 2005 UTC (21 years, 8 months ago) by elmex
Branch: MAIN
Changes since 1.14: +131 -4 lines
Log Message:
Some new events

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    
25 root 1.14 sub _to32($) {
26     unpack "l", pack "L", (new Math::BigInt $_[0]) & 0xffffffff
27     }
28    
29 root 1.4 sub hash_pw($$) {
30     my ($challenge, $pw) = @_;
31    
32     my $l1 = length $pw;
33     my $l2 = length $challenge;
34    
35     my $k = chr ($l1 ^ ($l2 << 4));
36    
37     my $l = $l1 < $l2 ? $l2 : $l1;
38    
39     my $xor = substr +($pw x 100) ^ ($challenge x 100) ^ ($k x 100), 0, $l;
40    
41     my ($i, $j);
42    
43     --$l;
44    
45     if ($l <= 17) {
46     for (0 .. $l) {
47 root 1.14 $i = _to32 $i * 3 + ord substr $xor, $l - $_;
48     $j = _to32 $j * 5 + ord substr $xor, $_;
49 root 1.4 }
50     } else {
51     for ($_ = $l; $_ >= 0; $_ -= int $_/19) {
52 root 1.14 $i = _to32 $i * 5 + ord substr $xor, $_;
53     $j = _to32 $j * 3 + ord substr $xor, $l - $_;
54 root 1.4 }
55     }
56    
57     $i ^= $j;
58 root 1.14 _to32 (($i & 0xffffff) ^ ($i >> 24))
59 root 1.4 }
60    
61 root 1.10 my $RE_dec = join "|", keys %$Net::Knuddels::Dictionary;
62    
63     sub decode {
64     my $bin = unpack "b*", $_[0];
65     my $res = "";
66    
67     while ($bin =~ /\G($RE_dec)/cog) {
68     my $frag = $Net::Knuddels::Dictionary->{$1};
69 root 1.14 $frag = chr unpack "v", pack "b*", $bin =~ /\G(.{16})/cg && $1 if $frag eq "\\\\\\";
70 root 1.10 $res .= $frag;
71     }
72     $bin =~ /\G(.*[^0].*)$/ and die "Net::Knuddels::Receiver: undecodable message tail '$1'";
73    
74     $res
75     }
76    
77 root 1.9 my %encode = reverse %$Net::Knuddels::Dictionary;
78    
79 root 1.10 my $RE_enc = join "|", map quotemeta, sort { (length $b) <=> (length $a) } keys %encode;
80 root 1.9
81 root 1.10 sub encode($) {
82 root 1.9 my ($msg) = @_;
83    
84     my $data = "";
85    
86     while () {
87     $data .= $encode{$1} while $msg =~ /\G($RE_enc)/cog;
88    
89     $msg =~ /\G./csog
90     or last;
91    
92     $data .= $encode{"\\\\\\"} . unpack "b*", pack "v", ord $1;
93     }
94    
95     pack "b*", $data
96     }
97    
98 root 1.5 =head2 CLASS Net::Knuddels::Protocol
99    
100     You B<must> call the C<destroy> method of this class when you no longer
101     use it, as circular references will keep the object alive otherwise.
102    
103 root 1.6 =over 4
104    
105 root 1.8 =cut
106    
107     package Net::Knuddels::Protocol;
108    
109 root 1.6 =item new
110    
111     Create a new C<Net::Knuddels::Protocol> object.
112    
113 root 1.5 =cut
114 root 1.2
115 elmex 1.15 sub handle_room {
116     my ($self, $room) = @_;
117    
118     if ($room eq "-") {
119     if (defined $self->{only_room}) {
120     return $self->{only_room};
121     } else {
122     warn "Couldn't assign '-' room to a room!";
123     return '-';
124     }
125     } else {
126     return $room;
127     }
128     }
129    
130     sub calc_user_stats {
131     my ($self, $user) = @_;
132    
133     if ($user->{name} =~ s/\cJ(\d+)$//) {
134     $user->{age} = $1
135     }
136    
137     if ($user->{picture} =~ m/\bmale/) {
138     $user->{gender} = 'm';
139     } elsif ($user->{picture} =~ m/female/) {
140     $user->{gender} = 'f';
141     }
142     return $user;
143     }
144    
145 root 1.2 sub new {
146     my $class = shift;
147    
148 root 1.4 my %data;
149    
150     my $self = bless {
151     @_
152     }, $class;
153    
154     $self->register ("(" => sub {
155 root 1.14 $self->{login_challenge} = $_[0];
156     $self->{login_room} = $_[1];
157 root 1.13 $self->feed_event ("login");
158 root 1.4 });
159 root 1.14 $self->register (r => sub {
160 elmex 1.15 $self->feed_event (msg_priv => $self->handle_room ($_[2]), $_[0], $_[1], $_[3]);
161 root 1.14 });
162     $self->register (e => sub {
163 elmex 1.15 $self->feed_event (msg_room => $self->handle_room ($_[1]), $_[0], $_[2]);
164     });
165     $self->register (l => sub {
166     my $room = $self->handle_room ($_[0]);
167     return if $room eq "-"; # things that shouln't happen
168    
169     my $user = {
170     name => $_[1],
171     flag => $_[2],
172     color => $_[3],
173     picture => $_[4]
174     };
175    
176     $self->calc_user_stats ($user);
177    
178     my $rl = $self->{user_lists}->{lc $room}->{lc $user->{name}} = $user;
179    
180     $self->feed_event (join_room => $room, $user);
181     });
182     $self->register (w => sub {
183     my $room = $self->handle_room ($_[1]);
184     return if $room eq "-"; # things that shouln't happen
185    
186     my $username = $_[0];
187    
188     my $u = delete $self->{user_lists}->{lc $room}->{lc $username};
189    
190     if (not defined $u) {
191     warn "User $username wasn't in room $room, trying to fix... but be careful!!!\n";
192     $u = { name => $username };
193     }
194    
195     $self->feed_event (part_room => $room, $u);
196     });
197     $self->register (a => sub {
198     # the only_room stuff is from java-code, which has naughy semantics
199     if (not defined $self->{only_room}) {
200     $self->{only_room} = $_[0];
201     } else {
202     $self->{only_room} = "-";
203     }
204    
205     $self->{my_nick} = $_[1]; # i'm really _not_ shure about this
206    
207     my $ri = $self->{room}->{lc $_[0]} = {
208     picture => $_[7],
209     };
210    
211     $self->feed_event (room_info => $_[0], $ri);
212     });
213     $self->register (u => sub {
214     my $room = shift;
215     my $rl = $self->{user_lists}->{lc $room} = {};
216     my $cur_u = {};
217    
218     while (@_) {
219     $cur_u->{name} = shift;
220     $cur_u->{flag} = shift;
221     $cur_u->{color} = shift;
222    
223     my $i = 0;
224    
225     while ((my $nxt = shift) ne "-") {
226     if ($i == 0) {
227     $cur_u->{picture} = $nxt;
228     }
229     $i++;
230     }
231    
232     $self->calc_user_stats ($cur_u);
233     $rl->{lc $cur_u->{name}} = $cur_u;
234     $cur_u = {};
235     }
236     $self->feed_event (user_list => $room, $rl);
237 root 1.14 });
238 root 1.4
239     $self;
240 root 1.2 }
241    
242 root 1.6 =item $protocol->feed_data ($octets)
243    
244     Feed raw protocol data into the decoder.
245    
246     =cut
247    
248 root 1.2 sub feed_data($$) {
249     my ($self, $data) = @_;
250    
251     # split data stream into packets
252    
253     $data = "$self->{rbuf}$data";
254    
255     while () {
256     1 <= length $data or last;
257     my $len = ord substr $data, 0, 1;
258    
259     my $skip;
260     if ($len & 0x80) {
261     my $tail = (($len >> 5) & 3) - 1;
262     $len = ($len & 0x1f) + 1;
263    
264     $tail < length $data or last;
265     $len += (ord substr $data, $_ + 1, 1) << ($_ * 8 + 5)
266     for 0 .. $tail;
267    
268     $skip = 2 + $tail;
269     } else {
270     $skip = 1;
271     $len++;
272     }
273    
274     $len + $skip <= length $data or last;
275     substr $data, 0, $skip, "";
276     my $msg = substr $data, 0, $len, "";
277    
278     $self->feed_msg ($msg);
279     }
280    
281     $self->{rbuf} = $data;
282     }
283    
284     sub feed_msg($$) {
285     my ($self, $msg) = @_;
286 root 1.1
287 root 1.10 $self->feed_event (split /\0/, Net::Knuddels::decode $msg);
288 root 1.1 }
289    
290 root 1.2 sub feed_event($@) {
291 root 1.14 my ($self, @cmd) = @_;
292 root 1.1
293 root 1.14 my $ev = $self->{cb}{ALL};
294     $_->(@cmd) for values %$ev;
295    
296     unless ($self->{cb}{$cmd[0]}) {
297     my $ev = $self->{cb}{UNHANDLED};
298     $_->(@cmd) for values %$ev;
299 root 1.1 }
300 root 1.14
301     my $ev = $self->{cb}{shift @cmd};
302     $_->(@cmd) for values %$ev;
303 root 1.2 }
304 root 1.1
305 root 1.11 =item $msg = $protocol->encode_msg (@strings)
306    
307     Join the strings with C<\0>, encode the result into a protocol packet and
308     return it.
309    
310     =cut
311    
312     sub encode_msg($@) {
313     my ($self, @args) = @_;
314     my $msg = Net::Knuddels::encode join "\0", @args;
315 root 1.12 my $len = (length $msg) - 1;
316    
317     if ($len < 0x80) {
318     (chr $len) . $msg
319     } else {
320     (chr 0x80 | 0x40 | ($len & 0x1f))
321     . (chr +($len >> 5) % 0xff)
322     . (chr +($len >> 13) % 0xff)
323     . $msg
324     }
325 root 1.11 }
326    
327 root 1.6 =item $protocol->register ($type => $callback)
328    
329     Register a callback for events of type C<$type>, which is either the name
330     of a low-level event sent by the server (such as "k" for dialog box) or
331 root 1.8 the name of a generated event, such as C<login_info>.
332 root 1.6
333 root 1.14 The following events will be generated:
334    
335     login
336     set_nick can only be called _after_ a login event has occured.
337    
338     msg_room => $room, $user, $msg
339     produced when a public message is uttered :)
340    
341     msg_room => $room, $src, $dst, $msg
342     personal message from $src to $dst
343    
344 elmex 1.15 user_list => $room, $list
345     the userlist of a channel named $room, a elmement of the list (a user)
346     looks like:
347     {
348     name => <name>,
349     flag => <some flag i don't know what it means>,
350     color => like /\d+.\d+.\d+/,
351     age => /\d+/,
352     gender => /(f|m)/,
353     picture => <the picture file to put behind the nick>
354     }
355    
356     room_info => $room, $room_info
357     some information about the $room:
358     $room_info =
359     {
360     picture => <some picturefile>
361     }
362    
363     join_room => $room, $user
364     join message of $user joined the room $room
365     $user contains the user structure (see user_list).
366    
367     part_room => $room, $user
368     part message of $user who left the room $room
369     $user contains the user structure (see user_list).
370 root 1.6 =cut
371    
372 root 1.2 sub register {
373     my ($self, $type, $cb) = @_;
374 root 1.1
375 root 1.2 $self->{cb}{$type}{$cb} = $cb;
376 root 1.1 }
377    
378 root 1.8 =item $protocol->destroy
379    
380 root 1.9 I<MUST> be called to destroy the object, otherwise it will leak (no automatic cleanup).
381 root 1.8
382     =cut
383    
384 root 1.5 sub destroy {
385     my ($self) = @_;
386    
387     delete $self->{cb};
388     }
389    
390 root 1.6 =back
391    
392 root 1.8 =head2 CLASS Net::Knuddels::Client
393    
394 root 1.9 Implement a Knuddels client connection.
395    
396 root 1.8 =over 4
397    
398     =cut
399    
400     package Net::Knuddels::Client;
401    
402 root 1.9 =item new Net::Knuddels::Client [IO::Socket::new arguments]
403    
404     Create a new client connection.
405    
406     =cut
407    
408     use IO::Socket::INET;
409    
410     sub new {
411     my ($class, @arg) = @_;
412    
413     my $fh = new IO::Socket::INET @arg
414     or Carp::croak "Net::Knuddels::Client::new: $!";
415    
416     my $self = bless {
417     fh => $fh,
418     proto => (new Net::Knuddels::Protocol),
419     }, $class;
420    
421     syswrite $fh, "\0";
422    
423     $self
424     }
425    
426     =item $client->fh
427    
428     Return the fh used for communications. You are responsible for calling C<<
429 root 1.13 $client->ready >> whenever the fh becomes ready for reading.
430 root 1.9
431     =cut
432    
433     sub fh {
434     $_[0]->{fh}
435     }
436    
437 root 1.13 =item $client->ready
438    
439     To be called then the filehandle is ready for reading. Returns false if
440     the server closed the connection, true otherwise.
441    
442     =cut
443    
444     sub ready {
445     my ($self) = @_;
446    
447     sysread $self->{fh}, my $buf, 8192
448     or return;
449    
450     $self->{proto}->feed_data ($buf);
451    
452     1;
453     }
454    
455 root 1.9 =item $client->command ($type => @args)
456    
457     Send a message of type C<$type> and the given arguments to the server.
458    
459     =cut
460    
461     sub command {
462     my ($self, $type, @args) = @_;
463    
464 root 1.14 #use Dumpvalue; Dumpvalue->new (compactDump => 1, veryCompact => 1, quoteHighBit => 1, tick => '"')->dumpValue ([$type, @args]);
465 root 1.13
466     syswrite $self->{fh}, $self->{proto}->encode_msg ($type, @args);
467 root 1.9 }
468    
469     =item $client->login ($url, $unknown)
470    
471     Send a 't' message. The default for C<$url> is
472     C<http://www.knuddels.de/applet.html?v=86a&c=0> and C<$unknown> is C<6>.
473    
474     =cut
475    
476     sub login {
477 root 1.13 my ($self, $url, $unknown) = @_;
478    
479     $self->command ("t", "V8.6a", $url || "http://www.knuddels.de/applet.html?v=86a&c=0", $unknown || 3);
480     }
481    
482     =item $client->set_nick ($room, $nick, $password)
483    
484     Registers the nick with the given password.
485    
486     =cut
487    
488     sub set_nick {
489     my ($self, $room, $nick, $password) = @_;
490    
491     exists $self->{proto}{login_challenge} or Carp::croak "set_nick can only be called after a login event";
492    
493     $self->command ("n", $room, $nick, Net::Knuddels::hash_pw $self->{proto}{login_challenge}, $password);
494 root 1.9 }
495    
496     =item $client->register ($type => $cb)
497    
498     See L<Net::Knuddels::Protocol::register>.
499    
500     =cut
501    
502     sub register {
503     my ($self, $type, $cb) = @_;
504    
505 root 1.13 $self->{proto}->register ($type, $cb);
506 root 1.9 }
507    
508 root 1.8 =back
509    
510     =head1 AUTHOR
511    
512     Marc Lehmann <pcg@goof.com>
513     http://home.schmorp.de/
514    
515 root 1.6 =cut
516    
517 root 1.2 1;
518