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 (19 years, 4 months ago) by root
Branch: MAIN
Changes since 1.28: +2 -0 lines
Log Message:
*** empty log message ***

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 #TODO (Singles 15-17) headbanger16 >> LINK:Net-Knuddels (privat): hä was ins arschloch??
16
17 package Net::Knuddels;
18
19 use Net::Knuddels::Dictionary;
20
21 use strict;
22 use utf8;
23
24 use Carp;
25 use Math::BigInt;
26 use Time::HiRes;
27
28 sub _to32($) {
29 unpack "l", pack "L", (new Math::BigInt $_[0]) & 0xffffffff
30 }
31
32 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 $i = _to32 $i * 3 + ord substr $xor, $l - $_;
51 $j = _to32 $j * 5 + ord substr $xor, $_;
52 }
53 } else {
54 for ($_ = $l; $_ >= 0; $_ -= int $_/19) {
55 $i = _to32 $i * 5 + ord substr $xor, $_;
56 $j = _to32 $j * 3 + ord substr $xor, $l - $_;
57 }
58 }
59
60 $i ^= $j;
61 _to32 (($i & 0xffffff) ^ ($i >> 24))
62 }
63
64 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 $frag = chr unpack "v", pack "b*", $bin =~ /\G(.{16})/cg && $1 if $frag eq "\\\\\\";
73 $res .= $frag;
74 }
75 $bin =~ /\G(.*[^0].*)$/ and die "Net::Knuddels::Receiver: undecodable message tail '$1'";
76
77 $res
78 }
79
80 my %encode = reverse %$Net::Knuddels::Dictionary;
81
82 my $RE_enc = join "|", map quotemeta, sort { (length $b) <=> (length $a) } keys %encode;
83
84 sub encode($) {
85 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 =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 =over 4
107
108 =cut
109
110 package Net::Knuddels::Protocol;
111
112 =item new
113
114 Create a new C<Net::Knuddels::Protocol> object.
115
116 =cut
117
118 sub new {
119 my $class = shift;
120
121 my %data;
122
123 my $self = bless {
124 @_
125 }, $class;
126
127 $self;
128 }
129
130 =item $protocol->feed_data ($octets)
131
132 Feed raw protocol data into the decoder.
133
134 =cut
135
136 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
175 $self->feed_event (split /\0/, Net::Knuddels::decode $msg);
176 }
177
178 sub feed_event($@) {
179 my ($self, @cmd) = @_;
180
181 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 }
188
189 my $ev = $self->{cb}{shift @cmd};
190 $_->(@cmd) for values %$ev;
191 }
192
193 =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 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 }
214
215 =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 the name of a generated event, such as C<login>.
220
221 =cut
222
223 sub register {
224 my ($self, $type, $cb) = @_;
225
226 $self->{cb}{$type}{$cb} = $cb;
227 }
228
229 =item $protocol->destroy
230
231 I<MUST> be called to destroy the object, otherwise it will leak (no automatic cleanup).
232
233 =cut
234
235 sub destroy {
236 my ($self) = @_;
237
238 delete $self->{cb};
239 }
240
241 =back
242
243 =head2 CLASS Net::Knuddels::Client
244
245 Implement a Knuddels client connection.
246
247 =over 4
248
249 =cut
250
251 package Net::Knuddels::Client;
252
253 sub handle_room {
254 my ($self, $room) = @_;
255
256 if ($room eq "-") {
257 if (scalar (keys %{$self->{room}}) == 1) {
258 return (keys %{$self->{room}})[0];
259 } else {
260 warn "Couldn't assign '-' room to a room!";
261 return '#nosuchroom';
262 }
263 } else {
264 return $room;
265 }
266 }
267
268 sub update_user_stats {
269 my ($user) = @_;
270
271 if ($user->{name} =~ s/\cJ(\d+)$//) {
272 $user->{age} = $1
273 }
274
275 if ($user->{picture} =~ m/\bmale/) {
276 $user->{gender} = 'm';
277
278 } elsif ($user->{picture} =~ m/female/) {
279 $user->{gender} = 'f';
280 }
281
282 return $user;
283 }
284
285 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 =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 queue => [],
329 fh => $fh,
330 proto => (new Net::Knuddels::Protocol),
331 rate => 1, # commands/s
332 @arg,
333 }, $class;
334
335 syswrite $fh, "\0";
336
337 $self->register ("(" => sub {
338 $self->{login_challenge} = $_[0];
339 $self->{login_room} = $_[1];
340 $self->{proto}->feed_event (login => $_[1]);
341 });
342
343 $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
350 $self->register (t => sub {
351 my $src = $_[0];
352
353 if ($src eq '-') {
354 $_[2] = $self->{nick} . " " . $_[2];
355
356 } else {
357 $_[2] = $src . " " . $_[2];
358 }
359 $self->{proto}->feed_event (action_room => $self->handle_room ($_[1]), $_[2]);
360 });
361
362 my %last_msg; # the last message of a user, to avoid duplicates
363
364 $self->register (r => sub {
365 my $src = $_[0];
366 $src = $self->{nick} if $src eq "-";
367
368 $self->{proto}->feed_event (msg_priv => $self->handle_room ($_[2]), $src, $_[1], $_[3]);
369
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 });
380
381 $self->register (e => sub {
382 $self->{proto}->feed_event (msg_room => $self->handle_room ($_[1]), $_[0], $_[2]);
383 });
384
385 $self->register (l => sub {
386 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 update_user_stats ($user);
397
398 my $rl = $self->{user_lists}->{lc $room}->{lc $user->{name}} = $user;
399
400 $self->{proto}->feed_event (join_room => $room, $user);
401 });
402
403 $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
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
434 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 }
460
461 $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 $self->{my_nick} = $_[1]; # i'm really _not_ shure about this
483
484 my $ri = $self->{room}->{lc $_[0]} = {
485 name => $_[0],
486 picture => $_[7],
487 };
488
489 $self->{proto}->feed_event (room_info => $_[0], $ri);
490 });
491
492 $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 update_user_stats ($cur_u);
512 $rl->{lc $cur_u->{name}} = $cur_u;
513 $cur_u = {};
514 }
515 $self->{proto}->feed_event (user_list => $room, $rl);
516 });
517
518 $self
519 }
520
521 =item $client->fh
522
523 Return the fh used for communications. You are responsible for calling C<<
524 $client->ready >> whenever the fh becomes ready for reading.
525
526 =cut
527
528 sub fh {
529 $_[0]->{fh}
530 }
531
532 =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 =item $client->command ($type => @args)
551
552 Send a message of type C<$type> and the given arguments to the server,
553 ensures a proper rate-limit.
554
555 =cut
556
557 sub command {
558 my ($self, $type, @args) = @_;
559
560 my $wait = $self->{next_command} - Time::HiRes::time;
561 select undef, undef, undef, $wait if $wait > 0;
562 $self->{next_command} = Time::HiRes::time + $self->{rate};
563
564 #use Dumpvalue; Dumpvalue->new (compactDump => 1, veryCompact => 1, quoteHighBit => 1, tick => '"')->dumpValue ([$type, @args]);
565 syswrite $self->{fh}, $self->{proto}->encode_msg ($type, @args);
566 }
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 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 =item $client->enter_room ($room, $nick, $password)
582
583 Enters a room C<$room> with C<$nick> and C<$password>. (for joining
584 multiple rooms call this multiple times)
585
586 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
590 =cut
591
592 sub enter_room {
593 my ($self, $room, $nick, $password) = @_;
594
595 if (defined $self->{nick} and $self->{nick} ne $nick) {
596 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 exists $self->{login_challenge} or Carp::croak "set_nick can only be called after a login event";
601
602 $self->{nick} = $nick;
603 $self->command ("n", $room, $nick, Net::Knuddels::hash_pw $self->{login_challenge}, $password);
604 }
605
606 =item $client->send_whois ($nick)
607
608 Sends a whois-request for $nick.
609
610 =cut
611
612 sub send_whois {
613 my ($self, $room, $nick) = @_;
614
615 $self->command ("e", $room, "/w $nick");
616 }
617
618
619 =item $client->send_room_msg ($nick, $room, $message)
620
621 Sends a private C<$message> to C<$nick> over C<$room>.
622
623 =cut
624
625 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 Sends a private C<$message> to C<$nick> over C<$room>.
635
636 =cut
637
638 sub send_priv_msg {
639 my ($self, $nick, $room, $message) = @_;
640
641 $self->command ("e", $room, "/p $nick:$message");
642 }
643
644 =item $client->send_join_room ($oldroom, $room)
645
646 Sends the server a join command for C<$room>. This will result in a room
647 change from C<$oldroom> to C<$room>.
648
649 =cut
650
651 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 Exits C<$room> completly. (can be seen as counter method for C<enter_room ()>)
660
661 =cut
662
663 sub send_exit_room {
664 my ($self, $room) = @_;
665 $self->command ("w", $room, "\0", "\0");
666 delete $self->{room}->{lc $room};
667 }
668
669 =item $client->register ($type => $cb)
670
671 See L<Net::Knuddels::Protocol::register>. The following extra events will
672 be generated by this class:
673
674 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 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
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 =cut
720
721 sub register {
722 my ($self, $type, $cb) = @_;
723
724 $self->{proto}->register ($type, $cb);
725 }
726
727 =back
728
729 =head1 AUTHOR
730
731 Marc Lehmann <pcg@goof.com>
732 http://home.schmorp.de/
733
734 =cut
735
736 1;
737