package POE::Component::IRC::Plugin::DCC;
our $AUTHORITY = 'cpan:HINRIK';
$POE::Component::IRC::Plugin::DCC::VERSION = '6.90';
use strict;
use warnings FATAL => 'all';
use Carp;
use File::Basename qw(fileparse);
use File::Glob ':glob';
use File::Spec::Functions 'rel2abs';
use POE qw(Driver::SysRW Filter::Line Filter::Stream
           Wheel::ReadWrite Wheel::SocketFactory);
use POE::Component::IRC::Plugin qw(:ALL);
use Socket qw(INADDR_ANY unpack_sockaddr_in inet_aton inet_ntoa);

use constant {
    OUT_BLOCKSIZE  => 1024,   # Send DCC data in 1k chunks
    IN_BLOCKSIZE   => 10_240, # 10k per DCC socket read
    LISTEN_TIMEOUT => 300,    # Five minutes for listening DCCs
};

sub new {
    my ($package) = shift;
    croak "$package requires an even number of arguments" if @_ & 1;
    my %self = @_;
    return bless \%self, $package;
}

sub PCI_register {
    my ($self, $irc) = @_;

    $self->{irc} = $irc;

    POE::Session->create(
        object_states => [
            $self => [qw(
                _start
                _dcc_read
                _dcc_failed
                _dcc_timeout
                _dcc_up
                _U_dcc
                _U_dcc_accept
                _U_dcc_chat
                _U_dcc_close
                _U_dcc_resume
                _cancel_timeout
            )],
        ],
    );

    $irc->plugin_register($self, 'SERVER', qw(disconnected dcc_request));
    $irc->plugin_register($self, 'USER', qw(dcc dcc_accept dcc_chat dcc_close dcc_resume));

    return 1;
}

sub PCI_unregister {
    my ($self) = @_;
    delete $self->{irc};
    delete $self->{$_} for qw(wheelmap dcc);
    $poe_kernel->refcount_decrement($self->{session_id}, __PACKAGE__);
    return 1;
}

sub _start {
    my ($kernel, $self) = @_[KERNEL, OBJECT];
    $self->{session_id} = $_[SESSION]->ID();
    $kernel->refcount_increment($self->{session_id}, __PACKAGE__);
    return;
}

# set the dcc ports
sub dccports {
    my ($self, $value) = @_;
    $self->{dccports} = $value;
    return;
}

# set the NAT address
sub nataddr {
    my ($self, $value) = @_;
    $self->{nataddr} = $value;
    return;
}

# returns information about a connection
sub dcc_info {
    my ($self, $id) = @_;

    if (!$self->{dcc}->{$id}) {
        warn "dcc_info: Unknown wheel ID: $id\n";
        return;
    }

    my %info;
    @info{qw(nick type port file size done peeraddr)}
        = @{ $self->{dcc}->{$id} }{qw(
            nick type port file size done peeraddr
        )};
    return \%info;
}

sub _quote_file {
    my ($file) = @_;

    if ($file =~ /[\s"]/) {
        $file =~ s|"|\\"|g;
        $file = qq{"$file"};
    }
    return $file;
}

sub S_disconnected {
    my ($self) = $_;
    # clean up old cookies for any ignored RESUME requests
    delete $self->{resuming};
    return PCI_EAT_NONE;
}

sub S_dcc_request {
    my ($self, $irc) = splice @_, 0, 2;
    my ($user, $type, $port, $cookie, $file, $size) = map { ref =~ /REF|SCALAR/ && ${ $_ } } @_;
    my $nick = (split /!/, $user)[0];

    if ($type eq 'ACCEPT' && $self->{resuming}->{"$port+$nick"}) {
        # the old cookie has the peer's address
        my $old_cookie = delete $self->{resuming}->{"$port+$nick"};
        $irc->yield(dcc_accept => $old_cookie);
    }
    elsif ($type eq 'RESUME') {
        for my $cookie (values %{ $self->{dcc} }) {
            next if $cookie->{nick} ne $nick;
            next if $cookie->{port} ne $port;
            $file = _quote_file($file);
            $cookie->{done} = $size;
            $irc->yield(ctcp => $nick => "DCC ACCEPT $file $port $size");
            last;
        }
    }

    return PCI_EAT_NONE;
}

# this is a stub handler for all U_dcc* events which redispatches them as
# events to our own POE session so that we can do stuff related to it,
# namely create wheels and set alarms/delays
sub _default {
    my ($self, $irc, $event) = splice @_, 0, 3;
    return PCI_EAT_NONE if $event !~ /^U_dcc(?:_accept|_chat|_close|_resume)?$/;
    $event =~ s/^U_/_U_/;
    pop @_;
    my @args = map { $$_ } @_;
    $poe_kernel->call($self->{session_id}, $event, @args);
    return PCI_EAT_NONE;
}

# Attempt to initiate a DCC SEND or CHAT connection with another person.
sub _U_dcc {
    my ($kernel, $self, $nick, $type, $file, $blocksize, $timeout)
        = @_[KERNEL, OBJECT, ARG0..$#_];

    if (!defined $type) {
        warn "The 'dcc' command requires at least two arguments\n";
        return;
    }

    my $irc = $self->{irc};
    my ($bindport, $bindaddr, $factory, $port, $addr, $size);

    $type = uc $type;
    if ($type eq 'CHAT') {
        $file = 'chat';   # As per the semi-specification
    }
    elsif ($type eq 'SEND') {
        if (!defined $file) {
            warn "The 'dcc' command requires three arguments for a SEND\n";
            return;
        }
        $file = rel2abs(bsd_glob($file));
        $size = (stat $file)[7];
        if (!defined $size) {
            $irc->send_event(
                'irc_dcc_error',
                undef,
                "Couldn't get ${file}'s size: $!",
                $nick,
                $type,
                undef,
                $file,
            );
            return;
        }
    }

    $bindaddr = $irc->localaddr();

    if ($self->{dccports}) {
        $bindport = shift @{ $self->{dccports} };
        if (!defined $bindport) {
          warn "dcc: Can't allocate listen port for DCC $type\n";
          return;
        }
    }

    $factory = POE::Wheel::SocketFactory->new(
        BindAddress  => $bindaddr || INADDR_ANY,
        BindPort     => $bindport,
        SuccessEvent => '_dcc_up',
        FailureEvent => '_dcc_failed',
        Reuse        => 'yes',
    );

    ($port, $addr) = unpack_sockaddr_in($factory->getsockname());
    $addr = inet_aton($self->{nataddr}) if $self->{nataddr};

    if (!defined $addr) {
        warn "dcc: Can't determine our IP address! ($!)\n";
        return;
    }
    $addr = unpack 'N', $addr;

    my $basename = fileparse($file);
    $basename = _quote_file($basename);

    # Tell the other end that we're waiting for them to connect.
    $irc->yield(ctcp => $nick => "DCC $type $basename $addr $port" . ($size ? " $size" : ''));

    my $alarm_id = $kernel->delay_set(
        '_dcc_timeout', ($timeout || LISTEN_TIMEOUT), $factory->ID,
    );

    # Store the state for this connection.
    $self->{dcc}->{ $factory->ID } = {
        open      => 0,
        nick      => $nick,
        type      => $type,
        file      => $file,
        size      => $size,
        port      => $port,
        addr      => $addr,
        done      => 0,
        blocksize => ($blocksize || OUT_BLOCKSIZE),
        listener  => 1,
        factory   => $factory,
        alarm_id  => $alarm_id,
    };

    return;
}

# Accepts a proposed DCC connection to another client. See '_dcc_up' for
# the rest of the logic for this.
sub _U_dcc_accept {
    my ($self, $cookie, $myfile) = @_[OBJECT, ARG0, ARG1];

    if (!defined $cookie) {
        warn "The 'dcc_accept' command requires at least one argument\n";
        return;
    }

    if ($cookie->{type} eq 'SEND') {
        $cookie->{type} = 'GET';
        $cookie->{file} = $myfile if defined $myfile;   # filename override
    }

    my $factory = POE::Wheel::SocketFactory->new(
        RemoteAddress => $cookie->{addr},
        RemotePort    => $cookie->{port},
        SuccessEvent  => '_dcc_up',
        FailureEvent  => '_dcc_failed',
    );

    $self->{dcc}->{$factory->ID} = $cookie;
    $self->{dcc}->{$factory->ID}->{factory} = $factory;

    return;
}

# Send data over a DCC CHAT connection.
sub _U_dcc_chat {
    my ($self, $id, @data) = @_[OBJECT, ARG0..$#_];

    if (!defined $id || !@data) {
        warn "The 'dcc_chat' command requires at least two arguments\n";
        return;
    }

    if (!exists $self->{dcc}->{$id}) {
        warn "dcc_chat: Unknown wheel ID: $id\n";
        return;
    }

    if (!exists $self->{dcc}->{$id}->{wheel}) {
        warn "dcc_chat: No DCC wheel for id $id!\n";
        return;
    }

    if ($self->{dcc}->{$id}->{type} ne 'CHAT') {
        warn "dcc_chat: id $id isn't associated with a DCC CHAT connection!\n";
        return;
    }

    $self->{dcc}->{$id}->{wheel}->put(join "\n", @data);
    return;
}

# Terminate a DCC connection manually.
sub _U_dcc_close {
    my ($kernel, $self, $id) = @_[KERNEL, OBJECT, ARG0];
    my $irc = $self->{irc};

    if (!defined $id) {
        warn "The 'dcc_close' command requires an id argument\n";
        return;
    }

    if (!exists $self->{dcc}->{$id}) {
        warn "dcc_close: Unknown wheel ID: $id\n";
        return;
    }

    if (!exists $self->{dcc}->{$id}->{wheel}) {
        warn "dcc_close: No DCC wheel for id $id!\n";
        return;
    }

    # pending data, wait till it has been flushed
    if ($self->{dcc}->{$id}->{wheel}->get_driver_out_octets()) {
        $kernel->delay_set(_U_dcc_close => 2, $id);
        return;
    }

    $irc->send_event(
        'irc_dcc_done',
        $id,
        @{ $self->{dcc}->{$id} }{qw(
            nick type port file size done peeraddr
        )},
    );

    # Reclaim our port if necessary.
    if ($self->{dcc}->{$id}->{listener} && $self->{dccports}) {
        push ( @{ $self->{dccports} }, $self->{dcc}->{$id}->{port} );
    }

    $self->_remove_dcc($id);
    return;
}

## no critic (InputOutput::RequireBriefOpen)
sub _U_dcc_resume {
    my ($self, $cookie, $myfile) = @_[OBJECT, ARG0, ARG1];
    my $irc = $self->{irc};

    my $sender_file = _quote_file($cookie->{file});
    $cookie->{file} = $myfile if defined $myfile;
    $cookie->{done} = -s $cookie->{file};
    $cookie->{resuming} = 1;

    if (open(my $handle, '>>', $cookie->{file})) {
        $irc->yield(ctcp => $cookie->{nick} => "DCC RESUME $sender_file $cookie->{port} $cookie->{done}");
        $self->{resuming}->{"$cookie->{port}+$cookie->{nick}"} = $cookie;
    }
    else {
        warn "dcc_resume: Can't append to file '$cookie->{file}'\n";
        return;
    }

    return;
}

# Accept incoming data on a DCC socket.
sub _dcc_read {
    my ($kernel, $self, $data, $id) = @_[KERNEL, OBJECT, ARG0, ARG1];
    my $irc = $self->{irc};

    $id = $self->{wheelmap}->{$id};
    if ($self->{dcc}{$id}{alarm_id}) {
        $kernel->call($self->{session_id}, '_cancel_timeout', $id);
    }

    if ($self->{dcc}->{$id}->{type} eq 'GET') {
        # Acknowledge the received data.
        print {$self->{dcc}->{$id}->{fh}} $data;
        $self->{dcc}->{$id}->{done} += length $data;
        $self->{dcc}->{$id}->{wheel}->put(
            pack 'N', $self->{dcc}->{$id}->{done}
        );

        # Send an event to let people know about the newly arrived data.
        $irc->send_event(
            'irc_dcc_get',
            $id,
            @{ $self->{dcc}->{$id} }{qw(
                nick port file size done peeraddr
            )},
        );
    }
    elsif ($self->{dcc}->{$id}->{type} eq 'SEND') {
        # Record the client's download progress.
        $self->{dcc}->{$id}->{done} = unpack 'N', substr( $data, -4 );

        $irc->send_event(
            'irc_dcc_send',
            $id,
            @{ $self->{dcc}->{$id} }{qw(
                nick port file size done peeraddr
            )},
        );

        # Are we done yet?
        if ($self->{dcc}->{$id}->{done} >= $self->{dcc}->{$id}->{size}) {
            # Reclaim our port if necessary.
            if ( $self->{dcc}->{$id}->{listener} && $self->{dccports}) {
                push @{ $self->{dccports} }, $self->{dcc}->{$id}->{port};
            }

            $irc->send_event(
                'irc_dcc_done',
                $id,
                @{ $self->{dcc}->{$id} }{qw(
                    nick type port file size done peeraddr
                )},
            );

            $self->_remove_dcc($id);
            return;
        }

        # Send the next 'blocksize'-sized packet.
        read $self->{dcc}->{$id}->{fh}, $data,
            $self->{dcc}->{$id}->{blocksize};
        $self->{dcc}->{$id}->{wheel}->put( $data );
    }
    else {
        $irc->send_event(
            'irc_dcc_' . lc $self->{dcc}->{$id}->{type},
            $id,
            @{ $self->{dcc}->{$id} }{qw(nick port)},
            $data,
            $self->{dcc}->{$id}->{peeraddr},
        );
    }

    return;
}

# What happens when an attempted DCC connection fails.
sub _dcc_failed {
    my ($self, $operation, $errnum, $errstr, $id) = @_[OBJECT, ARG0 .. ARG3];
    my $irc = $self->{irc};

    if (!exists $self->{dcc}->{$id}) {
        if (exists $self->{wheelmap}->{$id}) {
            $id = $self->{wheelmap}->{$id};
        }
        else {
            warn "_dcc_failed: Unknown wheel ID: $id\n";
            return;
        }
    }

    # Reclaim our port if necessary.
    if ( $self->{dcc}->{$id}->{listener} && $self->{dccports}) {
        push ( @{ $self->{dccports} }, $self->{dcc}->{$id}->{port} );
    }

    DCC: {
        last DCC if $errnum != 0;

        # Did the peer of a DCC GET connection close the socket after the file
        # transfer finished? If so, it's not really an error.
        if ($self->{dcc}->{$id}->{type} eq 'GET') {
            if ($self->{dcc}->{$id}->{done} < $self->{dcc}->{$id}->{size}) {
                last DCC;
            }
        }

        if ($self->{dcc}->{$id}->{type} =~ /^(GET|CHAT)$/) {
            $irc->send_event(
                'irc_dcc_done',
                $id,
                @{ $self->{dcc}->{$id} }{qw(
                    nick type port file size done peeraddr
                )},
            );

            $self->_remove_dcc($id);
        }

        return;
    }

    # something went wrong
    if ($errnum == 0 && $self->{dcc}->{$id}->{type} eq 'GET') {
        $errstr = 'Aborted by sender';
    }
    else {
        $errstr = $errstr
            ? $errstr = "$operation error $errnum: $errstr"
            : $errstr = "$operation error $errnum"
        ;
    }

    $irc->send_event(
        'irc_dcc_error',
        $id,
        $errstr,
        @{ $self->{dcc}->{$id} }{qw(
            nick type port file size done peeraddr
        )},
    );

    $self->_remove_dcc($id);
    return;
}

# What happens when a DCC connection sits waiting for the other end to
# pick up the phone for too long.
sub _dcc_timeout {
    my ($kernel, $self, $id) = @_[KERNEL, OBJECT, ARG0];

    if (exists $self->{dcc}->{$id} && !$self->{dcc}->{$id}->{open}) {
        $kernel->yield(
            '_dcc_failed',
            'connection',
            0,
            'DCC connection timed out',
            $id,
        );
    }
    return;
}

# This event occurs when a DCC connection is established.
## no critic (InputOutput::RequireBriefOpen)
sub _dcc_up {
    my ($kernel, $self, $sock, $peeraddr, $id) =
        @_[KERNEL, OBJECT, ARG0, ARG1, ARG3];
    my $irc = $self->{irc};

    # Delete the listening socket and monitor the accepted socket
    # for incoming data
    delete $self->{dcc}->{$id}->{factory};
    $self->{dcc}->{$id}->{open} = 1;
    $self->{dcc}->{$id}->{peeraddr} = inet_ntoa($peeraddr);

    $self->{dcc}->{$id}->{wheel} = POE::Wheel::ReadWrite->new(
        Handle => $sock,
        Driver => ($self->{dcc}->{$id}->{type} eq 'GET'
            ? POE::Driver::SysRW->new( BlockSize => IN_BLOCKSIZE )
            : POE::Driver::SysRW->new()
        ),
        Filter => ($self->{dcc}->{$id}->{type} eq 'CHAT'
            ? POE::Filter::Line->new( Literal => "\012" )
            : POE::Filter::Stream->new()
        ),
        InputEvent => '_dcc_read',
        ErrorEvent => '_dcc_failed',
    );

    $self->{wheelmap}->{ $self->{dcc}->{$id}->{wheel}->ID } = $id;

    my $handle;
    if ($self->{dcc}->{$id}->{type} eq 'GET') {
        # check if we're resuming
        my $mode = $self->{dcc}->{$id}->{resuming} ? '>>' : '>';

        if ( !open $handle, $mode, $self->{dcc}->{$id}->{file} ) {
            $kernel->yield(_dcc_failed => 'open file', $! + 0, $!, $id);
            return;
        }

        binmode $handle;
        $self->{dcc}->{$id}->{fh} = $handle;
    }
    elsif ($self->{dcc}->{$id}->{type} eq 'SEND') {
        if (!open $handle, '<', $self->{dcc}->{$id}->{file}) {
            $kernel->yield(_dcc_failed => 'open file', $! + 0, $!, $id);
            return;
        }

        binmode $handle;
        seek $handle, $self->{dcc}{$id}{done}, 0;
        # Send the first packet to get the ball rolling.
        read $handle, my $buffer, $self->{dcc}->{$id}->{blocksize};
        $self->{dcc}->{$id}->{wheel}->put($buffer);
        $self->{dcc}->{$id}->{fh} = $handle;
    }

    # Tell any listening sessions that the connection is up.
    $irc->send_event(
        'irc_dcc_start',
        $id,
        @{ $self->{dcc}->{$id} }{qw(
            nick type port file size peeraddr
        )},
    );

    return;
}

sub _cancel_timeout {
    my ($kernel, $self, $id) = @_[KERNEL, OBJECT, ARG0];
    my $alarm_id = delete $self->{dcc}{$id}{alarm_id};
    $kernel->alarm_remove($alarm_id);
    return;
}

sub _remove_dcc {
    my ($self, $id) = @_;

    if (exists $self->{dcc}{$id}{alarm_id}) {
        $poe_kernel->call($self->{session_id}, '_cancel_timeout', $id);
    }

    if (exists $self->{dcc}{$id}{wheel}) {
        delete $self->{wheelmap}{ $self->{dcc}{$id}{wheel}->ID };
        if ($^O =~ /cygwin|MSWin/) {
          $self->{dcc}{$id}{wheel}->$_ for qw(shutdown_input shutdown_output);
        }
    }

    # flush the filehandle
    close $self->{dcc}{$id}{fh} if $self->{dcc}{$id}{type} eq 'GET';

    delete $self->{dcc}{$id};
    return;
}

1;

=encoding utf8

=head1 NAME

POE::Component::IRC::Plugin::DCC - A PoCo-IRC plugin providing support for
DCC transfers

=head1 SYNOPSIS

 # send a file
 my $file = '/home/user/secret.pdf';
 my $recipient = 'that_guy';
 $irc->yield(dcc => $recipient => SEND => $file);

 # receive a file
 sub irc_dcc_request {
     my ($user, $type, $port, $cookie, $file, $size, $addr) = @_[ARG0..$#_];
     return if $type ne 'SEND';

     my $irc = $_[SENDER]->get_heap();
     my $nick = (split /!/, $user)[0];

     print "$nick wants to send me '$file' ($size bytes) from $addr:$port\n");
     $irc->yield(dcc_accept => $cookie);
 }

=head1 DESCRIPTION

This plugin provides the IRC commands needed to make use of DCC. It is used
internally by L<POE::Component::IRC|POE::Component::IRC> so there's no
need to add it manually.

=head1 METHODS

=head2 C<new>

Takes no arguments.

Returns a plugin object suitable for feeding to
L<POE::Component::IRC|POE::Component::IRC>'s C<plugin_add> method.

=head2 C<dccports>

Sets the TCP ports that can be used for DCC sends. Takes one argument,
an arrayref containing the port numbers.

=head2 C<nataddr>

Sets the public NAT address to be used for DCC sends.

=head2 C<dcc_info>

Takes one argument, a DCC connection id (see below). Returns a hash of
information about the connection. The keys are: B<'nick'>, B<'type'>,
B<'port'>, B<'file'>, B<'size'>, B<'done,'>, and B<'peeraddr'>.

=head1 COMMANDS

The plugin responds to the following
L<POE::Component::IRC|POE::Component::IRC> commands.

=head2 C<dcc>

Send a DCC SEND or CHAT request to another person. Takes at least two
arguments: the nickname of the person to send the request to and the type
of DCC request (SEND or CHAT). For SEND requests, be sure to add a third
argument for the filename you want to send. Optionally, you can add a fourth
argument for the DCC transfer blocksize, but the default of 1024 should
usually be fine. The fifth (and optional) argument is the request timeout
value in seconds (default: 300).

Incidentally, you can send other weird nonstandard kinds of DCCs too;
just put something besides 'SEND' or 'CHAT' (say, 'FOO') in the type
field, and you'll get back C<irc_dcc_foo> events (with the same arguments as
L<C<irc_dcc_chat>|/irc_dcc_chat>) when data arrives on its DCC connection.

If you are behind a firewall or Network Address Translation, you may want to
consult L<POE::Component::IRC|POE::Component::IRC>'s
L<C<connect>|POE::Component::IRC/spawn> for some parameters that are
useful with this command.

=head2 C<dcc_accept>

Accepts an incoming DCC connection from another host. First argument:
the magic cookie from an L<C<irc_dcc_request>|/irc_dcc_request> event.
In the case of a DCC GET, the second argument can optionally specify a
new name for the destination file of the DCC transfer, instead of using
the sender's name for it. (See the L<C<irc_dcc_request>|/irc_dcc_request>
section below for more details.)

=head2 C<dcc_resume>

Resumes a DCC SEND file transfer. First argument: the magic cookie from an
L<C<irc_dcc_request>|/irc_dcc_request> event. An optional second argument
provides the name of the file to which you want to write.

=head2 C<dcc_chat>

Sends lines of data to the person on the other side of a DCC CHAT connection.
The first argument should be the wheel id of the connection which you got
from an L<C<irc_dcc_start>|/irc_dcc_start> event, followed by all the data
you wish to send (it'll be separated with newlines for you).

=head2 C<dcc_close>

Terminates a DCC SEND or GET connection prematurely, and causes DCC CHAT
connections to close gracefully. Takes one argument: the wheel id of the
connection which you got from an L<C<irc_dcc_start>|/irc_dcc_start>
(or similar) event.

=head1 OUTPUT EVENTS

=head2 C<irc_dcc_request>

B<Note:> This event is actually emitted by
L<POE::Filter::IRC::Compat|POE::Filter::IRC::Compat>, but documented here
to keep all the DCC documentation in one place. In case you were wondering.

You receive this event when another IRC client sends you a DCC
(e.g. SEND or CHAT) request out of the blue. You can examine the request
and decide whether or not to accept it (with L<C<dcc_accept>|/dcc_accept>)
here. In the case of DCC SENDs, you can also request to resume the file with
L<C<dcc_resume>|/dcc_resume>.

B<Note:> DCC doesn't provide a way to explicitly reject requests, so if you
don't intend to accept one, just ignore it or send a
L<NOTICE|POE::Component::IRC/notice> or L<PRIVMSG|POE::Component::IRC/privmsg>
to the peer explaining why you're not going to accept.

=over 4

=item * C<ARG0>: the peer's nick!user@host

=item * C<ARG1>: the DCC type (e.g. 'CHAT' or 'SEND')

=item * C<ARG2>: the port which the peer is listening on

=item * C<ARG3>: this connection's "magic cookie"

=item * C<ARG4>: the file name (SEND only)

=item * C<ARG5>: the file size (SEND only)

=item * C<ARG6>: the IP address which the peer is listening on

=back

=head2 C<irc_dcc_start>

This event notifies you that a DCC connection has been successfully
established.

=over 4

=item * C<ARG0>: the connection's wheel id

=item * C<ARG1>: the peer's nickname

=item * C<ARG2>: the DCC type

=item * C<ARG3>: the port number

=item * C<ARG4>: the file name (SEND/GET only)

=item * C<ARG5>: the file size (SEND/GET only)

=item * C<ARG6>: the peer's IP address

=back

=head2 C<irc_dcc_chat>

Notifies you that one line of text has been received from the
client on the other end of a DCC CHAT connection.

=over 4

=item * C<ARG0>: the connection's wheel id

=item * C<ARG1>: the peer's nickname

=item * C<ARG2>: the port number

=item * C<ARG3>: the text they sent

=item * C<ARG4>: the peer's IP address

=back

=head2 C<irc_dcc_get>

Notifies you that another block of data has been successfully
transferred from the client on the other end of your DCC GET connection.

=over 4

=item * C<ARG0>: the connection's wheel id

=item * C<ARG1>: the peer's nickname

=item * C<ARG2>: the port number

=item * C<ARG3>: the file name

=item * C<ARG4>: the file size

=item * C<ARG5>: transferred file size

=item * C<ARG6>: the peer's IP address

=back

=head2 C<irc_dcc_send>

Notifies you that another block of data has been successfully
transferred from you to the client on the other end of a DCC SEND
connection.

=over 4

=item * C<ARG0>: the connection's wheel id

=item * C<ARG1>: the peer's nickname

=item * C<ARG2>: the port number

=item * C<ARG3>: the file name

=item * C<ARG4>: the file size

=item * C<ARG5>: transferred file size

=item * C<ARG6>: the peer's IP address

=back

=head2 C<irc_dcc_done>

You receive this event when a DCC connection terminates normally.
Abnormal terminations are reported by L<C<irc_dcc_error>|/irc_dcc_error>.

=over 4

=item * C<ARG0>: the connection's wheel id

=item * C<ARG1>: the peer's nickname

=item * C<ARG2>: the DCC type

=item * C<ARG3>: the port number

=item * C<ARG4>: the filename (SEND/GET only)

=item * C<ARG5>: file size (SEND/GET only)

=item * C<ARG6>: transferred file size (SEND/GET only)

=item * C<ARG7>: the peer's IP address

=back

=head2 C<irc_dcc_error>

You get this event whenever a DCC connection or connection attempt
terminates unexpectedly or suffers some fatal error. Some of the
following values might be undefined depending the stage at which
the connection/attempt failed.

=over 4

=item * C<ARG0>: the connection's wheel id

=item * C<ARG1>: the error string

=item * C<ARG2>: the peer's nickname

=item * C<ARG3>: the DCC type

=item * C<ARG4>: the port number

=item * C<ARG5>: the file name

=item * C<ARG6>: file size in bytes

=item * C<ARG7>: transferred file size in bytes

=item * C<ARG8>: the peer's IP address

=back

=head1 AUTHOR

Dennis 'C<fimmtiu>' Taylor and Hinrik E<Ouml>rn SigurE<eth>sson, hinrik.sig@gmail.com

=cut
