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