Author: dylan
Date: 2005-07-18 00:34:36 -0400 (Mon, 18 Jul 2005)
New Revision: 869
Modified:
trunk/
trunk/perl/client/lib/Haver/Client/POE.pm
Log:
[EMAIL PROTECTED]: dylan | 2005-07-18 00:34:14 -0400
Um, svk is being weird.
Property changes on: trunk
___________________________________________________________________
Name: svk:merge
- 1f59643a-e6e5-0310-bc24-f7d4c744f460:/haver/havercurs-objc:43089
1f59643a-e6e5-0310-bc24-f7d4c744f460:/haver/local/trunk:11166
1f59643a-e6e5-0310-bc24-f7d4c744f460:/haver/local/trunk-merge-10131:11178
27e50396-46e3-0310-8b22-ae223a1f35ce:/local:212
e9404bb1-7af0-0310-a7ff-e22194cd388b:/haver/local:1268
edfcd8bd-4ce7-0310-a97e-bb1efd40edf3:/local:238
+ 1f59643a-e6e5-0310-bc24-f7d4c744f460:/haver/havercurs-objc:43089
1f59643a-e6e5-0310-bc24-f7d4c744f460:/haver/local/trunk:11166
1f59643a-e6e5-0310-bc24-f7d4c744f460:/haver/local/trunk-merge-10131:11178
27e50396-46e3-0310-8b22-ae223a1f35ce:/local:212
e9404bb1-7af0-0310-a7ff-e22194cd388b:/haver/local:1272
edfcd8bd-4ce7-0310-a97e-bb1efd40edf3:/local:238
Modified: trunk/perl/client/lib/Haver/Client/POE.pm
===================================================================
--- trunk/perl/client/lib/Haver/Client/POE.pm 2005-07-18 04:32:51 UTC (rev
868)
+++ trunk/perl/client/lib/Haver/Client/POE.pm 2005-07-18 04:34:36 UTC (rev
869)
@@ -17,16 +17,37 @@
# along with this module; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-# heap fields:
-# heap => {
-# name => user name,
-# reg => {
-# event name => { session ID => 1 }
-# },
-# wheel => active wheel,
-# state => see below constants,
-# # for connect events sent while still shutting down
-# pending => { Host => host, Port => port, Name => name ] | nonexistent
+# object fields:
+# self => {
+# [persistent data]
+# options => { key => value },
+# handlers => {
+# type => {
+# event name => {
+# reverse => {
+# stringified $subref => index
+# },
+# forward => [
+# $subref,
+# ...
+# ]
+# } # event name =>
+# } # type =>
+# }, # handlers =>
+# connection => {
+# [connection data]
+# UID => $uid,
+# authmethods => [ acceptable auth methods ],
+# state => STATE_NAME,
+# want => 'CURRENTLY_WANTED',
+# pending => [
+# ['WAITING', 'FOR', 'WANT'],
+# ],
+# POE => {
+# connwheel => POE::Wheel::ConnectionFactory,
+# sockwheel => POE::Wheel::ReadWrite
+# },
+# } # connection =>
# }
=head1 NAME
@@ -41,7 +62,7 @@
$conn->register(connected => \&on_connect);
$conn->connect( Host => 'example.com',
Port => 7070,
- Name => 'example');
+ UID => 'example');
=head1 DESCRIPTION
@@ -56,72 +77,53 @@
package Haver::Client::POE;
use strict;
use warnings;
-use Carp;
-use Data::Dumper;
-use Regexp::Shellish;
use constant {
- S_IDLE => 0, # not connected, not connecting
- S_CONN => 1, # establishing socket connection
- S_INIT => 2, # C: HAVER sent
- S_LOGIN => 3, # S: HAVER recieved, C: IDENT sent
- S_ONLINE => 4, # S: HELLO received
- S_DYING => 5, # C: BYE sent, socket still open
+ TRUE => 1,
+ FALSE => 0,
+ STATE_IDLE => 0, # not connected or connecting
+ STATE_CONNECT => 1, # establishing connection
+ STATE_AUTH => 2, # authenticating
+ STATE_READY => 3, # ready for user control
+ STATE_DISCON => 4, # disconnecting
};
+use Carp;
+
use POE qw(Wheel::ReadWrite
Wheel::SocketFactory);
-use Haver::Protocol::Filter;
+use POE::Filter::Haver;
+use Haver::Preprocessor;
+use Haver::Formats::Error;
our $VERSION = 0.06;
-our %failures = (
- 'invalid.name' => "The server rejected the name %1%.",
- 'reserved.name' => "The name %1% is reserved for internal use by the
server.",
- 'exists.user' => "The name %1% is in use.",
- 'unknown.user' => "The user %1% is not online.",
- 'unknown.channel' => "The channel %1% does not exist.",
- 'unknown.namespace' => "The namespace %1% does not exist. This is probably
an application bug.",
- 'invalid.type' => "The type of a message was invalid. This is almost
certainly an application error.",
- 'already.joined' => "Tried to join %1% when already in it.",
- 'already.parted' => "Tried to leave %1% when not in it.",
-);
-
-sub _format {
- my @args = @_;
- shift @args; # S: FAIL
- my $code = $args[0];
-
- if (!$failures{$code}) {
- return "Unknown error: " . join(' ', @args);
- }
-
- my $msg = $failures{$code};
- $msg = s{%(\d+)%}{$args[$1] || "MISSING ARGUMENT $1"}eg;
- return $msg;
-}
-
sub _call {
return POE::Kernel->call(POE::Kernel->get_active_session(), @_);
}
-sub _dispatch {
- _call('__dispatch', @_);
-}
-
sub _dprint {
my ($level, @text) = @_;
- return unless POE::Kernel->get_active_session()->get_heap()->{debug} >=
$level;
+ return unless
POE::Kernel->get_active_session()->get_heap()->{object}{debug} >= $level;
print STDERR @text;
}
sub _dprintf {
my ($level, $fmt, @text) = @_;
- return unless POE::Kernel->get_active_session()->get_heap()->{debug} >=
$level;
+ return unless
POE::Kernel->get_active_session()->get_heap()->{object}{debug} >= $level;
printf STDERR $fmt, @text;
}
+sub _object_states {
+ my ($self, $ehash) = @_;
+ $ehash = {qw{
+ _start _start
+ _default _default
+ }, %$ehash};
+ return $self->SUPER::_object_states($ehash);
+}
+
### SETUP
=head2 spawn($alias [, $Z<>resolver])
@@ -136,10 +138,12 @@
sub spawn {
my ($pkg, $alias, $resolver) = @_;
- my $heap = {
- reg => {},
- state => S_IDLE,
- alias => $alias,
+ my $self = {
+ options => {
+ version => "Haver::Client::POE/$VERSION",
+ },
+ handlers => {
+ },
resolver => $resolver
};
@@ -187,16 +191,22 @@
}
sub _start {
- my ($kernel, $alias) = @_[KERNEL,ARG0];
- $kernel->alias_set($alias);
+ die "STUB";
}
-### SESSION MANAGEMENT
+# Internal: _init_event_type($type)
+# Prepares to register events of type $type
-=head2 connect(Host => $Z<>host, Name => $Z<>name, [Port => $Z<>port])
+sub _init_event_type {
+ my $self = shift;
+ $self->{handlers}{shift} = {
+ hhash => {},
+ byname => {},
+ }
+}
-Connects to the haver server. The Host option is mandatory, all others are
optional.
-If it is already connected, it will disconnect, then connect with the new
parameters.
+# Internal: _init_wants
+# Sets up default handlers for wants
=cut
@@ -301,19 +311,41 @@
# XXX: timeout
}
-sub _input {
- my ($kernel, $heap, $arg) = @_[KERNEL,HEAP,ARG0];
- return if (ref $arg ne 'ARRAY' || @$arg == 0);
- print STDERR "S: ", join "\t", @$arg;
- print STDERR "\n";
- _dispatch('raw_in', @$arg);
- my $cmd = $arg->[0];
- $kernel->yield("_ev_$cmd", @$arg);
+
+
+=head2 setoptions(option => value [, ...])
+
+Sets one or more options to the given value. The following options are
available:
+
+=head3 debug => level
+
+Sets debugging to the given level. 0 will disable debugging.
+
+=head3 autorespond => [ ... ]
+
+Enables autoresponding to certain types of messages. Currently only PING? and
TIME? are supported.
+
+=head3 version => "foobar/0.1"
+
+Sets the string sent in response to S: WANT VERSION
+
+=cut
+
+sub setoptions {
+ die "STUB";
}
-sub _err {
- _dispatch('disconnected', @_[ARG0..ARG2]);
- _call('_cleanup');
+### SESSION MANAGEMENT
+
+=head2 connect(Host => $Z<>host, [Port => $Z<>port])
+
+Connects to the haver server. The Host option is mandatory, all others are
optional.
+If it is already connected, it will disconnect, then connect with the new
parameters.
+
+=cut
+
+sub connect {
+ die "STUB";
}
=head2 disconnect(Z<>)
@@ -330,27 +362,9 @@
$poe_kernel->delay('_force_down', 5);
}
-sub _force_down {
- my $heap = $_[HEAP];
- $heap->{state} = S_IDLE;
- _call('_cleanup');
-}
-
-sub _cleanup {
- my $heap = $_[HEAP];
- $poe_kernel->delay('_force_down');
- if ($heap->{pending}) {
- my @opts = %{delete $heap->{pending}};
- $poe_kernel->yield('connect', @opts);
- }
- delete $heap->{wheel};
- delete $heap->{name};
- $heap->{state} = S_IDLE;
-}
-
=head2 send_raw(@args)
-Sends the arguments specified to the haver server. No checking is performed,
though escaping will be done.
+Sends the arguments specified to the haver server. No checking is performed,
though escaping may occur.
=cut
@@ -372,12 +386,34 @@
=cut
sub send {
- my ($kernel, @args) = @_[KERNEL,ARG0..$#_];
- _call('send_raw', @args);
+ die "STUB"
}
### CLIENT EVENTS
+=head2 login(UID => $Z<>uid)
+
+Specifies the UID to use to log in. If authentication is already complete,
+this has no effect.
+
+=cut
+
+sub login {
+ die "STUB";
+}
+
+=head2 authenticate($Z<>method, @Z<>args)
+
+Authenticates to the server. $method must be an object of type
+Haver::Client::POE::Auth capable of handling an authentication
+type listed in the auth_request() message.
+
+=cut
+
+sub authenticate {
+ die "STUB";
+}
+
=head2 join($Z<>channel)
Attempts to join $channel
@@ -385,8 +421,7 @@
=cut
sub join {
- my $channel = $_[ARG0];
- _call('send', 'JOIN', $channel);
+ die "STUB";
}
=head2 part($Z<>channel)
@@ -396,205 +431,367 @@
=cut
sub part {
- my $channel = $_[ARG0];
- _call('send', 'PART', $channel);
+ die "STUB";
}
-=head2 in($Z<>channel, $Z<>type, @Z<>args)
+=head2 make($Z<>channel)
-Sends a message with specified type and arguments to $channel.
+Ask the server to create a channel $channel.
=cut
-sub public {
- my ($kernel, $heap, $c, $t, @a) = @_[KERNEL,HEAP,ARG0..$#_];
- _call('send', 'IN', $c, $t, @a);
+sub make {
+ die "STUB";
}
-=head2 to($Z<>uid, $Z<>type, @Z<>args)
+=head2 msg($Z<>channel, $Z<>type, $Z<>text)
-Sends a private message with specified type and arguments to $uid.
+Sends a message with specified type and text to $channel.
=cut
-sub private {
- my ($kernel, $heap, $d, $t, @a) = @_[KERNEL,HEAP,ARG0..$#_];
- _call('send', 'TO', $d, $t, @a);
+sub msg {
+ die "STUB";
}
-=head2 list($Z<>channel [, $Z<>type])
+=head2 pmsg($Z<>uid, $Z<>type, $Z<>text)
-Ask the server which entities of $type are in $channel. If $type is not
-given, it defaults to 'user'.
+Sends a private message with specified type and text to $uid.
=cut
-sub list {
- my ($chan, $type) = @_[ARG0, ARG1];
- $type = defined $type || 'user';
- _call('send', 'LIST', $chan, $type);
+sub pmsg {
+ die "STUB";
}
-=head2 destroy(Z<>)
+=head2 users($Z<>channel)
-Disconnects from the Haver server, and destroys the session.
+Ask the server which users are on $channel.
=cut
-sub destroy {
- my ($kernel, $heap) = @_[KERNEL,HEAP];
- _dispatch('destroyed');
- delete $heap->{pending};
- my $reg = $heap->{reg};
- foreach my $ehash (values %$reg) {
- foreach my $id (keys %$ehash) {
- $poe_kernel->refcount_decrement($id, $ehash->{$id});
- }
- }
- $heap->{reg} = {};
- _call('disconnect');
- $kernel->alias_remove($heap->{alias});
+sub users {
+ die "STUB";
}
-## server-response stuff
+=head2 chans(Z<>)
-sub _ev_HAVER {
- my ($kernel, $heap) = @_[KERNEL,HEAP];
- return if ($heap->{state} != S_INIT); # should never happen, unless the
- # server is non-compliant
- $kernel->yield('send_raw', 'IDENT', $heap->{name});
- $heap->{state} = S_LOGIN;
-}
+Ask the server for a list of all channels
-sub _ev_HELLO {
- my $heap = $_[HEAP];
- $heap->{state} = S_ONLINE;
- _dispatch('ready');
-}
+=cut
-sub _ev_JOIN {
- my ($heap, $chan, $name) = @_[HEAP,ARG1,ARG2];
- if ($name eq $heap->{name}) {
- _dispatch('ijoined', $chan);
- } else {
- _dispatch('join', $chan, $name);
- }
+sub chans {
+ die "STUB";
}
-sub _ev_PART {
- my ($heap, $chan, $name) = @_[HEAP,ARG1,ARG2];
- if ($name eq $heap->{name}) {
- _dispatch('iparted', $chan);
- } else {
- _dispatch('part', $chan, $name);
- }
-}
+=head2 destroy(Z<>)
-sub _ev_LIST {
- my ($heap, $chan, $ns, @things) = @_[HEAP,ARG1..$#_];
- return unless defined $ns;
- _dispatch('list', $chan, $ns, @things);
-}
+Disconnects from the Haver server, and destroys the session. This event may
not complete
+immediately, so you should not attempt to create another session with the same
alias
+until it finishes.
-sub _ev_IN {
- _dispatch('public', @_[ARG1..$#_]);
-}
+=cut
-sub _ev_FROM {
- _dispatch('private', @_[ARG1..$#_]);
+sub destroy {
+ die "STUB";
}
-sub _ev_PING {
- _call('send_raw', 'PONG', @_[ARG1..$#_]);
+sub _default {
+ my ( $kernel, $state, $event, $args, $self ) = @_[ KERNEL, STATE, ARG0,
ARG1, OBJECT ];
+ $args ||= []; # Prevents uninitialized-value warnings.
+ # DEBUG: "default: $state = $event. Args:\n", Dumper $args;
+ return 0;
}
-sub _ev_BYE {
- my ($type, $detail) = @_[ARG2,ARG3];
- _dispatch('bye', $detail);
- _call('_cleanup');
+=head1 HANDLERS
+
+Handlers are subroutines called to handle some event from the server. Any
+number of handlers may be registered to an event, and they will be called
+in reverse order of registration until one indicates that it has handled the
+event.
+
+The handler prototype is as follows:
+
+ sub handler {
+ my ($handled, $type, $name, $args) = @_;
+ # ...
+ $handled = 1; # To handle the event
+ }
+
+$type is the type of event, $name is the event name, and $args is an array ref
+of arguments for the event.
+
+If $$handled is 0 when the handler returns, the next handler in the chain will
+be called. Any changes to the contents of $args will be passed on.
+
+If no handler in a chain handles the event, an event with the same type and
+a name of '_default' will be generated, with the old event name prepended
+to @args. If no handler in the _default chain handles the event, it will be
+discarded.
+
+=head2 METHODS FOR HANDLER MANIPULATION
+
+=head3 register_handler($Z<>type, $Z<>event, $Z<>handler)
+
+Register a handler $handler for an event of type $type, name $event. If such
+a handler already exists, does nothing.
+
+=cut
+
+sub register_handler {
+ my ($self, $type, $event, $handler) = @_;
+ my $htype = $self->{handlers}{$type}
+ or croak "Unknown event type $type";
+ my $hevent = $htype->{$event};
+ if (!$hevent) {
+ $hevent = $htype->{$event} = {
+ forward => {},
+ reverse => {},
+ };
+ }
+ return if (exists $hevent->{reverse}{$handler});
+ my $index = @{$hevent->{forward}};
+ push @{$hevent->{forward}}, $handler;
+ $hevent->{reverse}{$handler} = $index;
}
-sub _ev_FAIL {
- my $msg = _format(@_[ARG0..$#_]);
- _dispatch('fail', $msg, @_[ARG0..$#_]);
- my $code = $_[ARG1];
- $code =~ tr/./_/;
- _dispatch("fail_$code", $msg, @_[ARG0..$#_]);
+=head3 unregister_handler($Z<>type, $Z<>event, $Z<>handler)
+
+Unregisters a handler previously registered by passing the same arguments
+to register_handler. If such a handler does not exist, does nothing.
+
+=cut
+
+sub unregister_handler {
+ my ($self, $type, $event, $handler) = @_;
+ my $htype = $self->{handlers}{$type}
+ or croak "Unknown event type $type";
+ my $hevent = $self->{handlers}{$type}{$event} or return;
+ my $index = $hevent->{reverse}{$handler} or return;
+ # Remove the item
+ splice @{$hevent->{forward}}, $index, 1;
+ # and renumber the ones afterward
+ my $len = @{$hevent->{forward}};
+ for (; $index < $len; $index++) {
+ my $handler = $hevent->{forward}[$index];
+ $hevent->{reverse}{$handler} = $index;
+ }
+ # and delete the reverse mapping
+ delete $hevent->{reverse}{$handler};
}
-sub _default {
- my ( $kernel, $state, $event, $args, $self ) = @_[ KERNEL, STATE, ARG0,
ARG1, OBJECT ];
- $args ||= []; # Prevents uninitialized-value warnings.
- # DEBUG: "default: $state = $event. Args:\n", Dumper $args;
- return 0;
+# Internal: _event_dispatch($type, $event, @args)
+# dispatches event $event
+
+sub _event_dispatch {
+ my ($self, $type, $event, @args) = @_;
+ my $etype = $self->{handlers}{$type}
+ or croak "Unknown event type $etype";
+ my $ev = $etype->{$event};
+ if (!$ev) {
+ if ($event eq '_default') {
+ return; # Nobody seems to be interested.
+ }
+ $self->_event_dispatch($type, '_default', $event, @args);
+ }
+ for my $handler (@{$ev->{forward}}) {
+ my $handled = 0;
+ $handler->(\$handled, $type, $event, [EMAIL PROTECTED]);
+ return if $handled;
+ }
+ if ($event ne '_default') {
+ $self->_event_dispatch($type, '_default', $event, @args);
+ }
}
+
+
+1;
+__END__
+
+=head2 TYPES OF HANDLERS
+
+=head3 want
+
+Handlers of type 'want' are invoked when the server sends a S: WANT message.
The
+first argument of the S: WANT is the event name, the rest are used as
+arguments for the first handler.
+
+The default action, should all handlers pass the event, is to send a
+C: CANT $event to the server.
+
+=head3 smsg
+
+Handlers of type 'smsg' are invoked whenever a command is received from the
server.
+The command name is used for the event name, and the arguments are passed for
+the first handler's @args.
+
+Note that adding a handler with $type of smsg, and $event of 'WANT' may
+prevent handlers of $type want from being called.
+
=head1 EVENTS
-docme
+Events are sent to inform multiple handlers of some happening on the server.
+Any number of event callbacks may be registered to an event, and they will all
+be invoked in an undefined order when the event is dispatched.
=cut
+# implementation notes:
+# events are implemented with handlers, with type 'event'
+# register and unregister create an anonymous wrapper closure to hide the
+# details. These subs are stored in %{$self->{evwrap}}
+# TODO: expire evwrap entries
+
=head2 EVENT MANIPULATION FUNCTIONS
-=head3 register(@Z<>events)
+=head3 register(event => handler)
+Registers to receive a specified event with the specified handler function.
+The special event 'all' may be used to receive all events.
+
=cut
sub register {
- my ($kernel, $heap, $session, @events) =
@_[KERNEL,HEAP,SENDER,ARG0..$#_];
- my $reg = $heap->{reg};
- my $id = $session->ID;
- foreach my $event (@events) {
- $event = uc $event;
- next if exists $reg->{$event}{$id};
- my $tag = '1' . $reg->{$event} . '\0' . $id . '\0' . rand;
- $reg->{$event}{$id} = $tag;
- $kernel->refcount_increment( $id, $tag );
- }
+ my ($self, @events) = @_;
+ while (@events) {
+ my ($ev, $handler) = splice @events, 0, 2;
+ my $wrap = $self->{evwrap}{$handler} || sub {
+ my ($handled, $type, $name, $args) = @_[1..3];
+ if ($type ne 'event') {
+ die "Impossible: event handler on non-event
type!";
+ }
+ $handler->($name, @$args);
+ };
+ $self->{evwrap}{$handler} = $wrap;
+ $self->register_handler('event', $ev, $wrap);
+ }
}
-=head3 unregister(@Z<>events)
+=head3 unregister(event => handler)
+Unregisters the given handler for the specified event.
=cut
sub unregister {
- my ($kernel, $heap, $session, @events) =
@_[KERNEL,HEAP,SENDER,ARG0..$#_];
- my $reg = $heap->{reg};
- my $id = $session->ID;
- foreach my $event (@events) {
- $event = uc $event;
- my $tag;
- next unless $tag = delete $reg->{$event}{$id};
- $kernel->refcount_decrement( $id, $tag );
- }
+ my ($self, @events) = @_;
+ while (@events) {
+ my ($ev, $handler) = splice @events, 0, 2;
+ my $wrap = $self->{evwrap}{$handler} or next;
+ $self->unregister_handler('event', $ev, $wrap);
+ }
}
-sub __dispatch {
- my ($kernel, $heap, $evname, @args) = @_[KERNEL,HEAP,ARG0..$#_];
- $evname = uc $evname;
- my $reg = $heap->{reg};
- $reg->{$evname} ||= {};
- $reg->{ALL} ||= {};
- my %targ = (%{$reg->{$evname}}, %{$reg->{ALL}});
- my @ids = keys %targ;
+=head2 TYPES OF EVENTS
- unshift @args, [$heap->{alias}];
+Client events have the following prototype:
foreach my $id (@ids) {
$kernel->post($id, "haver_$evname", @args);
}
}
-=head2 TYPES OF EVENTS
+The contents of @args vary depending on the event name, and are documented
+below.
-docme
+=head4 connected(Z<>)
+This event is sent when a connection is established (but before it is logged
in)
+
+=head4 connect_fail($Z<>enum, $Z<>estr)
+
+The connection could not be established. An error code is in $enum, and the
human-readable
+version is in $estr
+
+=head4 disconnected($Z<>enum, $Z<>estr)
+
+The connection has been lost. If the server closed the connection, $enum will
be 0 and $estr will
+be meaningless. If the user closed the connection, and the server failed to
respond, $enum will be -1.
+Otherwise, $enum will contain an error code, and $estr the human-readable
version.
+
+=head4 raw_in(@data)
+
+The client has received @data from the Haver server. Mostly useful for
debugging.
+
+=head4 raw_out(@data)
+
+The client has sent @data to the Haver server. Mostly useful for debugging.
+
+=head4 login_request(Z<>)
+
+The server is asking for a UID, and one was not provided in connect().
+The authentication will not proceed until login() is invoked with the UID.
+
+=head4 auth_request(@Z<>methods)
+
+The server requests authentication using one of the listed methods. The
+authentication will not proceed until authenticate() is invoked with a
+suitable authentication handler.
+
+=head4 login(Z<>)
+
+The client has successfully logged in.
+
+=head4 close($Z<>etyp, $Z<>estr)
+Z<XXX: Describe args>
+
+Server is closing connection, and sent $etyp and $estr
+
+=head4 join(Z<>)
+
+$uid has joined $cid
+
+=head4 joined(Z<>)
+
+The client has joined $cid.
+
+=head4 part(Z<>)
+
+$uid has left $cid.
+
+=head4 parted(Z<>)
+
+The client has left $Z<>cid.
+
+=head4 msg($Z<>type, @Z<>msg)
+
+A public message with type $type and contents @msg was sent on channel $cid by
user $uid.
+
+=head4 pmsg($Z<>type, @Z<>text)
+
+A private message with type $type and contents @msg was sent to you by user
$uid.
+
+=head4 users(@Z<>who)
+
+Channel $cid has the users listed in @who in it. Sent in response to message
users().
+
+=head4 bye($Z<>why)
+
+The server is disconnecting you due to the reason in $why
+
+=head4 chans(@Z<>channels)
+
+The server has the channels listed in @channels. Sent in response to message
chans()
+
+=head4 warn($Z<>err, $Z<>short, $Z<>long, @Z<>args)
+
+The server has sent a non-fatal error message with code $err and arguments
@args. $short and $long have the
+short and long human-readable forms, respectively.
+
+=head4 die($Z<>err, $Z<>short, $Z<>long, @Z<>args)
+
+The server has sent a fatal error message with code $err and arguments @args.
$short and $long have the
+short and long human-readable forms, respectively. The connection will be
closed shortly.
+
+=head4 quit($Z<>why)
+
+UID $uid has disconnected due to the reason in $why.
+
=head1 SEE ALSO
-L<http://haverdev.org>
+L<http://wiki.chani3.com/wiki/ProjectHaver/>
=head1 AUTHOR
@@ -621,5 +818,3 @@
=cut
-
-1;