package Net::IRC3::Client::Connection; use base "Net::IRC3::Connection"; use Net::IRC3::Util qw/prefix_nick/; use strict; our $DEBUG; =head1 NAME Net::IRC3::Client::Connection - A highlevel IRC connection =head1 SYNOPSIS use AnyEvent; use Net::IRC3::Client::Connection; my $c = AnyEvent->condvar; my $timer; my $con = new Net::IRC3::Client::Connection; $con->reg_cb (registered => sub { print "I'm in!\n"; 0 }); $con->reg_cb (disconnect => sub { print "I'm out!\n"; 0 }); $con->reg_cb ( sent => sub { if ($_[2] eq 'PRIVMSG') { print "Sent message!\n"; $timer = AnyEvent->timer (after => 1, cb => sub { $c->broadcast }); } 1 } ); $con->send_srv (PRIVMSG => "Hello there i'm the cool Net::IRC3 test script!", 'elmex'); $con->connect ("localhost", 6667); $con->register (qw/testbot testbot testbot/); $c->wait; undef $timer; $con->disconnect; =head1 DESCRIPTION L is a highlevel client connection, that manages all the stuff that noone wants to implement again and again when handling with IRC. For example it PONGs the server or keeps track of the users on a channel. =head2 A NOTE TO CASE MANAGEMENT The case insensitivity of channelnames and nicknames can lead to headaches when dealing with IRC in an automated client which tracks channels and nicknames. I tried to preserve the case in all channel and nicknames Net::IRC3::Client::Connection passes to his user. But in the internal structures i'm using lower case for the channel names. The returned hash from C for example has the lower case of the joined channels as keys. But i tried to preserve the case in all events that are emitted. Please keep this in mind when handling the events. For example a user might joins #TeSt and parts #test later. =head1 EVENTS The following events are emitted by L. Use C as described in L to register to such an event. =over 4 =item B Emitted when the connection got successfully registered. =item B Emitted when C<@nicks> are added to the channel C<$channel>, this happens for example when someone JOINs a channel or when you get a RPL_NAMREPLY (see RFC2812). =item B Emitted when C<@nicks> are removed from the channel C<$channel>, happens for example when they PART, QUIT or get KICKed. =item B Emitted when a nickname on a channel changes. This is emitted when a NICK change occurs from C<$old_nick> to C<$new_nick> give the application a chance to quickly analyze what channels were affected. C<$is_myself> is true when youself was the one who changed the nick. =item B Emitted when C<$nick> enters the channel C<$channel> by JOINing. C<$is_myself> is true if youself are the one who JOINs. =item B Emitted when C<$nick> PARTs the channel C<$channel>. C<$is_myself> is true if youself are the one who PARTs. C<$msg> is the PART message. =item B Emitted when C<$old_nick> is renamed to C<$new_nick>. C<$is_myself> is true when youself was the one who changed the nick. =item B Emitted when the nickname C<$nick> QUITs with the message C<$msg>. =item B Emitted for NOTICE and PRIVMSG where the target C<$channel> is a channel. C<$ircmsg> is the original IRC message hash like it is returned by C. =item B Emitted for NOTICE and PRIVMSG where the target C<$nick> (most of the time you) is a nick. C<$ircmsg> is the original IRC message hash like it is returned by C. =item B Emitted when any error occurs. C<$code> is the 3 digit error id string from RFC 2812 and C<$message> is a description of the error. C<@params> is the complete parameter list of the error command, including the C<$message> at the end of C<@params>. You may use Net::IRC3::Util::rfc_code_to_name to convert C<$code> to the error name from the RFC 2812. eg.: rfc_code_to_name ('471') => 'ERR_CHANNELISFULL' =back =head1 METHODS =over 4 =cut sub new { my $this = shift; my $class = ref($this) || $this; my $self = $class->SUPER::new (@_); $self->reg_cb (irc_001 => \&welcome_cb); $self->reg_cb (irc_join => \&join_cb); $self->reg_cb (irc_nick => \&nick_cb); $self->reg_cb (irc_part => \&part_cb); $self->reg_cb (irc_kick => \&kick_cb); $self->reg_cb (irc_quit => \&quit_cb); $self->reg_cb (irc_353 => \&namereply_cb); $self->reg_cb (irc_366 => \&endofnames_cb); $self->reg_cb (irc_ping => \&ping_cb); $self->reg_cb (irc_privmsg => \&privmsg_cb); $self->reg_cb (irc_notice => \&privmsg_cb); $self->reg_cb ('irc_*' => \&debug_cb) if $DEBUG; $self->reg_cb ('irc_*' => \&anymsg_cb); $self->reg_cb (channel_remove => \&channel_remove_event); $self->reg_cb (channel_add => \&channel_add_event); $self->reg_cb (disconnect => \&disconnect_cb); $self->reg_cb (irc_437 => \&change_nick_login); $self->reg_cb (irc_433 => \&change_nick_login); $self->{nick_change} = sub { my ($old_nick) = @_; "${old_nick}_" }; return $self; } =item B Sends the IRC registration commands NICK and USER. =cut sub register { my ($self, $nick, $user, $real) = @_; $self->{nick} = $nick; $self->{user} = $user; $self->{real} = $real; $self->send_msg (undef, "NICK", undef, $nick); $self->send_msg (undef, "USER", $real || $nick, $user || $nick, "*", "0"); } =item B This method lets you modify the nickname renaming mechanism when registering the connection. C<$callback> is called with the current nickname as first argument when a ERR_NICKNAMEINUSE or ERR_UNAVAILRESOURCE error occurs on login. The returnvalue of C<$callback> will then be used to change the nickname. The default callback appends '_' to the end of the nickname supplied in the C routine. If the callback returns the same nickname that was given it the connection will be terminated. =cut sub set_nick_change_cb { my ($self, $cb) = @_; $self->{nick_change} = $cb; } =item B Returns the current nickname, under which this connection is registered at the IRC server. It might be different from the one that was passed to C as a nick-collision might happened on login. =cut sub nick { $_[0]->{nick} } =item B Returns a true value when the connection has been registered successfull and you can send commands. =cut sub connected { $_[0]->{connected} } =item B This returns a hash reference. The keys are the currently joined channels in lower case. The values are hash references which contain the joined nicks as key. NOTE: Future versions might preserve the case from the JOIN command to the channels. =cut sub channel_list { my ($self) = @_; return $self->{channel_list} || {}; } =item B This function sends an IRC message that is constructed by C (see L). If the connection isn't yet registered (for example if the connection is slow) and hasn't got a welcome (IRC command 001) from the server yet, the IRC message is queued until it gets a welcome. =cut sub send_srv { my ($self, @msg) = @_; if ($self->{connected}) { $self->send_msg (undef, @msg); } else { push @{$self->{con_queue}}, \@msg; } } =item B Clears the server send queue. =cut sub clear_srv_queue { my ($self) = @_; $self->{con_queue} = []; } =item B This function sends a message (constructed by C to the server, like C only that it will queue the messages if it hasn't joined the channel C<$channel> yet. The queued messages will be send once the connection successfully JOINed the C<$channel>. C<$channel> will be lowercased so that any case that comes from the server matches. (Yes, IRC handles upper and lower case as equal :-( Be careful with this, there are chances you might not join the channel you wanted to join. You may wanted to join #bla and the server redirects that and sends you that you joined #blubb. You may use C to remove the queue after some timeout after joining, so that you don't end up with a memory leak. =cut sub send_chan { my ($self, $chan, @msg) = @_; if ($self->{channel_list}->{lc $chan}) { $self->send_msg (undef, @msg); } else { push @{$self->{chan_queue}->{lc $chan}}, \@msg; } } =item B Clears the channel queue of the channel C<$channel>. =cut sub clear_chan_queue { my ($self, $chan) = @_; $self->{chan_queue}->{lc $chan} = []; } ################################################################################ # Private utility functions ################################################################################ sub _was_me { my ($self, $msg) = @_; lc prefix_nick ($msg) eq lc $self->nick () } ################################################################################ # Callbacks ################################################################################ sub channel_remove_event { my ($self, $chan, @nicks) = @_; for my $nick (@nicks) { if (lc ($nick) eq lc ($self->nick ())) { delete $self->{chan_queue}->{lc $chan}; delete $self->{channel_list}->{lc $chan}; last; } else { delete $self->{channel_list}->{lc $chan}->{$nick}; } } 1; } sub channel_add_event { my ($self, $chan, @nicks) = @_; for my $nick (@nicks) { if (lc ($nick) eq lc ($self->nick ())) { for (@{$self->{chan_queue}->{lc $chan}}) { $self->send_msg (undef, @$_); } $self->clear_chan_queue ($chan); } $self->{channel_list}->{lc $chan}->{$nick} = 1; } 1; } sub anymsg_cb { my ($self, $msg) = @_; my $cmd = lc $msg->{command}; if ( $cmd ne "privmsg" and $cmd ne "notice" and $cmd ne "part" and $cmd ne "join" and not ($cmd >= 400 and $cmd <= 599) ) { $self->event (statmsg => $msg); } elsif ($cmd >= 400 and $cmd <= 599) { $self->event (error => $msg->{command}, $msg->{trailing}, @{$msg->{params}}); } 1; } sub privmsg_cb { my ($self, $msg) = @_; my $targ = $msg->{params}->[0]; if ($targ =~ m/^(?:[#+&]|![A-Z0-9]{5})/) { $self->event (publicmsg => $targ, $msg); } else { $self->event (privatemsg => $targ, $msg); } 1; } sub welcome_cb { my ($self, $msg) = @_; $self->{connected} = 1; for (@{$self->{con_queue}}) { $self->send_msg (undef, @$_); } $self->clear_srv_queue (); $self->event ('registered'); 1; } sub ping_cb { my ($self, $msg) = @_; $self->send_msg (undef, "PONG", $msg->{params}->[0]); 1; } sub nick_cb { my ($self, $msg) = @_; my $nick = prefix_nick ($msg); my $newnick = $msg->{params}->[0]; my $wasme = $self->_was_me ($msg); if ($wasme) { $self->{nick} = $newnick } my @chans; for my $channame (keys %{$self->{channel_list}}) { my $chan = $self->{channel_list}->{$channame}; if (exists $chan->{$nick}) { delete $chan->{$nick}; $chan->{$newnick} = 1; push @chans, $channame; } } for (@chans) { $self->event (channel_change => $_, $nick, $newnick, $wasme); } $self->event (nick_change => $nick, $newnick, $wasme); } sub namereply_cb { my ($self, $msg) = @_; my @nicks = split / /, $msg->{trailing}; push @{$self->{_tmp_namereply}}, @nicks; 1; } sub endofnames_cb { my ($self, $msg) = @_; my $chan = $msg->{params}->[1]; $self->event (channel_add => $chan, map { s/^[@\+]//; $_ } @{delete $self->{_tmp_namereply}}); 1; } sub join_cb { my ($self, $msg) = @_; my $chan = $msg->{params}->[0]; my $nick = prefix_nick ($msg); $self->event (channel_add => $chan, $nick); $self->event (join => $nick, $chan, $self->_was_me ($msg)); 1; } sub part_cb { my ($self, $msg) = @_; my $chan = $msg->{params}->[0]; my $nick = prefix_nick ($msg); $self->event (part => $nick, $chan, $self->_was_me ($msg), $msg->{params}->[1]); $self->event (channel_remove => $chan, $nick); 1; } sub kick_cb { my ($self, $msg) = @_; my $chan = $msg->{params}->[0]; my $kicked_nick = $msg->{params}->[1]; $self->event (channel_remove => $chan, $kicked_nick); 1; } sub quit_cb { my ($self, $msg) = @_; my $nick = prefix_nick ($msg); $self->event (quit => $nick, $msg->{params}->[1]); for (keys %{$self->{channel_list}}) { $self->event (channel_remove => $_, $nick) if $self->{channel_list}->{$_}->{$nick}; } 1; } sub debug_cb { my ($self, $msg) = @_; print "$self->{h}:$self->{p} > "; my $par = delete $msg->{params}; print (join " ", map { $_ => $msg->{$_} } sort keys %$msg); print " params:"; print (join ",", @$par); print "\n"; 1; } sub change_nick_login { my ($self, $msg) = @_; unless ($self->{connected}) { my $newnick = $self->{nick_change}->($self->nick); if (lc $newnick eq lc $self->{nick}) { $self->disconnect; return 0; } $self->{nick} = $newnick; $self->send_msg (undef, "NICK", undef, $newnick); } not ($self->{connected}) # kill the cb when connected } sub disconnect_cb { my ($self) = @_; for (keys %{$self->{channel_list}}) { $self->event (channel_remove => $_, $self->nick) } 1 } =back =head1 EXAMPLES See samples/netirc3cl and other samples in samples/ for some examples on how to use Net::IRC3::Client::Connection. =head1 AUTHOR Robin Redeker, C<< >> =head1 SEE ALSO L RFC 2812 - Internet Relay Chat: Client Protocol =head1 COPYRIGHT & LICENSE Copyright 2006 Robin Redeker, all rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut 1;