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