mirror of
https://github.com/pragma-/pbot.git
synced 2024-11-25 13:29:29 +01:00
1701 lines
56 KiB
Perl
1701 lines
56 KiB
Perl
#####################################################################
|
|
# #
|
|
# Net::IRC -- Object-oriented Perl interface to an IRC server #
|
|
# #
|
|
# Connection.pm: The basic functions for a simple IRC connection #
|
|
# #
|
|
# #
|
|
# Copyright (c) 2001 Pete Sergeant, Greg Bacon & Dennis Taylor. #
|
|
# All rights reserved. #
|
|
# #
|
|
# This module is free software; you can redistribute or #
|
|
# modify it under the terms of Perl's Artistic License. #
|
|
# #
|
|
#####################################################################
|
|
|
|
package PBot::Core::IRC::Connection; # pragma_ 2011/21/01
|
|
|
|
use feature 'unicode_strings';
|
|
use utf8;
|
|
|
|
use PBot::Core::IRC::Event; # pragma_ 2011/21/01
|
|
use PBot::Core::IRC::DCC; # pragma_ 2011/21/01
|
|
use IO::Socket;
|
|
use IO::Socket::INET;
|
|
use Symbol;
|
|
use Carp;
|
|
use Encode;
|
|
|
|
# all this junk below just to conditionally load a module
|
|
# sometimes even perl is braindead...
|
|
|
|
eval 'use Time::HiRes qw(time)';
|
|
if (!$@) {
|
|
sub time ();
|
|
use subs 'time';
|
|
require Time::HiRes;
|
|
Time::HiRes->import('time');
|
|
}
|
|
|
|
use strict;
|
|
|
|
use vars (
|
|
'$AUTOLOAD',
|
|
);
|
|
|
|
# The names of the methods to be handled by &AUTOLOAD.
|
|
my %autoloaded = (
|
|
'ircname' => undef,
|
|
'port' => undef,
|
|
'username' => undef,
|
|
'socket' => undef,
|
|
'verbose' => undef,
|
|
'parent' => undef,
|
|
'hostname' => undef,
|
|
'pacing' => undef,
|
|
'utf8' => undef,
|
|
'pbot' => undef,
|
|
'tls' => undef,
|
|
'tls_ca_path' => undef,
|
|
'tls_ca_file' => undef,
|
|
);
|
|
|
|
# This hash will contain any global default handlers that the user specifies.
|
|
|
|
my %_udef = ();
|
|
|
|
# Creates a new IRC object and assigns some default attributes.
|
|
sub new {
|
|
my $proto = shift;
|
|
|
|
my $self = { # obvious defaults go here, rest are user-set
|
|
_debug => $_[0]->{_debug},
|
|
_port => 6667,
|
|
|
|
# Evals are for non-UNIX machines, just to make sure.
|
|
_username => eval { scalar getpwuid($>) } || $ENV{USER} || $ENV{LOGNAME} || "japh",
|
|
_ircname => $ENV{IRCNAME} || eval { (getpwuid($>))[6] } || "Just Another Perl Hacker",
|
|
_nick => $ENV{IRCNICK} || eval { scalar getpwuid($>) } || $ENV{USER} || $ENV{LOGNAME} || "WankerBot",
|
|
_ignore => {},
|
|
_handler => {},
|
|
_verbose => 0, # Is this an OK default?
|
|
_parent => shift,
|
|
_frag => '',
|
|
_connected => 0,
|
|
_maxlinelen => 510, # The RFC says we shouldn't exceed this.
|
|
_lastsl => 0,
|
|
_pacing => 0, # no pacing by default
|
|
_tls => 0, # no tls by default (TODO: perhaps this should be enabled by default)
|
|
_tls_ca_path => undef,
|
|
_tls_ca_file => undef,
|
|
_utf8 => 0,
|
|
_format => {'default' => "[%f:%t] %m <%d>",},
|
|
_pbot => undef,
|
|
};
|
|
|
|
bless $self, $proto;
|
|
|
|
# do any necessary initialization here
|
|
$self->connect(@_) if @_;
|
|
|
|
return $self;
|
|
}
|
|
|
|
# Takes care of the methods in %autoloaded
|
|
# Sets specified attribute, or returns its value if called without args.
|
|
sub AUTOLOAD {
|
|
my $self = @_; ## can't modify @_ for goto &name
|
|
my $class = ref $self; ## die here if !ref($self) ?
|
|
my $meth;
|
|
|
|
# -- #perl was here! --
|
|
# <Teratogen> absolute power corrupts absolutely, but it's a helluva lot
|
|
# of fun.
|
|
# <Teratogen> =)
|
|
|
|
($meth = $AUTOLOAD) =~ s/^.*:://; ## strip fully qualified portion
|
|
|
|
unless (exists $autoloaded{$meth}) { croak "No method called \"$meth\" for $class object."; }
|
|
|
|
eval <<EOSub;
|
|
sub $meth {
|
|
my \$self = shift;
|
|
|
|
if (\@_) {
|
|
my \$old = \$self->{"_$meth"};
|
|
|
|
\$self->{"_$meth"} = shift;
|
|
|
|
return \$old;
|
|
}
|
|
else {
|
|
return \$self->{"_$meth"};
|
|
}
|
|
}
|
|
EOSub
|
|
|
|
# no reason to play this game every time
|
|
goto &$meth;
|
|
}
|
|
|
|
# This sub is the common backend to add_handler and add_global_handler
|
|
#
|
|
sub _add_generic_handler {
|
|
my ($self, $event, $ref, $rp, $hash_ref, $real_name) = @_;
|
|
my $ev;
|
|
my %define = ("replace" => 0, "before" => 1, "after" => 2);
|
|
|
|
unless (@_ >= 3) { croak "Not enough arguments to $real_name()"; }
|
|
unless (ref($ref) eq 'CODE') { croak "Second argument of $real_name isn't a coderef"; }
|
|
|
|
# Translate REPLACE, BEFORE and AFTER.
|
|
if (not defined $rp) { $rp = 0; }
|
|
elsif ($rp =~ /^\D/) { $rp = $define{lc $rp} || 0; }
|
|
|
|
foreach $ev (ref $event eq "ARRAY" ? @{$event} : $event) {
|
|
# Translate numerics to names
|
|
if ($ev =~ /^\d/) {
|
|
$ev = PBot::Core::IRC::Event->trans($ev); # pragma_ 2011/21/01
|
|
unless ($ev) {
|
|
carp "Unknown event type in $real_name: $ev";
|
|
return;
|
|
}
|
|
}
|
|
|
|
$hash_ref->{lc $ev} = [$ref, $rp];
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
# This sub will assign a user's custom function to a particular event which
|
|
# might be received by any Connection object.
|
|
# Takes 3 args: the event to modify, as either a string or numeric code
|
|
# If passed an arrayref, the array is assumed to contain
|
|
# all event names which you want to set this handler for.
|
|
# a reference to the code to be executed for the event
|
|
# (optional) A value indicating whether the user's code should replace
|
|
# the built-in handler, or be called with it. Possible values:
|
|
# 0 - Replace the built-in handlers entirely. (the default)
|
|
# 1 - Call this handler right before the default handler.
|
|
# 2 - Call this handler right after the default handler.
|
|
# These can also be referred to by the #define-like strings in %define.
|
|
sub add_global_handler {
|
|
my ($self, $event, $ref, $rp) = @_;
|
|
return $self->_add_generic_handler($event, $ref, $rp, \%_udef, 'add_global_handler');
|
|
}
|
|
|
|
# This sub will assign a user's custom function to a particular event which
|
|
# this connection might receive. Same args as above.
|
|
sub add_handler {
|
|
my ($self, $event, $ref, $rp) = @_;
|
|
return $self->_add_generic_handler($event, $ref, $rp, $self->{_handler}, 'add_handler');
|
|
}
|
|
|
|
# Hooks every event we know about...
|
|
sub add_default_handler {
|
|
my ($self, $ref, $rp) = @_;
|
|
foreach my $eventtype (keys(%PBot::Core::IRC::Event::_names)) { # pragma_ 2011/21/01
|
|
$self->_add_generic_handler($eventtype, $ref, $rp, $self->{_handler}, 'add_default_handler');
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
# Why do I even bother writing subs this simple? Sends an ADMIN command.
|
|
# Takes 1 optional arg: the name of the server you want to query.
|
|
sub admin {
|
|
my $self = shift; # Thank goodness for AutoLoader, huh?
|
|
# Perhaps we'll finally use it soon.
|
|
|
|
$self->sl("ADMIN" . ($_[0] ? " $_[0]" : ""));
|
|
}
|
|
|
|
# Toggles away-ness with the server. Optionally takes an away message.
|
|
sub away {
|
|
my $self = shift;
|
|
$self->sl("AWAY" . ($_[0] ? " :$_[0]" : ""));
|
|
}
|
|
|
|
# Attempts to connect to the specified IRC (server, port) with the specified
|
|
# (nick, username, ircname). Will close current connection if already open.
|
|
sub connect {
|
|
my $self = shift;
|
|
my ($password, $sock);
|
|
|
|
if (@_) {
|
|
my (%arg) = @_;
|
|
|
|
$self->hostname($arg{'LocalAddr'}) if exists $arg{'LocalAddr'};
|
|
$password = $arg{'Password'} if exists $arg{'Password'};
|
|
$self->nick($arg{'Nick'}) if exists $arg{'Nick'};
|
|
$self->port($arg{'Port'}) if exists $arg{'Port'};
|
|
$self->server($arg{'Server'}) if exists $arg{'Server'};
|
|
$self->ircname($arg{'Ircname'}) if exists $arg{'Ircname'};
|
|
$self->username($arg{'Username'}) if exists $arg{'Username'};
|
|
$self->pacing($arg{'Pacing'}) if exists $arg{'Pacing'};
|
|
$self->debug($arg{'Debug'}) if exists $arg{'Debug'};
|
|
$self->utf8($arg{'UTF8'}) if exists $arg{'UTF8'};
|
|
$self->pbot($arg{'PBot'}) if exists $arg{'PBot'};
|
|
$self->tls($arg{'TLS'}) if exists $arg{'TLS'};
|
|
$self->tls_ca_path($arg{'TLS_ca_path'}) if exists $arg{'TLS_ca_path'};
|
|
$self->tls_ca_file($arg{'TLS_ca_file'}) if exists $arg{'TLS_ca_file'};
|
|
}
|
|
|
|
# Lots of error-checking claptrap first...
|
|
unless ($self->server) {
|
|
unless ($ENV{IRCSERVER}) { croak "No server address specified in connect()"; }
|
|
$self->server($ENV{IRCSERVER});
|
|
}
|
|
unless ($self->nick) {
|
|
$self->nick($ENV{IRCNICK} || eval { scalar getpwuid($>) } || $ENV{USER} || $ENV{LOGNAME} || "WankerBot");
|
|
}
|
|
unless ($self->port) { $self->port($ENV{IRCPORT} || 6667); }
|
|
unless ($self->ircname) {
|
|
$self->ircname($ENV{IRCNAME} || eval { (getpwuid($>))[6] } || "Just Another Perl Hacker");
|
|
}
|
|
unless ($self->username) {
|
|
$self->username(eval { scalar getpwuid($>) } || $ENV{USER} || $ENV{LOGNAME} || "japh");
|
|
}
|
|
|
|
# Now for the socket stuff...
|
|
if ($self->connected) { $self->quit("Changing servers"); }
|
|
|
|
if ($self->tls) {
|
|
use IO::Socket::SSL;
|
|
|
|
if ($self->tls_ca_file) {
|
|
$self->socket(
|
|
IO::Socket::SSL->new(
|
|
PeerAddr => $self->server,
|
|
PeerPort => $self->port,
|
|
Proto => "tcp",
|
|
LocalAddr => $self->hostname,
|
|
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
SSL_ca_file => $self->tls_ca_file,
|
|
)
|
|
);
|
|
} elsif ($self->tls_ca_path) {
|
|
$self->socket(
|
|
IO::Socket::SSL->new(
|
|
PeerAddr => $self->server,
|
|
PeerPort => $self->port,
|
|
Proto => "tcp",
|
|
LocalAddr => $self->hostname,
|
|
SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
|
|
SSL_ca_path => $self->tls_ca_path,
|
|
)
|
|
);
|
|
} else {
|
|
$self->socket(
|
|
IO::Socket::SSL->new(
|
|
PeerAddr => $self->server,
|
|
PeerPort => $self->port,
|
|
Proto => "tcp",
|
|
LocalAddr => $self->hostname,
|
|
)
|
|
);
|
|
}
|
|
} else {
|
|
$self->socket(
|
|
IO::Socket::INET->new(
|
|
PeerAddr => $self->server,
|
|
PeerPort => $self->port,
|
|
Proto => "tcp",
|
|
LocalAddr => $self->hostname,
|
|
)
|
|
);
|
|
}
|
|
|
|
if (!$self->socket) {
|
|
if ($self->tls) {
|
|
carp(
|
|
sprintf "Can't connect to %s:%s: error=$! SSL_ERROR=$SSL_ERROR",
|
|
$self->server, $self->port
|
|
);
|
|
} else {
|
|
carp(
|
|
sprintf "Can't connect to %s:%s: $!",
|
|
$self->server, $self->port
|
|
);
|
|
}
|
|
$self->error(1);
|
|
return;
|
|
}
|
|
|
|
if ($self->tls) {
|
|
$self->socket->blocking(0);
|
|
}
|
|
|
|
# send CAP LS first
|
|
$self->sl("CAP LS 302");
|
|
|
|
# Send a PASS command if they specified a password. According to
|
|
# the RFC, we should do this as soon as we connect.
|
|
if (defined $password) { $self->sl("PASS $password"); }
|
|
|
|
# Now, log in to the server...
|
|
unless (
|
|
$self->sl('NICK ' . $self->nick()) and $self->sl(
|
|
sprintf(
|
|
"USER %s %s %s :%s",
|
|
$self->username(),
|
|
"foo.bar.com",
|
|
$self->server(),
|
|
$self->ircname()
|
|
)
|
|
)
|
|
)
|
|
{
|
|
carp "Couldn't send introduction to server: $!";
|
|
$self->error(1);
|
|
$! = "Couldn't send NICK/USER introduction to " . $self->server;
|
|
return;
|
|
}
|
|
|
|
$self->{_connected} = 1;
|
|
$self->parent->addconn($self);
|
|
}
|
|
|
|
# Returns a boolean value based on the state of the object's socket.
|
|
sub connected {
|
|
my $self = shift;
|
|
|
|
return ($self->{_connected} and $self->socket());
|
|
}
|
|
|
|
# Sends a CTCP request to some hapless victim(s).
|
|
# Takes at least two args: the type of CTCP request (case insensitive)
|
|
# the nick or channel of the intended recipient(s)
|
|
# Any further args are arguments to CLIENTINFO, ERRMSG, or ACTION.
|
|
sub ctcp {
|
|
my ($self, $type, $target) = splice @_, 0, 3;
|
|
$type = uc $type;
|
|
|
|
unless ($target) { croak "Not enough arguments to ctcp()"; }
|
|
|
|
if ($type eq "PING") {
|
|
unless ($self->sl("PRIVMSG $target :\001PING " . int(time) . "\001")) {
|
|
carp "Socket error sending $type request in ctcp()";
|
|
return;
|
|
}
|
|
} elsif (($type eq "CLIENTINFO" or $type eq "ACTION") and @_) {
|
|
unless ($self->sl("PRIVMSG $target :\001$type " . CORE::join(" ", @_) . "\001")) {
|
|
carp "Socket error sending $type request in ctcp()";
|
|
return;
|
|
}
|
|
} elsif ($type eq "ERRMSG") {
|
|
unless (@_) {
|
|
carp "Not enough arguments to $type in ctcp()";
|
|
return;
|
|
}
|
|
unless ($self->sl("PRIVMSG $target :\001ERRMSG " . CORE::join(" ", @_) . "\001")) {
|
|
carp "Socket error sending $type request in ctcp()";
|
|
return;
|
|
}
|
|
} else {
|
|
unless ($self->sl("PRIVMSG $target :\001$type " . CORE::join(" ", @_) . "\001")) {
|
|
carp "Socket error sending $type request in ctcp()";
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Sends replies to CTCP queries. Simple enough, right?
|
|
# Takes 2 args: the target person or channel to send a reply to
|
|
# the text of the reply
|
|
sub ctcp_reply {
|
|
my $self = shift;
|
|
|
|
$self->notice($_[0], "\001" . $_[1] . "\001");
|
|
}
|
|
|
|
# Sets or returns the debugging flag for this object.
|
|
# Takes 1 optional arg: a new boolean value for the flag.
|
|
sub debug {
|
|
my $self = shift;
|
|
if (@_) { $self->{_debug} = $_[0]; }
|
|
return $self->{_debug};
|
|
}
|
|
|
|
# Dequotes CTCP messages according to ctcp.spec. Nothing special.
|
|
# Then it breaks them into their component parts in a flexible, ircII-
|
|
# compatible manner. This is not quite as trivial. Oh, well.
|
|
# Takes 1 arg: the line to be dequoted.
|
|
sub dequote {
|
|
my $line = shift;
|
|
my ($order, @chunks) = (0, ()); # CHUNG! CHUNG! CHUNG!
|
|
|
|
# Filter misplaced \001s before processing... (Thanks, Tom!)
|
|
substr($line, rindex($line, "\001"), 1) = '\\a' unless ($line =~ tr/\001//) % 2 == 0;
|
|
|
|
# Thanks to Abigail (abigail@fnx.com) for this clever bit.
|
|
if (index($line, "\cP") >= 0) { # dequote low-level \n, \r, ^P, and \0.
|
|
my (%h) = (n => "\012", r => "\015", 0 => "\0", "\cP" => "\cP");
|
|
$line =~ s/\cP([nr0\cP])/$h{$1}/g;
|
|
}
|
|
$line =~ s/\\([^\\a])/$1/g; # dequote unnecessarily quoted characters.
|
|
|
|
# If true, it's in odd order... ctcp commands start with first chunk.
|
|
$order = 1 if index($line, "\001") == 0;
|
|
@chunks = map { s/\\\\/\\/g; $_ } (split /\cA/, $line);
|
|
|
|
return ($order, @chunks);
|
|
}
|
|
|
|
# Standard destructor method for the GC routines. (HAHAHAH! DIE! DIE! DIE!)
|
|
sub DESTROY {
|
|
my $self = shift;
|
|
$self->handler("destroy", "nobody will ever use this");
|
|
$self->quit();
|
|
|
|
# anything else?
|
|
}
|
|
|
|
# Disconnects this Connection object cleanly from the server.
|
|
# Takes at least 1 arg: the format and args parameters to Event->new().
|
|
sub disconnect {
|
|
my $self = shift;
|
|
|
|
$self->{_connected} = 0;
|
|
$self->parent->removeconn($self);
|
|
$self->socket(undef);
|
|
$self->handler(
|
|
PBot::Core::IRC::Event->new(
|
|
"disconnect", # pragma_ 2011/21/01
|
|
$self->server,
|
|
'',
|
|
@_
|
|
)
|
|
);
|
|
}
|
|
|
|
# Tells IRC.pm if there was an error opening this connection. It's just
|
|
# for sane error passing.
|
|
# Takes 1 optional arg: the new value for $self->{'iserror'}
|
|
sub error {
|
|
my $self = shift;
|
|
|
|
$self->{'iserror'} = $_[0] if @_;
|
|
return $self->{'iserror'};
|
|
}
|
|
|
|
# Lets the user set or retrieve a format for a message of any sort.
|
|
# Takes at least 1 arg: the event whose format you're inquiring about
|
|
# (optional) the new format to use for this event
|
|
sub format {
|
|
my ($self, $ev) = splice @_, 0, 2;
|
|
|
|
unless ($ev) { croak "Not enough arguments to format()"; }
|
|
|
|
if (@_) { $self->{'_format'}->{$ev} = $_[0]; }
|
|
else { return ($self->{'_format'}->{$ev} || $self->{'_format'}->{'default'}); }
|
|
}
|
|
|
|
# Calls the appropriate handler function for a specified event.
|
|
# Takes 2 args: the name of the event to handle
|
|
# the arguments to the handler function
|
|
sub handler {
|
|
my ($self, $event) = splice @_, 0, 2;
|
|
|
|
unless (defined $event) { croak 'Too few arguments to Connection->handler()'; }
|
|
|
|
# Get name of event.
|
|
my $ev;
|
|
if (ref $event) { $ev = $event->type; }
|
|
elsif (defined $event) {
|
|
$ev = $event;
|
|
$event = PBot::Core::IRC::Event->new($event, '', '', ''); # pragma_ 2011/21/01
|
|
} else {
|
|
croak "Not enough arguments to handler()";
|
|
}
|
|
|
|
unless ($ev eq 'pong') {
|
|
print STDERR "--- Trying to handle event '$ev'.\n" if $self->{_debug};
|
|
|
|
if ($self->{_debug} > 1) {
|
|
use Data::Dumper;
|
|
print STDERR "ev: ", Dumper($ev), "\nevent: ", Dumper($event), "\n";
|
|
}
|
|
}
|
|
|
|
my $handler = undef;
|
|
if (exists $self->{_handler}->{$ev}) { $handler = $self->{_handler}->{$ev}; }
|
|
elsif (exists $_udef{$ev}) { $handler = $_udef{$ev}; }
|
|
else { return $self->_default($event, @_); }
|
|
|
|
my ($code, $rp) = @{$handler};
|
|
|
|
# If we have args left, try to call the handler.
|
|
if ($rp == 0) { # REPLACE
|
|
&$code($self, $event, @_);
|
|
} elsif ($rp == 1) { # BEFORE
|
|
&$code($self, $event, @_);
|
|
$self->_default($event, @_);
|
|
} elsif ($rp == 2) { # AFTER
|
|
$self->_default($event, @_);
|
|
&$code($self, $event, @_);
|
|
} else {
|
|
confess "Bad parameter passed to handler(): rp=$rp";
|
|
}
|
|
|
|
unless ($ev eq 'pong') {
|
|
print STDERR "--- Handler for '$ev' called.\n" if $self->{_debug};
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
# Lets a user set hostmasks to discard certain messages from, or (if called
|
|
# with only 1 arg), show a list of currently ignored hostmasks of that type.
|
|
# Takes 2 args: type of ignore (public, msg, ctcp, etc)
|
|
# (optional) [mask(s) to be added to list of specified type]
|
|
sub ignore {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak "Not enough arguments to ignore()"; }
|
|
|
|
if (@_ == 1) {
|
|
if (exists $self->{_ignore}->{$_[0]}) { return @{$self->{_ignore}->{$_[0]}}; }
|
|
else { return (); }
|
|
} elsif (@_ > 1) { # code defensively, remember...
|
|
my $type = shift;
|
|
|
|
# I moved this part further down as an Obsessive Efficiency
|
|
# Initiative. It shouldn't be a problem if I do _parse right...
|
|
# ... but those are famous last words, eh?
|
|
unless (grep { $_ eq $type } qw(public msg ctcp notice channel nick other all)) {
|
|
carp "$type isn't a valid type to ignore()";
|
|
return;
|
|
}
|
|
|
|
if (exists $self->{_ignore}->{$type}) { push @{$self->{_ignore}->{$type}}, @_; }
|
|
else { $self->{_ignore}->{$type} = [@_]; }
|
|
}
|
|
}
|
|
|
|
# Yet Another Ridiculously Simple Sub. Sends an INFO command.
|
|
# Takes 1 optional arg: the name of the server to query.
|
|
sub info {
|
|
my $self = shift;
|
|
|
|
$self->sl("INFO" . ($_[0] ? " $_[0]" : ""));
|
|
}
|
|
|
|
# Invites someone to an invite-only channel. Whoop.
|
|
# Takes 2 args: the nick of the person to invite
|
|
# the channel to invite them to.
|
|
# I hate the syntax of this command... always seemed like a protocol flaw.
|
|
sub invite {
|
|
my $self = shift;
|
|
|
|
unless (@_ > 1) { croak "Not enough arguments to invite()"; }
|
|
|
|
$self->sl("INVITE $_[0] $_[1]");
|
|
}
|
|
|
|
# Checks if a particular nickname is in use.
|
|
# Takes at least 1 arg: nickname(s) to look up.
|
|
sub ison {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak 'Not enough args to ison().'; }
|
|
|
|
$self->sl("ISON " . CORE::join(" ", @_));
|
|
}
|
|
|
|
# Joins a channel on the current server if connected, eh?.
|
|
# Corresponds to /JOIN command.
|
|
# Takes 2 args: name of channel to join
|
|
# optional channel password, for +k channels
|
|
sub join {
|
|
my $self = shift;
|
|
|
|
unless ($self->connected) {
|
|
carp "Can't join() -- not connected to a server";
|
|
return;
|
|
}
|
|
|
|
unless (@_) { croak "Not enough arguments to join()"; }
|
|
|
|
return $self->sl("JOIN $_[0]" . ($_[1] ? " $_[1]" : ""));
|
|
|
|
}
|
|
|
|
# Takes at least 2 args: the channel to kick the bastard from
|
|
# the nick of the bastard in question
|
|
# (optional) a parting comment to the departing bastard
|
|
sub kick {
|
|
my $self = shift;
|
|
|
|
unless (@_ > 1) { croak "Not enough arguments to kick()"; }
|
|
return $self->sl("KICK $_[0] $_[1]" . ($_[2] ? " :$_[2]" : ""));
|
|
}
|
|
|
|
# Gets a list of all the servers that are linked to another visible server.
|
|
# Takes 2 optional args: it's a bitch to describe, and I'm too tired right
|
|
# now, so read the RFC.
|
|
sub links {
|
|
my ($self) = (shift, undef);
|
|
|
|
$self->sl("LINKS" . (scalar(@_) ? " " . CORE::join(" ", @_[0, 1]) : ""));
|
|
}
|
|
|
|
# Requests a list of channels on the server, or a quick snapshot of the current
|
|
# channel (the server returns channel name, # of users, and topic for each).
|
|
sub list {
|
|
my $self = shift;
|
|
|
|
$self->sl("LIST " . CORE::join(",", @_));
|
|
}
|
|
|
|
# Sends a request for some server/user stats.
|
|
# Takes 1 optional arg: the name of a server to request the info from.
|
|
sub lusers {
|
|
my $self = shift;
|
|
|
|
$self->sl("LUSERS" . ($_[0] ? " $_[0]" : ""));
|
|
}
|
|
|
|
# Gets and/or sets the max line length. The value previous to the sub
|
|
# call will be returned.
|
|
# Takes 1 (optional) arg: the maximum line length (in bytes)
|
|
sub maxlinelen {
|
|
my $self = shift;
|
|
|
|
my $ret = $self->{_maxlinelen};
|
|
|
|
$self->{_maxlinelen} = shift if @_;
|
|
|
|
return $ret;
|
|
}
|
|
|
|
# Sends an action to the channel/nick you specify. It's truly amazing how
|
|
# many IRCers have no idea that /me's are actually sent via CTCP.
|
|
# Takes 2 args: the channel or nick to bother with your witticism
|
|
# the action to send (e.g., "weed-whacks billn's hand off.")
|
|
sub me {
|
|
my $self = shift;
|
|
|
|
$self->ctcp("ACTION", $_[0], $_[1]);
|
|
}
|
|
|
|
# Change channel and user modes (this one is easy... the handler is a bitch.)
|
|
# Takes at least 1 arg: the target of the command (channel or nick)
|
|
# (optional) the mode string (i.e., "-boo+i")
|
|
# (optional) operands of the mode string (nicks, hostmasks, etc.)
|
|
sub mode {
|
|
my $self = shift;
|
|
|
|
unless (@_ >= 1) { croak "Not enough arguments to mode()"; }
|
|
$self->sl("MODE $_[0] " . CORE::join(" ", @_[1 .. $#_]));
|
|
}
|
|
|
|
# Sends a MOTD command to a server.
|
|
# Takes 1 optional arg: the server to query (defaults to current server)
|
|
sub motd {
|
|
my $self = shift;
|
|
|
|
$self->sl("MOTD" . ($_[0] ? " $_[0]" : ""));
|
|
}
|
|
|
|
# Requests the list of users for a particular channel (or the entire net, if
|
|
# you're a masochist).
|
|
# Takes 1 or more optional args: name(s) of channel(s) to list the users from.
|
|
sub names {
|
|
my $self = shift;
|
|
|
|
$self->sl("NAMES " . CORE::join(",", @_));
|
|
|
|
} # Was this the easiest sub in the world, or what?
|
|
|
|
# Creates and returns a DCC CHAT object, analogous to IRC.pm's newconn().
|
|
# Takes at least 1 arg: An Event object for the DCC CHAT request.
|
|
# OR A list or listref of args to be passed to new(),
|
|
# consisting of:
|
|
# - A boolean value indicating whether or not
|
|
# you're initiating the CHAT connection.
|
|
# - The nick of the chattee
|
|
# - The address to connect to
|
|
# - The port to connect on
|
|
sub new_chat {
|
|
my $self = shift;
|
|
my ($init, $nick, $address, $port);
|
|
|
|
if (ref($_[0]) =~ /Event/) {
|
|
# If it's from an Event object, we can't be initiating, right?
|
|
($init, undef, undef, undef, $address, $port) = (0, $_[0]->args);
|
|
$nick = $_[0]->nick;
|
|
|
|
} elsif (ref($_[0]) eq "ARRAY") {
|
|
($init, $nick, $address, $port) = @{$_[0]};
|
|
} else {
|
|
($init, $nick, $address, $port) = @_;
|
|
}
|
|
|
|
PBot::Core::IRC::DCC::CHAT->new($self, $init, $nick, $address, $port); # pragma_ 2011/21/01
|
|
}
|
|
|
|
# Creates and returns a DCC GET object, analogous to IRC.pm's newconn().
|
|
# Takes at least 1 arg: An Event object for the DCC SEND request.
|
|
# OR A list or listref of args to be passed to new(),
|
|
# consisting of:
|
|
# - The nick of the file's sender
|
|
# - The name of the file to receive
|
|
# - The address to connect to
|
|
# - The port to connect on
|
|
# - The size of the incoming file
|
|
# For all of the above, an extra argument should be added at the end:
|
|
# An open filehandle to save the incoming file into,
|
|
# in globref, FileHandle, or IO::* form.
|
|
# If you wish to do a DCC RESUME, specify the offset in bytes that you
|
|
# want to start downloading from as the last argument.
|
|
sub new_get {
|
|
my $self = shift;
|
|
my ($nick, $name, $address, $port, $size, $offset, $handle);
|
|
|
|
if (ref($_[0]) =~ /Event/) {
|
|
(undef, undef, $name, $address, $port, $size) = $_[0]->args;
|
|
$nick = $_[0]->nick;
|
|
$handle = $_[1] if defined $_[1];
|
|
} elsif (ref($_[0]) eq "ARRAY") {
|
|
($nick, $name, $address, $port, $size) = @{$_[0]};
|
|
$handle = $_[1] if defined $_[1];
|
|
} else {
|
|
($nick, $name, $address, $port, $size, $handle) = @_;
|
|
}
|
|
|
|
unless (defined $handle and ref $handle and (ref $handle eq "GLOB" or $handle->can('print'))) {
|
|
carp("Filehandle argument to Connection->new_get() must be " . "a glob reference or object");
|
|
return; # is this behavior OK?
|
|
}
|
|
|
|
my $dcc = PBot::Core::IRC::DCC::GET->new(
|
|
$self, $nick, $address, $port, $size, # pragma_ 2011/21/01
|
|
$name, $handle, $offset
|
|
);
|
|
|
|
$self->parent->addconn($dcc) if $dcc;
|
|
return $dcc;
|
|
}
|
|
|
|
# Creates and returns a DCC SEND object, analogous to IRC.pm's newconn().
|
|
# Takes at least 2 args: The nickname of the person to send to
|
|
# The name of the file to send
|
|
# (optional) The blocksize for the connection (default 1k)
|
|
sub new_send {
|
|
my $self = shift;
|
|
my ($nick, $filename, $blocksize);
|
|
|
|
if (ref($_[0]) eq "ARRAY") { ($nick, $filename, $blocksize) = @{$_[0]}; }
|
|
else { ($nick, $filename, $blocksize) = @_; }
|
|
|
|
PBot::Core::IRC::DCC::SEND->new($self, $nick, $filename, $blocksize); # pragma_ 2011/21/01
|
|
}
|
|
|
|
# Selects nick for this object or returns currently set nick.
|
|
# No default; must be set by user.
|
|
# If changed while the object is already connected to a server, it will
|
|
# automatically try to change nicks.
|
|
# Takes 1 arg: the nick. (I bet you could have figured that out...)
|
|
sub nick {
|
|
my $self = shift;
|
|
|
|
if (@_) {
|
|
$self->{'_nick'} = shift;
|
|
if ($self->connected) { return $self->sl("NICK " . $self->{'_nick'}); }
|
|
} else {
|
|
return $self->{'_nick'};
|
|
}
|
|
}
|
|
|
|
# Sends a notice to a channel or person.
|
|
# Takes 2 args: the target of the message (channel or nick)
|
|
# the text of the message to send
|
|
# The message will be chunked if it is longer than the _maxlinelen
|
|
# attribute, but it doesn't try to protect against flooding. If you
|
|
# give it too much info, the IRC server will kick you off!
|
|
sub notice {
|
|
my ($self, $to) = splice @_, 0, 2;
|
|
|
|
unless (@_) { croak "Not enough arguments to notice()"; }
|
|
|
|
my ($buf, $length, $line) = (CORE::join("", @_), $self->{_maxlinelen});
|
|
|
|
while (length($buf) > 0) {
|
|
($line, $buf) = unpack("a$length a*", $buf);
|
|
$self->sl("NOTICE $to :$line");
|
|
}
|
|
}
|
|
|
|
# Makes you an IRCop, if you supply the right username and password.
|
|
# Takes 2 args: Operator's username
|
|
# Operator's password
|
|
sub oper {
|
|
my $self = shift;
|
|
|
|
unless (@_ > 1) { croak "Not enough arguments to oper()"; }
|
|
|
|
$self->sl("OPER $_[0] $_[1]");
|
|
}
|
|
|
|
# This function splits apart a raw server line into its component parts
|
|
# (message, target, message type, CTCP data, etc...) and passes it to the
|
|
# appropriate handler. Takes no args, really.
|
|
sub parse {
|
|
my ($self) = shift;
|
|
my ($from, $type, $message, @stuff, $itype, $ev, @lines, $line, $tags);
|
|
|
|
my $n;
|
|
|
|
if ($self->tls) {
|
|
$n = sysread($self->socket, $line, 32767);
|
|
|
|
if (not defined $n) {
|
|
if ($!{EWOULDBLOCK}) {
|
|
if ($SSL_ERROR == SSL_WANT_READ or $SSL_ERROR == SSL_WANT_WRITE) {
|
|
return;
|
|
}
|
|
|
|
print STDERR "TLS broke: $SSL_ERROR\n";
|
|
}
|
|
}
|
|
|
|
} else {
|
|
$n = $self->socket->recv($line, 32767, 0);
|
|
}
|
|
|
|
if (defined $n and (length($self->{_frag}) + length($line)) > 0) {
|
|
# grab any remnant from the last go and split into lines
|
|
my $chunk = $self->{_frag} . $line;
|
|
@lines = split /\012/, $chunk;
|
|
|
|
# if the last line was incomplete, pop it off the chunk and
|
|
# stick it back into the frag holder.
|
|
$self->{_frag} = (substr($chunk, -1) ne "\012" ? pop @lines : '');
|
|
|
|
} else {
|
|
# um, if we can read, i say we should read more than 0
|
|
# besides, recv isn't returning undef on closed
|
|
# sockets. getting rid of this connection...
|
|
$self->disconnect('error', 'Connection reset by peer');
|
|
return;
|
|
}
|
|
|
|
PARSELOOP: foreach $line (@lines) {
|
|
if ($self->{_utf8}) { utf8::decode($line); }
|
|
|
|
# Clean the lint filter every 2 weeks...
|
|
$line =~ s/[\012\015]+$//;
|
|
next unless $line;
|
|
|
|
print STDERR "<<< $line\n" if $self->{_debug};
|
|
|
|
# Like the RFC says: "respond as quickly as possible..."
|
|
if ($line =~ /^PING/) {
|
|
$ev = (
|
|
PBot::Core::IRC::Event->new(
|
|
"ping", # pragma_ 2011/21/01
|
|
$self->server,
|
|
$self->nick,
|
|
"serverping", # FIXME?
|
|
substr($line, 5)
|
|
)
|
|
);
|
|
|
|
# Had to move this up front to avoid a particularly pernicious bug.
|
|
} elsif ($line =~ /^NOTICE/) {
|
|
$ev = PBot::Core::IRC::Event->new(
|
|
"snotice", # pragma_ 2011/21/01
|
|
$self->server,
|
|
'',
|
|
'server',
|
|
(split /:/, $line, 2)[1]
|
|
);
|
|
|
|
} elsif ($line =~ /^AUTHENTICATE \+$/) { # IRCv3 SASL pragma- June 11, 2021
|
|
$ev = PBot::Core::IRC::Event->new(
|
|
'authenticate',
|
|
$self->server,
|
|
$self->nick,
|
|
'server',
|
|
'+'
|
|
);
|
|
|
|
|
|
# Spurious backslashes are for the benefit of cperl-mode.
|
|
# Assumption: all non-numeric message types begin with a letter
|
|
} elsif (
|
|
$line =~ /^
|
|
(?:\@\S+\s)? # Optional message tags
|
|
:? # Initial colon
|
|
(?:[][}{\w\\\`^|\-]+? # The nick (valid nickname chars)
|
|
! # The nick-username separator
|
|
.+? # The username
|
|
\@)? # Umm, duh...
|
|
\S+ # The hostname
|
|
\s+ # Space between mask and message type
|
|
[A-Za-z] # First char of message type
|
|
[^\s:]+? # The rest of the message type
|
|
/x
|
|
) # That ought to do it for now...
|
|
{
|
|
$tags = undef;
|
|
$tags = $1 if $line =~ s/^@(\S+)\s//;
|
|
$line = substr $line, 1 if $line =~ /^:/;
|
|
|
|
# Patch submitted for v.0.72
|
|
# Fixes problems with IPv6 hostnames.
|
|
# ($from, $line) = split ":", $line, 2;
|
|
($from, $line) = $line =~ /^(?:|)(\S+\s+[^:]+):?(.*)/;
|
|
|
|
print STDERR "from: [$from], line: [$line]\n" if $self->{_debug} > 2;
|
|
|
|
($from, $type, @stuff) = split /\s+/, $from;
|
|
$type = lc $type;
|
|
|
|
# fix splitting of IPv6 hostnames in modes -- pragma- 2013/07/30
|
|
if ($type eq "mode" and $#stuff > -1 and length $line) {
|
|
my @other_stuff = split /\s+/, $line;
|
|
$stuff[$#stuff] .= ':' . shift @other_stuff;
|
|
push @stuff, @other_stuff;
|
|
$line = "";
|
|
}
|
|
|
|
# This should be fairly intuitive... (cperl-mode sucks, though)
|
|
|
|
if (defined $line and index($line, "\001") >= 0) {
|
|
$itype = "ctcp";
|
|
unless ($type eq "notice") { $type = (($stuff[0] =~ tr/\#\&//) ? "public" : "msg"); }
|
|
} elsif ($type eq "privmsg") {
|
|
$itype = $type = (($stuff[0] =~ tr/\#\&//) ? "public" : "msg");
|
|
} elsif ($type eq "notice") {
|
|
$itype = "notice";
|
|
} elsif ($type eq "join" or $type eq "part" or $type eq "mode" or $type eq "topic" or $type eq "kick") {
|
|
$itype = "channel";
|
|
} elsif ($type eq "nick") {
|
|
$itype = "nick";
|
|
} else {
|
|
$itype = "other";
|
|
}
|
|
|
|
# This goes through the list of ignored addresses for this message
|
|
# type and drops out of the sub if it's from an ignored hostmask.
|
|
|
|
study $from;
|
|
foreach ($self->ignore($itype), $self->ignore("all")) {
|
|
$_ = quotemeta; s/\\\*/.*/g;
|
|
next PARSELOOP if $from =~ /$_/i;
|
|
}
|
|
|
|
# It used to look a lot worse. Here was the original version...
|
|
# the optimization above was proposed by Silmaril, for which I am
|
|
# eternally grateful. (Mine still looks cooler, though. :)
|
|
|
|
# return if grep { $_ = join('.*', split(/\\\*/,
|
|
# quotemeta($_))); /$from/ }
|
|
# ($self->ignore($type), $self->ignore("all"));
|
|
|
|
# Add $line and $tags to @stuff for the handlers
|
|
push @stuff, $line if defined $line;
|
|
push @stuff, $tags if defined $tags;
|
|
|
|
# Now ship it off to the appropriate handler and forget about it.
|
|
if ($itype eq "ctcp") { # it's got CTCP in it!
|
|
$self->parse_ctcp($type, $from, $stuff[0], $line);
|
|
next;
|
|
} elsif ($type eq "public"
|
|
or $type eq "msg"
|
|
or $type eq "notice"
|
|
or $type eq "mode"
|
|
or $type eq "join"
|
|
or $type eq "part"
|
|
or $type eq "topic"
|
|
or $type eq "invite"
|
|
or $type eq "whoisaccount"
|
|
or $type eq "cap") # IRCv3 client capabilities pragma-
|
|
{
|
|
$ev = PBot::Core::IRC::Event->new(
|
|
$type, # pragma_ 2011/21/01
|
|
$from,
|
|
shift(@stuff),
|
|
$type,
|
|
@stuff,
|
|
);
|
|
} elsif ($type eq "quit"
|
|
or $type eq "nick"
|
|
or $type eq "account"
|
|
or $type eq "chghost") {
|
|
$ev = PBot::Core::IRC::Event->new(
|
|
$type, # pragma_ 2011/21/01
|
|
$from,
|
|
$from,
|
|
$type,
|
|
@stuff,
|
|
);
|
|
} elsif ($type eq "kick") {
|
|
$ev = PBot::Core::IRC::Event->new(
|
|
$type, # pragma_ 2011/21/01
|
|
$from,
|
|
$stuff[1],
|
|
$type,
|
|
@stuff[0, 2 .. $#stuff],
|
|
);
|
|
} elsif ($type eq "kill"
|
|
or $type eq "wallops"
|
|
or $type eq "pong")
|
|
{
|
|
$ev = PBot::Core::IRC::Event->new(
|
|
$type, # pragma_ 2011/21/01
|
|
$from,
|
|
'',
|
|
$type,
|
|
$line
|
|
); # Ahh, what the hell.
|
|
} else {
|
|
carp "Unknown event type: $type";
|
|
}
|
|
} elsif (
|
|
$line =~ /^:? # Here's Ye Olde Numeric Handler!
|
|
\S+? # the servername (can't assume RFC hostname)
|
|
\s+? # Some spaces here...
|
|
\d+? # The actual number
|
|
\b/x # Some other crap, whatever...
|
|
)
|
|
{
|
|
$ev = $self->parse_num($line);
|
|
|
|
} elsif ($line =~ /^:(\w+) MODE \1 /) {
|
|
$ev = PBot::Core::IRC::Event->new(
|
|
'umode', # pragma_ 2011/21/01
|
|
$self->server,
|
|
$self->nick,
|
|
'server',
|
|
substr($line, index($line, ':', 1) + 1)
|
|
);
|
|
|
|
} elsif (
|
|
$line =~ /^:? # Here's Ye Olde Server Notice handler!
|
|
.+? # the servername (can't assume RFC hostname)
|
|
\s+? # Some spaces here...
|
|
NOTICE # The server notice
|
|
\b/x # Some other crap, whatever...
|
|
)
|
|
{
|
|
$ev = PBot::Core::IRC::Event->new(
|
|
'snotice', # pragma_ 2011/21/01
|
|
$self->server,
|
|
'',
|
|
'server',
|
|
(split /\s+/, $line, 3)[2]
|
|
);
|
|
|
|
} elsif ($line =~ /^ERROR/) {
|
|
if ($line =~ /^ERROR :Closing [Ll]ink/) { # is this compatible?
|
|
$ev = 'done';
|
|
$self->disconnect('error', ($line =~ /(.*)/));
|
|
|
|
} else {
|
|
$ev = PBot::Core::IRC::Event->new(
|
|
"error", # pragma_ 2011/21/01
|
|
$self->server,
|
|
'',
|
|
'error',
|
|
(split /:/, $line, 2)[1]
|
|
);
|
|
}
|
|
} elsif ($line =~ /^Closing [Ll]ink/) {
|
|
$ev = 'done';
|
|
$self->disconnect('error', ($line =~ /(.*)/));
|
|
|
|
}
|
|
|
|
if ($ev) {
|
|
# We need to be able to fall through if the handler has
|
|
# already been called (i.e., from within disconnect()).
|
|
|
|
$self->handler($ev) unless $ev eq 'done';
|
|
|
|
} else {
|
|
# If it gets down to here, it's some exception I forgot about.
|
|
carp "Funky parse case: $line\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
# The backend that parse() sends CTCP requests off to. Pay no attention
|
|
# to the camel behind the curtain.
|
|
# Takes 4 arguments: the type of message
|
|
# who it's from
|
|
# the first bit of stuff
|
|
# the line from the server.
|
|
sub parse_ctcp {
|
|
my ($self, $type, $from, $stuff, $line) = @_;
|
|
|
|
my ($one, $two);
|
|
my ($odd, @foo) = (&dequote($line));
|
|
|
|
while (($one, $two) = (splice @foo, 0, 2)) {
|
|
|
|
($one, $two) = ($two, $one) if $odd;
|
|
|
|
my ($ctype) = $one =~ /^(\w+)\b/;
|
|
my $prefix = undef;
|
|
if ($type eq 'notice') { $prefix = 'cr'; }
|
|
elsif ($type eq 'public' or $type eq 'msg') { $prefix = 'c'; }
|
|
else {
|
|
carp "Unknown CTCP type: $type";
|
|
return;
|
|
}
|
|
|
|
if ($prefix) {
|
|
my $handler = $prefix . lc $ctype; # unit. value prob with $ctype
|
|
|
|
$one =~ s/^$ctype //i; # strip the CTCP type off the args
|
|
$self->handler(
|
|
PBot::Core::IRC::Event->new(
|
|
$handler, $from, $stuff, # pragma_ 2011/21/01
|
|
$handler, $one
|
|
)
|
|
);
|
|
}
|
|
|
|
$self->handler(PBot::Core::IRC::Event->new($type, $from, $stuff, $type, $two)) # pragma_ 2011/21/01
|
|
if $two;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
# Does special-case parsing for numeric events. Separate from the rest of
|
|
# parse() for clarity reasons (I can hear Tkil gasping in shock now. :-).
|
|
# Takes 1 arg: the raw server line
|
|
sub parse_num {
|
|
my ($self, $line) = @_;
|
|
|
|
# Figlet protection? This seems to be a bit closer to the RFC than
|
|
# the original version, which doesn't seem to handle :trailers quite
|
|
# correctly.
|
|
|
|
my ($from, $type, $stuff) = split(/\s+/, $line, 3);
|
|
my ($blip, $space, $other, @stuff);
|
|
while ($stuff) {
|
|
($blip, $space, $other) = split(/(\s+)/, $stuff, 2);
|
|
$space = "" unless $space;
|
|
$other = "" unless $other; # Thanks to jack velte...
|
|
if ($blip =~ /^:/) {
|
|
push @stuff, $blip . $space . $other;
|
|
last;
|
|
} else {
|
|
push @stuff, $blip;
|
|
$stuff = $other;
|
|
}
|
|
}
|
|
|
|
$from = substr $from, 1 if $from =~ /^:/;
|
|
|
|
return PBot::Core::IRC::Event->new(
|
|
$type, # pragma_ 2011/21/01
|
|
$from,
|
|
'',
|
|
'server',
|
|
@stuff
|
|
);
|
|
}
|
|
|
|
# Helps you flee those hard-to-stand channels.
|
|
# Takes at least one arg: name(s) of channel(s) to leave.
|
|
sub part {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak "No arguments provided to part()"; }
|
|
$self->sl("PART " . CORE::join(",", @_)); # "A must!"
|
|
}
|
|
|
|
# Tells what's on the other end of a connection. Returns a 2-element list
|
|
# consisting of the name on the other end and the type of connection.
|
|
# Takes no args.
|
|
sub peer {
|
|
my $self = shift;
|
|
|
|
return ($self->server(), "IRC connection");
|
|
}
|
|
|
|
# Prints a message to the defined error filehandle(s).
|
|
# No further description should be necessary.
|
|
sub printerr {
|
|
shift;
|
|
print STDERR @_, "\n";
|
|
}
|
|
|
|
# Prints a message to the defined output filehandle(s).
|
|
sub print {
|
|
shift;
|
|
print STDOUT @_, "\n";
|
|
}
|
|
|
|
# Sends a message to a channel or person.
|
|
# Takes 2 args: the target of the message (channel or nick)
|
|
# the text of the message to send
|
|
# Don't use this for sending CTCPs... that's what the ctcp() function is for.
|
|
# The message will be chunked if it is longer than the _maxlinelen
|
|
# attribute, but it doesn't try to protect against flooding. If you
|
|
# give it too much info, the IRC server will kick you off!
|
|
sub privmsg {
|
|
my ($self, $to) = splice @_, 0, 2;
|
|
|
|
unless (@_) { croak 'Not enough arguments to privmsg()'; }
|
|
|
|
my $buf = CORE::join '', @_;
|
|
my $length = $self->{_maxlinelen} - 11 - length($to);
|
|
|
|
print STDERR "privmsg trunc length: $length; msg len: " . (length $buf) . "\n" if $self->{_debug};
|
|
|
|
my $line;
|
|
|
|
if (ref($to) =~ /^(GLOB|IO::Socket)/) {
|
|
while (length($buf) > 0) {
|
|
($line, $buf) = unpack("a$length a*", $buf);
|
|
send($to, $line . "\012", 0);
|
|
}
|
|
} else {
|
|
while (length($buf) > 0) {
|
|
($line, $buf) = unpack("a$length a*", $buf);
|
|
if (ref $to eq 'ARRAY') {
|
|
$self->sl("PRIVMSG ", CORE::join(',', @$to), " :$line");
|
|
} else {
|
|
$self->sl("PRIVMSG $to :$line");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Closes connection to IRC server. (Corresponding function for /QUIT)
|
|
# Takes 1 optional arg: parting message, defaults to "Leaving" by custom.
|
|
sub quit {
|
|
my $self = shift;
|
|
|
|
# Do any user-defined stuff before leaving
|
|
$self->handler("leaving");
|
|
|
|
unless ($self->connected) { return (1) }
|
|
|
|
# Why bother checking for sl() errors now, after all? :)
|
|
# We just send the QUIT command and leave. The server will respond with
|
|
# a "Closing link" message, and parse() will catch it, close the
|
|
# connection, and throw a "disconnect" event. Neat, huh? :-)
|
|
|
|
$self->sl("QUIT :" . (defined $_[0] ? $_[0] : "Leaving"));
|
|
|
|
# since the quit sends a line to the server, we need to flush the
|
|
# output queue to make sure it gets there so the disconnect
|
|
$self->parent->flush_output_queue();
|
|
|
|
return 1;
|
|
}
|
|
|
|
# As per the RFC, ask the server to "re-read and process its configuration
|
|
# file." Your server may or may not take additional arguments. Generally
|
|
# requires IRCop status.
|
|
sub rehash {
|
|
my $self = shift;
|
|
$self->sl("REHASH" . CORE::join(" ", @_));
|
|
}
|
|
|
|
# As per the RFC, "force a server restart itself." (Love that RFC.)
|
|
# Takes no arguments. If it succeeds, you will likely be disconnected,
|
|
# but I assume you already knew that. This sub is too simple...
|
|
sub restart {
|
|
my $self = shift;
|
|
$self->sl("RESTART");
|
|
}
|
|
|
|
# Schedules an event to be executed after some length of time.
|
|
# Takes at least 2 args: the number of seconds to wait until it's executed
|
|
# a coderef to execute when time's up
|
|
# Any extra args are passed as arguments to the user's coderef.
|
|
sub schedule {
|
|
my $self = shift;
|
|
my $time = shift;
|
|
my $coderef = shift;
|
|
|
|
unless ($coderef) { croak 'Not enough arguments to Connection->schedule()'; }
|
|
unless (ref($coderef) eq 'CODE') { croak 'Second argument to schedule() isn\'t a coderef'; }
|
|
|
|
print STDERR "Scheduling event with time [$time]\n" if $self->{_debug} > 1;
|
|
$time += time;
|
|
$self->parent->enqueue_scheduled_event($time, $coderef, $self, @_);
|
|
}
|
|
|
|
sub schedule_output_event {
|
|
my $self = shift;
|
|
my $time = shift;
|
|
my $coderef = shift;
|
|
|
|
unless ($coderef) { croak 'Not enough arguments to Connection->schedule()'; }
|
|
unless (ref($coderef) eq 'CODE') { croak 'Second argument to schedule() isn\'t a coderef'; }
|
|
|
|
print STDERR "Scheduling output event with time [$time] [$_[0]]\n" if $self->{_debug} > 1;
|
|
$time += time;
|
|
$self->parent->enqueue_output_event($time, $coderef, $self, @_);
|
|
}
|
|
|
|
# Lets J. Random IRCop connect one IRC server to another. How uninteresting.
|
|
# Takes at least 1 arg: the name of the server to connect your server with
|
|
# (optional) the port to connect them on (default 6667)
|
|
# (optional) the server to connect to arg #1. Used mainly by
|
|
# servers to communicate with each other.
|
|
sub sconnect {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak "Not enough arguments to sconnect()"; }
|
|
$self->sl("CONNECT " . CORE::join(" ", @_));
|
|
}
|
|
|
|
# Sets/changes the IRC server which this instance should connect to.
|
|
# Takes 1 arg: the name of the server (see below for possible syntaxes)
|
|
# ((syntaxen? syntaxi? syntaces?))
|
|
sub server {
|
|
my ($self) = shift;
|
|
|
|
if (@_) {
|
|
# cases like "irc.server.com:6668"
|
|
if (index($_[0], ':') > 0) {
|
|
my ($serv, $port) = split /:/, $_[0];
|
|
if ($port =~ /\D/) {
|
|
carp "$port is not a valid port number in server()";
|
|
return;
|
|
}
|
|
$self->{_server} = $serv;
|
|
$self->port($port);
|
|
|
|
# cases like ":6668" (buried treasure!)
|
|
} elsif (index($_[0], ':') == 0 and $_[0] =~ /^:(\d+)/) {
|
|
$self->port($1);
|
|
|
|
# cases like "irc.server.com"
|
|
} else {
|
|
$self->{_server} = shift;
|
|
}
|
|
return (1);
|
|
|
|
} else {
|
|
return $self->{_server};
|
|
}
|
|
}
|
|
|
|
# sends a raw IRC line to the server, possibly with pacing
|
|
sub sl {
|
|
my $self = shift;
|
|
my $line = CORE::join '', @_;
|
|
|
|
unless (@_) { croak "Not enough arguments to sl()"; }
|
|
|
|
if (!$self->pacing) {
|
|
return $self->sl_real($line);
|
|
}
|
|
|
|
if ($self->{_slcount} < 10) {
|
|
$self->{_slcount}++;
|
|
$self->{_lastsl} = time;
|
|
|
|
### DEBUG DEBUG DEBUG
|
|
if ($self->{_debug} > 1) { print STDERR "S-> 0 " . (length ($line) + 2) . " $line\n"; }
|
|
|
|
return $self->schedule_output_event(0, \&sl_real, $line);
|
|
}
|
|
|
|
# calculate how long to wait before sending this line
|
|
my $time = time;
|
|
if ($time - $self->{_lastsl} > $self->pacing) {
|
|
$self->{_lastsl} = $time;
|
|
} else {
|
|
$self->{_lastsl} += $self->pacing;
|
|
}
|
|
|
|
my $seconds = $self->{_lastsl} - $time;
|
|
|
|
if ($seconds == 0) {
|
|
$self->{_slcount} = 0;
|
|
}
|
|
|
|
### DEBUG DEBUG DEBUG
|
|
if ($self->{_debug} > 1) { print STDERR "S-> $seconds " . (length ($line) + 2) . " $line\n"; }
|
|
|
|
$self->schedule_output_event($seconds, \&sl_real, $line);
|
|
}
|
|
|
|
# Sends a raw IRC line to the server.
|
|
# Corresponds to the internal sirc function of the same name.
|
|
# Takes 1 arg: string to send to server. (duh. :)
|
|
sub sl_real {
|
|
my $self = shift;
|
|
my $line = shift;
|
|
|
|
unless ($line) { croak "Not enough arguments to sl_real()"; }
|
|
|
|
### DEBUG DEBUG DEBUG
|
|
if ($self->{_debug}) { print STDERR ">>> (" . (length ($line) + 2) . ") $line\n"; }
|
|
|
|
return unless defined $self->socket;
|
|
|
|
if ($self->{_utf8}) { $line = encode('UTF-8', $line); }
|
|
|
|
my $rv = eval {
|
|
# RFC compliance can be kinda nice...
|
|
my $rv = $self->tls ? $self->socket->print("$line\015\012") : $self->socket->send("$line\015\012", 0);
|
|
unless ($rv) {
|
|
$self->handler("sockerror");
|
|
return;
|
|
}
|
|
return $rv;
|
|
};
|
|
|
|
if ($@) { print "Attempt to send bad line: [$line]\n"; }
|
|
return $rv;
|
|
}
|
|
|
|
# Tells any server that you're an oper on to disconnect from the IRC network.
|
|
# Takes at least 1 arg: the name of the server to disconnect
|
|
# (optional) a comment about why it was disconnected
|
|
sub squit {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak "Not enough arguments to squit()"; }
|
|
|
|
$self->sl("SQUIT $_[0]" . ($_[1] ? " :$_[1]" : ""));
|
|
}
|
|
|
|
# Gets various server statistics for the specified host.
|
|
# Takes at least 2 arg: the type of stats to request [chiklmouy]
|
|
# (optional) the server to request from (default is current server)
|
|
sub stats {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak "Not enough arguments passed to stats()"; }
|
|
|
|
$self->sl("STATS $_[0]" . ($_[1] ? " $_[1]" : ""));
|
|
}
|
|
|
|
# If anyone still has SUMMON enabled, this will implement it for you.
|
|
# If not, well...heh. Sorry. First arg mandatory: user to summon.
|
|
# Second arg optional: a server name.
|
|
sub summon {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak "Not enough arguments passed to summon()"; }
|
|
|
|
$self->sl("SUMMON $_[0]" . ($_[1] ? " $_[1]" : ""));
|
|
}
|
|
|
|
# Requests timestamp from specified server. Easy enough, right?
|
|
# Takes 1 optional arg: a server name/mask to query
|
|
# renamed to not collide with things... -- aburke
|
|
sub timestamp {
|
|
my ($self, $serv) = (shift, undef);
|
|
|
|
$self->sl("TIME" . ($_[0] ? " $_[0]" : ""));
|
|
}
|
|
|
|
# Sends request for current topic, or changes it to something else lame.
|
|
# Takes at least 1 arg: the channel whose topic you want to screw around with
|
|
# (optional) the new topic you want to impress everyone with
|
|
sub topic {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak "Not enough arguments to topic()"; }
|
|
|
|
# Can you tell I've been reading the Nethack source too much? :)
|
|
$self->sl("TOPIC $_[0]" . ($_[1] ? " :$_[1]" : ""));
|
|
}
|
|
|
|
# Sends a trace request to the server. Whoop.
|
|
# Take 1 optional arg: the server or nickname to trace.
|
|
sub trace {
|
|
my $self = shift;
|
|
|
|
$self->sl("TRACE" . ($_[0] ? " $_[0]" : ""));
|
|
}
|
|
|
|
# This method submitted by Dave Schmitt <dschmi1@umbc.edu>. Thanks, Dave!
|
|
sub unignore {
|
|
my $self = shift;
|
|
|
|
croak "Not enough arguments to unignore()" unless @_;
|
|
|
|
if (@_ == 1) {
|
|
if (exists $self->{_ignore}->{$_[0]}) { return @{$self->{_ignore}->{$_[0]}}; }
|
|
else { return (); }
|
|
} elsif (@_ > 1) { # code defensively, remember...
|
|
my $type = shift;
|
|
|
|
# I moved this part further down as an Obsessive Efficiency
|
|
# Initiative. It shouldn't be a problem if I do _parse right...
|
|
# ... but those are famous last words, eh?
|
|
unless (grep { $_ eq $type } qw(public msg ctcp notice channel nick other all)) {
|
|
carp "$type isn't a valid type to unignore()";
|
|
return;
|
|
}
|
|
|
|
if (exists $self->{_ignore}->{$type}) {
|
|
# removes all specifed entries ala _Perl_Cookbook_ recipe 4.7
|
|
my @temp = @{$self->{_ignore}->{$type}};
|
|
@{$self->{_ignore}->{$type}} = ();
|
|
my %seen = ();
|
|
foreach my $item (@_) { $seen{$item} = 1 }
|
|
foreach my $item (@temp) { push(@{$self->{_ignore}->{$type}}, $item) unless ($seen{$item}); }
|
|
} else {
|
|
carp "no ignore entry for $type to remove";
|
|
}
|
|
}
|
|
}
|
|
|
|
# Requests userhost info from the server.
|
|
# Takes at least 1 arg: nickname(s) to look up.
|
|
sub userhost {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak 'Not enough args to userhost().'; }
|
|
|
|
$self->sl("USERHOST " . CORE::join(" ", @_));
|
|
}
|
|
|
|
# Sends a users request to the server, which may or may not listen to you.
|
|
# Take 1 optional arg: the server to query.
|
|
sub users {
|
|
my $self = shift;
|
|
|
|
$self->sl("USERS" . ($_[0] ? " $_[0]" : ""));
|
|
}
|
|
|
|
# Asks the IRC server what version and revision of ircd it's running. Whoop.
|
|
# Takes 1 optional arg: the server name/glob. (default is current server)
|
|
sub version {
|
|
my $self = shift;
|
|
|
|
$self->sl("VERSION" . ($_[0] ? " $_[0]" : ""));
|
|
}
|
|
|
|
# Sends a message to all opers on the network. Hypothetically.
|
|
# Takes 1 arg: the text to send.
|
|
sub wallops {
|
|
my $self = shift;
|
|
|
|
unless ($_[0]) { croak 'No arguments passed to wallops()'; }
|
|
|
|
$self->sl("WALLOPS :" . CORE::join("", @_));
|
|
}
|
|
|
|
# Asks the server about stuff, you know. Whatever. Pass the Fritos, dude.
|
|
# Takes 2 optional args: the bit of stuff to ask about
|
|
# an "o" (nobody ever uses this...)
|
|
sub who {
|
|
my $self = shift;
|
|
|
|
# Obfuscation!
|
|
$self->sl("WHO" . (@_ ? " @_" : ""));
|
|
}
|
|
|
|
# If you've gotten this far, you probably already know what this does.
|
|
# Takes at least 1 arg: nickmasks or channels to /whois
|
|
sub whois {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak "Not enough arguments to whois()"; }
|
|
return $self->sl("WHOIS " . CORE::join(",", @_));
|
|
}
|
|
|
|
# Same as above, in the past tense.
|
|
# Takes at least 1 arg: nick to do the /whowas on
|
|
# (optional) max number of hits to display
|
|
# (optional) server or servermask to query
|
|
sub whowas {
|
|
my $self = shift;
|
|
|
|
unless (@_) { croak "Not enough arguments to whowas()"; }
|
|
return $self->sl("WHOWAS $_[0]" . ($_[1] ? " $_[1]" : "") . (($_[1] && $_[2]) ? " $_[2]" : ""));
|
|
}
|
|
|
|
# This sub executes the default action for an event with no user-defined
|
|
# handlers. It's all in one sub so that we don't have to make a bunch of
|
|
# separate anonymous subs stuffed in a hash.
|
|
sub _default {
|
|
my ($self, $event) = @_;
|
|
my $verbose = $self->verbose;
|
|
|
|
# Users should only see this if the programmer (me) fucked up.
|
|
unless ($event) { croak "You EEEEEDIOT!!! Not enough args to _default()!"; }
|
|
|
|
# Reply to PING from server as quickly as possible.
|
|
if ($event->type eq "ping") {
|
|
$self->sl("PONG " . (CORE::join ' ', $event->args));
|
|
|
|
} elsif ($event->type eq "disconnect") {
|
|
# I violate OO tenets. (It's consensual, of course.)
|
|
unless (keys %{$self->parent->{_connhash}} > 0) { die "No active connections left, exiting...\n"; }
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
Net::IRC::Connection - Object-oriented interface to a single IRC connection
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
Hard hat area: This section under construction.
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This documentation is a subset of the main Net::IRC documentation. If
|
|
you haven't already, please "perldoc Net::IRC" before continuing.
|
|
|
|
Net::IRC::Connection defines a class whose instances are individual
|
|
connections to a single IRC server. Several Net::IRC::Connection objects may
|
|
be handled simultaneously by one Net::IRC object.
|
|
|
|
=head1 METHOD DESCRIPTIONS
|
|
|
|
This section is under construction, but hopefully will be finally written up
|
|
by the next release. Please see the C<irctest> script and the source for
|
|
details about this module.
|
|
|
|
=head1 AUTHORS
|
|
|
|
Conceived and initially developed by Greg Bacon E<lt>gbacon@adtran.comE<gt> and
|
|
Dennis Taylor E<lt>dennis@funkplanet.comE<gt>.
|
|
|
|
Ideas and large amounts of code donated by Nat "King" Torkington E<lt>gnat@frii.comE<gt>.
|
|
|
|
Currently being hacked on, hacked up, and worked over by the members of the
|
|
Net::IRC developers mailing list. For details, see
|
|
http://www.execpc.com/~corbeau/irc/list.html .
|
|
|
|
=head1 URL
|
|
|
|
Up-to-date source and information about the Net::IRC project can be found at
|
|
http://netirc.betterbox.net/ .
|
|
|
|
=head1 SEE ALSO
|
|
|
|
=over
|
|
|
|
=item *
|
|
|
|
perl(1).
|
|
|
|
=item *
|
|
|
|
RFC 1459: The Internet Relay Chat Protocol
|
|
|
|
=item *
|
|
|
|
http://www.irchelp.org/, home of fine IRC resources.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|