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