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