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 (19 years, 4 months ago) by elmex
Branch: MAIN
Changes since 1.14: +131 -4 lines
Log Message:
Some new events

File Contents

# Content
1 =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 package Net::Knuddels;
16
17 use Net::Knuddels::Dictionary;
18
19 use strict;
20 use utf8;
21
22 use Carp;
23 use Math::BigInt;
24
25 sub _to32($) {
26 unpack "l", pack "L", (new Math::BigInt $_[0]) & 0xffffffff
27 }
28
29 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 $i = _to32 $i * 3 + ord substr $xor, $l - $_;
48 $j = _to32 $j * 5 + ord substr $xor, $_;
49 }
50 } else {
51 for ($_ = $l; $_ >= 0; $_ -= int $_/19) {
52 $i = _to32 $i * 5 + ord substr $xor, $_;
53 $j = _to32 $j * 3 + ord substr $xor, $l - $_;
54 }
55 }
56
57 $i ^= $j;
58 _to32 (($i & 0xffffff) ^ ($i >> 24))
59 }
60
61 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 $frag = chr unpack "v", pack "b*", $bin =~ /\G(.{16})/cg && $1 if $frag eq "\\\\\\";
70 $res .= $frag;
71 }
72 $bin =~ /\G(.*[^0].*)$/ and die "Net::Knuddels::Receiver: undecodable message tail '$1'";
73
74 $res
75 }
76
77 my %encode = reverse %$Net::Knuddels::Dictionary;
78
79 my $RE_enc = join "|", map quotemeta, sort { (length $b) <=> (length $a) } keys %encode;
80
81 sub encode($) {
82 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 =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 =over 4
104
105 =cut
106
107 package Net::Knuddels::Protocol;
108
109 =item new
110
111 Create a new C<Net::Knuddels::Protocol> object.
112
113 =cut
114
115 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 sub new {
146 my $class = shift;
147
148 my %data;
149
150 my $self = bless {
151 @_
152 }, $class;
153
154 $self->register ("(" => sub {
155 $self->{login_challenge} = $_[0];
156 $self->{login_room} = $_[1];
157 $self->feed_event ("login");
158 });
159 $self->register (r => sub {
160 $self->feed_event (msg_priv => $self->handle_room ($_[2]), $_[0], $_[1], $_[3]);
161 });
162 $self->register (e => sub {
163 $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 });
238
239 $self;
240 }
241
242 =item $protocol->feed_data ($octets)
243
244 Feed raw protocol data into the decoder.
245
246 =cut
247
248 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
287 $self->feed_event (split /\0/, Net::Knuddels::decode $msg);
288 }
289
290 sub feed_event($@) {
291 my ($self, @cmd) = @_;
292
293 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 }
300
301 my $ev = $self->{cb}{shift @cmd};
302 $_->(@cmd) for values %$ev;
303 }
304
305 =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 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 }
326
327 =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 the name of a generated event, such as C<login_info>.
332
333 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 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 =cut
371
372 sub register {
373 my ($self, $type, $cb) = @_;
374
375 $self->{cb}{$type}{$cb} = $cb;
376 }
377
378 =item $protocol->destroy
379
380 I<MUST> be called to destroy the object, otherwise it will leak (no automatic cleanup).
381
382 =cut
383
384 sub destroy {
385 my ($self) = @_;
386
387 delete $self->{cb};
388 }
389
390 =back
391
392 =head2 CLASS Net::Knuddels::Client
393
394 Implement a Knuddels client connection.
395
396 =over 4
397
398 =cut
399
400 package Net::Knuddels::Client;
401
402 =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 $client->ready >> whenever the fh becomes ready for reading.
430
431 =cut
432
433 sub fh {
434 $_[0]->{fh}
435 }
436
437 =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 =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 #use Dumpvalue; Dumpvalue->new (compactDump => 1, veryCompact => 1, quoteHighBit => 1, tick => '"')->dumpValue ([$type, @args]);
465
466 syswrite $self->{fh}, $self->{proto}->encode_msg ($type, @args);
467 }
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 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 }
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 $self->{proto}->register ($type, $cb);
506 }
507
508 =back
509
510 =head1 AUTHOR
511
512 Marc Lehmann <pcg@goof.com>
513 http://home.schmorp.de/
514
515 =cut
516
517 1;
518