ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-Knuddels/Net/Knuddels.pm
Revision: 1.32
Committed: Sun Jan 30 05:36:59 2005 UTC (21 years, 7 months ago) by root
Branch: MAIN
Changes since 1.31: +48 -6 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.8 =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 root 1.29 #TODO (Singles 15-17) headbanger16 >> LINK:Net-Knuddels (privat): hä was ins arschloch??
16    
17 root 1.31 #TODO: PING:
18 root 1.30 #0 4
19     #1 "elmecks|1106987572172"
20    
21 root 1.31 # TODO: anti-idle, immer oder optional
22    
23     # TODO: send_james for james-messages
24    
25 root 1.1 package Net::Knuddels;
26    
27 root 1.3 use Net::Knuddels::Dictionary;
28    
29 root 1.2 use strict;
30     use utf8;
31    
32 root 1.4 use Carp;
33     use Math::BigInt;
34 root 1.27 use Time::HiRes;
35 root 1.4
36 root 1.14 sub _to32($) {
37     unpack "l", pack "L", (new Math::BigInt $_[0]) & 0xffffffff
38     }
39    
40 root 1.4 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 root 1.25 $i = _to32 $i * 3 + ord substr $xor, $l - $_;
59     $j = _to32 $j * 5 + ord substr $xor, $_;
60 root 1.4 }
61     } else {
62     for ($_ = $l; $_ >= 0; $_ -= int $_/19) {
63 root 1.25 $i = _to32 $i * 5 + ord substr $xor, $_;
64     $j = _to32 $j * 3 + ord substr $xor, $l - $_;
65 root 1.4 }
66     }
67    
68     $i ^= $j;
69 root 1.14 _to32 (($i & 0xffffff) ^ ($i >> 24))
70 root 1.4 }
71    
72 root 1.10 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 root 1.14 $frag = chr unpack "v", pack "b*", $bin =~ /\G(.{16})/cg && $1 if $frag eq "\\\\\\";
81 root 1.10 $res .= $frag;
82     }
83     $bin =~ /\G(.*[^0].*)$/ and die "Net::Knuddels::Receiver: undecodable message tail '$1'";
84    
85     $res
86     }
87    
88 root 1.9 my %encode = reverse %$Net::Knuddels::Dictionary;
89    
90 root 1.10 my $RE_enc = join "|", map quotemeta, sort { (length $b) <=> (length $a) } keys %encode;
91 root 1.9
92 root 1.10 sub encode($) {
93 root 1.9 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 root 1.5 =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 root 1.6 =over 4
115    
116 root 1.8 =cut
117    
118     package Net::Knuddels::Protocol;
119    
120 root 1.6 =item new
121    
122     Create a new C<Net::Knuddels::Protocol> object.
123    
124 root 1.5 =cut
125 root 1.2
126     sub new {
127     my $class = shift;
128    
129 root 1.4 my %data;
130    
131     my $self = bless {
132     @_
133     }, $class;
134    
135     $self;
136 root 1.2 }
137    
138 root 1.6 =item $protocol->feed_data ($octets)
139    
140     Feed raw protocol data into the decoder.
141    
142     =cut
143    
144 root 1.2 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 root 1.1
183 root 1.10 $self->feed_event (split /\0/, Net::Knuddels::decode $msg);
184 root 1.1 }
185    
186 root 1.2 sub feed_event($@) {
187 root 1.14 my ($self, @cmd) = @_;
188 root 1.1
189 root 1.14 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 root 1.1 }
196 root 1.14
197     my $ev = $self->{cb}{shift @cmd};
198     $_->(@cmd) for values %$ev;
199 root 1.2 }
200 root 1.1
201 root 1.11 =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 root 1.12 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 root 1.11 }
222    
223 root 1.6 =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 root 1.16 the name of a generated event, such as C<login>.
228 root 1.6
229     =cut
230    
231 root 1.2 sub register {
232     my ($self, $type, $cb) = @_;
233 root 1.1
234 root 1.2 $self->{cb}{$type}{$cb} = $cb;
235 root 1.1 }
236    
237 root 1.8 =item $protocol->destroy
238    
239 root 1.9 I<MUST> be called to destroy the object, otherwise it will leak (no automatic cleanup).
240 root 1.8
241     =cut
242    
243 root 1.5 sub destroy {
244     my ($self) = @_;
245    
246     delete $self->{cb};
247     }
248    
249 root 1.6 =back
250    
251 root 1.8 =head2 CLASS Net::Knuddels::Client
252    
253 root 1.9 Implement a Knuddels client connection.
254    
255 root 1.8 =over 4
256    
257     =cut
258    
259     package Net::Knuddels::Client;
260    
261 root 1.16 sub handle_room {
262     my ($self, $room) = @_;
263    
264     if ($room eq "-") {
265 elmex 1.23 if (scalar (keys %{$self->{room}}) == 1) {
266     return (keys %{$self->{room}})[0];
267 root 1.16 } else {
268     warn "Couldn't assign '-' room to a room!";
269 elmex 1.23 return '#nosuchroom';
270 root 1.16 }
271     } else {
272     return $room;
273     }
274     }
275    
276 elmex 1.20 sub update_user_stats {
277     my ($user) = @_;
278 root 1.16
279     if ($user->{name} =~ s/\cJ(\d+)$//) {
280     $user->{age} = $1
281     }
282    
283     if ($user->{picture} =~ m/\bmale/) {
284     $user->{gender} = 'm';
285 elmex 1.20
286 root 1.16 } elsif ($user->{picture} =~ m/female/) {
287     $user->{gender} = 'f';
288     }
289 root 1.19
290 root 1.16 return $user;
291     }
292    
293 elmex 1.22 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 root 1.9 =item new Net::Knuddels::Client [IO::Socket::new arguments]
322    
323     Create a new client connection.
324    
325 root 1.32 Optional extra arguments:
326    
327     command_wait => $cb($client,$wait)
328     This callback will be called with the client object
329     and a time to wait. It must call the C<command_cb> method
330     after the time specified, either by a blocking wait
331     or via some event loop callback. The default implementation
332     just does a blockign wait.
333    
334 root 1.9 =cut
335    
336     use IO::Socket::INET;
337    
338     sub new {
339     my ($class, @arg) = @_;
340    
341     my $fh = new IO::Socket::INET @arg
342     or Carp::croak "Net::Knuddels::Client::new: $!";
343    
344     my $self = bless {
345 root 1.27 queue => [],
346 root 1.9 fh => $fh,
347     proto => (new Net::Knuddels::Protocol),
348 root 1.28 rate => 1, # commands/s
349 root 1.32 command_wait => sub {
350     select undef, undef, undef, $_[1];
351     $_[0]->command_cb;
352     },
353     queue => [],
354 root 1.27 @arg,
355 root 1.9 }, $class;
356    
357     syswrite $fh, "\0";
358    
359 root 1.16 $self->register ("(" => sub {
360     $self->{login_challenge} = $_[0];
361     $self->{login_room} = $_[1];
362 elmex 1.23 $self->{proto}->feed_event (login => $_[1]);
363 root 1.16 });
364 elmex 1.23
365 elmex 1.22 $self->register (k => sub {
366     my @str = map { s/[\356\343]//; $_ } @_;
367     my @out;
368     push @out, split /#/, $_ for @str;
369     $self->{proto}->feed_event (dialog => \@out);
370     });
371 elmex 1.23
372 elmex 1.21 $self->register (t => sub {
373     my $src = $_[0];
374 elmex 1.23
375 elmex 1.21 if ($src eq '-') {
376 root 1.27 $_[2] = $self->{nick} . " " . $_[2];
377 elmex 1.23
378 elmex 1.21 } else {
379     $_[2] = $src . " " . $_[2];
380     }
381     $self->{proto}->feed_event (action_room => $self->handle_room ($_[1]), $_[2]);
382     });
383 elmex 1.23
384 root 1.27 my %last_msg; # the last message of a user, to avoid duplicates
385    
386 root 1.16 $self->register (r => sub {
387 elmex 1.21 my $src = $_[0];
388 root 1.27 $src = $self->{nick} if $src eq "-";
389    
390 elmex 1.21 $self->{proto}->feed_event (msg_priv => $self->handle_room ($_[2]), $src, $_[1], $_[3]);
391 root 1.27
392     if ($src eq "James") {
393     $self->{proto}->feed_event (msg_priv_james => $self->handle_room ($_[2]), $src, $_[1], $_[3]);
394     } elsif ($src eq $self->{nick}) {
395     $self->{proto}->feed_event (msg_priv_echo => $self->handle_room ($_[2]), $src, $_[1], $_[3]);
396     } else {
397     $self->{proto}->feed_event (msg_priv_nondup => $self->handle_room ($_[2]), $src, $_[1], $_[3])
398     if $last_msg{$src} ne $_[3];
399     $last_msg{$src} = $_[3];
400     }
401 root 1.16 });
402 root 1.27
403 root 1.16 $self->register (e => sub {
404     $self->{proto}->feed_event (msg_room => $self->handle_room ($_[1]), $_[0], $_[2]);
405     });
406 elmex 1.23
407     $self->register (l => sub {
408 root 1.16 my $room = $self->handle_room ($_[0]);
409     return if $room eq "-"; # things that shouln't happen
410    
411     my $user = {
412     name => $_[1],
413     flag => $_[2],
414     color => $_[3],
415     picture => $_[4]
416     };
417    
418 elmex 1.20 update_user_stats ($user);
419 root 1.16
420     my $rl = $self->{user_lists}->{lc $room}->{lc $user->{name}} = $user;
421    
422     $self->{proto}->feed_event (join_room => $room, $user);
423     });
424 root 1.27
425 root 1.16 $self->register (w => sub {
426     my $room = $self->handle_room ($_[1]);
427     return if $room eq "-"; # things that shouln't happen
428    
429     my $username = $_[0];
430    
431     my $u = delete $self->{user_lists}->{lc $room}->{lc $username};
432    
433     if (not defined $u) {
434     warn "User $username wasn't in room $room, trying to fix... but be careful!!!\n";
435     $u = { name => $username };
436     }
437    
438     $self->{proto}->feed_event (part_room => $room, $u);
439     });
440 elmex 1.23
441     $self->register (b => sub {
442     my @arg = @_;
443     my $cc = {};
444     $self->{knuddels_rooms}->{$cc->{name}} = {};
445    
446     my $last = $cc;
447     my $chan_cnt = 2;
448    
449     while (@arg) {
450     $cc->{name} = shift @arg;
451    
452     if ($cc->{name} =~ s/\cJ(\d+)$//) {
453     $cc->{user_count} = $1;
454     }
455 root 1.27
456 elmex 1.23 if ($cc->{name} =~ m/^"/) {
457     $cc->{name} = "$last->{name} $chan_cnt";
458     $chan_cnt++;
459    
460     } else {
461     $last = $cc;
462     $chan_cnt = 2;
463     }
464    
465     $cc->{flag1} = shift @arg;
466     $cc->{flag2} = shift @arg;
467    
468     my $i = 0;
469    
470     for (my $a = shift @arg; $a ne "-"; $a = shift @arg) {
471    
472     if ($i == 0) {
473     $cc->{picture} = $a;
474     $cc->{full_flag} = 1 if $cc->{picture} =~ m/full/i;
475     }
476     $i++;
477     }
478    
479     $self->{knuddels_rooms}->{$cc->{name}} = $cc;
480     $cc = {};
481 root 1.16 }
482    
483 elmex 1.23 $self->{proto}->feed_event (room_list => $self->{knuddels_rooms});
484     });
485    
486     $self->register (d => sub {
487     my $room = $self->handle_room ($_[0]);
488    
489     delete $self->{room}->{lc $room};
490     $self->{room}->{lc $_[1]} = { name => $_[1] };
491    
492     $self->{proto}->feed_event (change_room => $room, $_[1]);
493     });
494    
495     $self->register ('6' => sub {
496     # i have no exact clue what this message does,
497     # but java-code seems to say i should do this:
498    
499     warn "*********************** SIX MESSAGE GOT!!! CHECK PROTOCOL FOR OCCURENCE!!";
500     # delete $self->{room}->{lc ($self->handle_room ($_[0]))};
501     });
502    
503     $self->register (a => sub {
504 root 1.16 $self->{my_nick} = $_[1]; # i'm really _not_ shure about this
505    
506     my $ri = $self->{room}->{lc $_[0]} = {
507 elmex 1.23 name => $_[0],
508 root 1.16 picture => $_[7],
509     };
510    
511     $self->{proto}->feed_event (room_info => $_[0], $ri);
512     });
513 elmex 1.23
514 root 1.16 $self->register (u => sub {
515     my $room = shift;
516     my $rl = $self->{user_lists}->{lc $room} = {};
517     my $cur_u = {};
518    
519     while (@_) {
520     $cur_u->{name} = shift;
521     $cur_u->{flag} = shift;
522     $cur_u->{color} = shift;
523    
524     my $i = 0;
525    
526     while ((my $nxt = shift) ne "-") {
527     if ($i == 0) {
528     $cur_u->{picture} = $nxt;
529     }
530     $i++;
531     }
532    
533 elmex 1.20 update_user_stats ($cur_u);
534 root 1.16 $rl->{lc $cur_u->{name}} = $cur_u;
535     $cur_u = {};
536     }
537     $self->{proto}->feed_event (user_list => $room, $rl);
538     });
539    
540 root 1.9 $self
541     }
542    
543     =item $client->fh
544    
545     Return the fh used for communications. You are responsible for calling C<<
546 root 1.13 $client->ready >> whenever the fh becomes ready for reading.
547 root 1.9
548     =cut
549    
550     sub fh {
551     $_[0]->{fh}
552     }
553    
554 root 1.13 =item $client->ready
555    
556     To be called then the filehandle is ready for reading. Returns false if
557     the server closed the connection, true otherwise.
558    
559     =cut
560    
561     sub ready {
562     my ($self) = @_;
563    
564     sysread $self->{fh}, my $buf, 8192
565     or return;
566    
567     $self->{proto}->feed_data ($buf);
568    
569     1;
570     }
571    
572 root 1.32 =item $client->command_cb
573    
574     Should be called by the C<command_wait> callback when the timer has
575     expired, to send a delayed command to the server.
576    
577     =cut
578    
579     sub command_cb {
580     my ($self) = @_;
581    
582     while () {
583     last unless @{ $self->{queue} };
584    
585     my $NOW = Time::HiRes::time;
586     my $wait = $self->{next_command} - $NOW;
587    
588     if ($wait > 1e-4) {
589     $self->{command_wait}->($self, $wait);
590     last;
591     } else {
592     my ($type, @args) = @{ shift @{ $self->{queue} } };
593     $self->{next_command} = $NOW + $self->{rate} + 1e-4;
594    
595     #use Dumpvalue; Dumpvalue->new (compactDump => 1, veryCompact => 1, quoteHighBit => 1, tick => '"')->dumpValue ([$type, @args]);
596     syswrite $self->{fh}, $self->{proto}->encode_msg ($type, @args);
597     }
598     }
599     }
600    
601    
602    
603 root 1.9 =item $client->command ($type => @args)
604    
605 root 1.27 Send a message of type C<$type> and the given arguments to the server,
606     ensures a proper rate-limit.
607 root 1.9
608     =cut
609    
610     sub command {
611     my ($self, $type, @args) = @_;
612    
613 root 1.32 if (1 == push @{ $self->{queue} }, [$type, @args]) {
614     $self->command_cb;
615     }
616 root 1.9 }
617    
618     =item $client->login ($url, $unknown)
619    
620     Send a 't' message. The default for C<$url> is
621     C<http://www.knuddels.de/applet.html?v=86a&c=0> and C<$unknown> is C<6>.
622    
623     =cut
624    
625     sub login {
626 root 1.13 my ($self, $url, $unknown) = @_;
627    
628     $self->command ("t", "V8.6a", $url || "http://www.knuddels.de/applet.html?v=86a&c=0", $unknown || 3);
629     }
630    
631 elmex 1.23 =item $client->enter_room ($room, $nick, $password)
632    
633 root 1.27 Enters a room C<$room> with C<$nick> and C<$password>. (for joining
634     multiple rooms call this multiple times)
635 root 1.13
636 root 1.27 NOTE: i won't allow joins to multiple rooms with different
637     nick/password's, the java client reacted very confused.. don't know
638     whether the server supports this.
639 root 1.13
640     =cut
641    
642 elmex 1.23 sub enter_room {
643 root 1.13 my ($self, $room, $nick, $password) = @_;
644    
645 root 1.27 if (defined $self->{nick} and $self->{nick} ne $nick) {
646 elmex 1.23 return # i don't think knuddels-server will be happy if
647     # we join multiple rooms on multiple accounts over 1 connection
648     }
649    
650 root 1.16 exists $self->{login_challenge} or Carp::croak "set_nick can only be called after a login event";
651 root 1.13
652 root 1.27 $self->{nick} = $nick;
653 root 1.16 $self->command ("n", $room, $nick, Net::Knuddels::hash_pw $self->{login_challenge}, $password);
654 root 1.9 }
655    
656 elmex 1.22 =item $client->send_whois ($nick)
657    
658 root 1.27 Sends a whois-request for $nick.
659 elmex 1.22
660     =cut
661 root 1.27
662 elmex 1.22 sub send_whois {
663     my ($self, $room, $nick) = @_;
664    
665     $self->command ("e", $room, "/w $nick");
666     }
667    
668    
669 elmex 1.21 =item $client->send_room_msg ($nick, $room, $message)
670    
671 root 1.27 Sends a private C<$message> to C<$nick> over C<$room>.
672 elmex 1.21
673     =cut
674 root 1.27
675 elmex 1.21 sub send_room_msg {
676     my ($self, $room, $message) = @_;
677    
678     $self->command ("e", $room, $message);
679     }
680    
681    
682     =item $client->send_priv_msg ($nick, $room, $message)
683    
684 root 1.27 Sends a private C<$message> to C<$nick> over C<$room>.
685 elmex 1.21
686     =cut
687 root 1.27
688 elmex 1.21 sub send_priv_msg {
689     my ($self, $nick, $room, $message) = @_;
690    
691     $self->command ("e", $room, "/p $nick:$message");
692     }
693    
694 elmex 1.23 =item $client->send_join_room ($oldroom, $room)
695    
696 root 1.27 Sends the server a join command for C<$room>. This will result in a room
697     change from C<$oldroom> to C<$room>.
698 elmex 1.23
699     =cut
700 root 1.27
701 elmex 1.23 sub send_join_room {
702     my ($self, $old_room, $room) = @_;
703    
704     $self->command ("e", $old_room, "/go $room");
705     }
706    
707     =item $client->send_exit_room ($room)
708    
709 root 1.27 Exits C<$room> completly. (can be seen as counter method for C<enter_room ()>)
710 elmex 1.23
711     =cut
712 root 1.27
713 elmex 1.23 sub send_exit_room {
714     my ($self, $room) = @_;
715     $self->command ("w", $room, "\0", "\0");
716     delete $self->{room}->{lc $room};
717     }
718    
719 root 1.9 =item $client->register ($type => $cb)
720    
721 root 1.16 See L<Net::Knuddels::Protocol::register>. The following extra events will
722     be generated by this class:
723 root 1.9
724 root 1.16 login
725     set_nick can only be called _after_ a login event has occured.
726    
727     msg_room => $room, $user, $msg
728     produced when a public message is uttered :)
729    
730 root 1.27 msg_priv => $room, $src, $dst, $msg
731     personal message from $src to $dst. better use msg_priv_nondup,
732     msg_priv_james or msg_priv_echo
733    
734     msg_priv_echo => $room, $src, $dst, $msg
735     like msg_priv, but only for echoed messages
736    
737     msg_priv_james => $room, $src, $dst, $msg
738     like msg_priv, but only for messages from James
739    
740     msg_priv_nondup => $room, $src, $dst, $msg
741     like msg_priv, but avoids duplicate messages, echos and james.
742 root 1.16
743     user_list => $room, $list
744     the userlist of a channel named $room, a elmement of the list (a user)
745     looks like:
746     {
747     name => <name>,
748     flag => <some flag i don't know what it means>,
749     color => like /\d+.\d+.\d+/,
750     age => /\d+/,
751     gender => /(f|m)/,
752     picture => <the picture file to put behind the nick>
753     }
754    
755     room_info => $room, $room_info
756     some information about the $room:
757     $room_info =
758     {
759     picture => <some picturefile>
760     }
761    
762     join_room => $room, $user
763     join message of $user joined the room $room
764     $user contains the user structure (see user_list).
765    
766     part_room => $room, $user
767     part message of $user who left the room $room
768     $user contains the user structure (see user_list).
769 root 1.9 =cut
770    
771     sub register {
772     my ($self, $type, $cb) = @_;
773    
774 root 1.13 $self->{proto}->register ($type, $cb);
775 root 1.9 }
776    
777 root 1.8 =back
778    
779     =head1 AUTHOR
780    
781     Marc Lehmann <pcg@goof.com>
782     http://home.schmorp.de/
783    
784 root 1.6 =cut
785    
786 root 1.2 1;
787