AnyEvent-IRC-Server

 view release on metacpan or  search on metacpan

lib/AnyEvent/IRC/Server.pm  view on Meta::CPAN

package AnyEvent::IRC::Server;

use strict;
use warnings;
our $VERSION = '0.03';
use base qw/Object::Event/;
use AnyEvent::Handle;
use AnyEvent::Socket;
use AnyEvent::IRC::Util qw/parse_irc_msg/;
use Sys::Hostname;
use POSIX;
use Scalar::Util qw/refaddr/;

use Class::Accessor::Lite (
    rw => [
        qw(host port handles servername channels topics spoofed_nick prepared_cb nick2handle)
    ],
);

my $CRLF = "\015\012";

BEGIN {
    no strict 'refs';
    while (my ($code, $name) = each %AnyEvent::IRC::Util::RFC_NUMCODE_MAP) {
        *{"${name}"} = sub () { $code };
    }
};

sub debugf {
    return unless $ENV{AEIS_DEBUG};
    require Data::Dumper;
    require Term::ANSIColor;
    local $Data::Dumper::Terse=1;
    local $Data::Dumper::Indent=0;
    my $fmt = shift;
    my $s = sprintf $fmt, (map {
        ref($_) ? (
            Data::Dumper::Dumper($_)
        ) : (defined($_) ? $_ : '<<UNDEF>>')
    } @_);
    my ($package, $filename, $line) = caller(0);
    $s .= " at $filename line $line\n";
    print Term::ANSIColor::colored(["cyan"], $s);
}

sub new {
    my $class = shift;
    my $self = $class->SUPER::new(
        handles      => {}, # refaddr($handle) => $handle
        channels     => {},
        topics       => {},
        spoofed_nick => {},
        nick2handle  => {}, # $nick => $hanldle,
        welcome      => 'Welcome to the my IRC server',
        servername   => hostname(),
        network      => 'AnyEventIRCServer',
        ctime        => POSIX::strftime( '%Y-%m-%d %H:%M:%S', localtime() ),
        channel_chars => '#&',
        prepared_cb  => sub {
            my ($self, $host, $port) = @_;
            print "$class is ready on : $host:$port\n";
        },
        @_,
    );

    
    my $say = sub {
        my ($handle, $cmd, @args) = @_;
        my $msg = mk_msg_ex($self->host, $cmd, $handle->{nick}, @args);
        debugf("Sending '%s'", $msg);
        $msg .= $CRLF;
        $handle->push_write($msg)
    };
    my $need_more_params = sub {
        my ($handle, $cmd) = @_;
        $say->($handle, ERR_NEEDMOREPARAMS, $cmd, 'Not enough parameters');
    };
    $self->reg_cb(
        nick => sub {
            my ($self, $msg, $handle) = @_;
            my ($nick) = @{$msg->{params}};
            unless (defined $nick) {
                return $need_more_params->($handle, 'NICK');
            }
            if ($self->nick2handle->{$nick}) {
                return $say->($handle, ERR_NICKNAMEINUSE, $nick, 'Nickname already in use');
            }
            debugf("Set nick: %s", $nick);
            $handle->{nick} = $nick;
            $self->nick2handle->{$nick} = $handle;
            # TODO: broadcast to each user
        },
        user => sub {
            my ($self, $msg, $handle) = @_;
            my ($user, $host, $server, $realname) = @{$msg->{params}};
            # TODO: Note that hostname and servername are normally ignored by the IRC server when the USER command comes from a directly connected client (for security reasons)
            $handle->{user} = $user;
            $handle->{hostname} = $host;
            $handle->{servername} = $server;
            $handle->{realname} = $realname;

            $say->( $handle, RPL_WELCOME(), $self->{welcome} );
            $say->( $handle, RPL_YOURHOST(), "Your host is @{[ $self->servername ]} [@{[ $self->servername ]}/@{[ $self->port ]}]. @{[ ref $self ]}/$VERSION" ); # 002
            $say->( $handle, RPL_CREATED(), "This server was created $self->{ctime}");
            $say->( $handle, RPL_MYINFO(), "@{[ $self->servername ]} @{[ ref $self ]}-$VERSION" ); # 004
            $say->( $handle, ERR_NOMOTD(), "MOTD File is missing" );
        },
        'join' => sub {
            my ($self, $msg, $handle) = @_;
            my ($chans) = @{$msg->{params}};
            unless ($chans) {
                return $need_more_params->($handle, 'JOIN');
            }
            for my $chan ( split /,/, $chans ) {
                my $nick = $handle->{nick};
                debugf("%s joined to %s", $nick, $chans);
                $self->channels->{$chan}->{handles}->{$nick} = $handle;

                # server reply
                $say->( $handle, RPL_TOPIC(), $chan, $self->topics->{$chan} || '' );
                for my $handle (values %{$self->channels->{$chan}->{handles}}) {
                    next unless $handle->{nick};
                    next if $self->spoofed_nick->{$handle->{nick}};
                    $say->( $handle, RPL_NAMREPLY(), $chan, $handle->{nick} );
                }
                $say->( $handle, RPL_ENDOFNAMES(), $chan, 'End of NAMES list.' );

                # send join message
                my $comment = sprintf '%s!%s@%s', $nick, $nick, $self->servername;
                # my $comment = sprintf '%s!%s@%s', $nick, $handle->{user}, $handle->{servername};
                my $raw = mk_msg_ex($comment, 'JOIN', $chan) . $CRLF;
                for my $handle (values %{$self->channels->{$chan}->{handles}}) {
                    next unless $handle->{nick};
                    next if $self->spoofed_nick->{$handle->{nick}};
                    $handle->push_write($raw);
                }
                $self->event('daemon_join' => $nick, $chan);
            }
        },
        part => sub {
            my ($self, $msg, $handle) = @_;
            my ($chans, $text) = @{$msg->{params}};
            unless ($chans) {
                return $need_more_params->($handle, 'PART');
            }
            for my $chan ( split /,/, $chans ) {
                my $nick = $handle->{nick};
                $self->_intern_part($nick, $chan, $text);
                $self->event('daemon_part' => $nick, $chan);
            }
        },
        topic => sub {
            my ($irc, $msg, $handle) = @_;
            my ($chan, $topic) = @{$msg->{params}};
            unless ($chan) {
                return $need_more_params->($handle, 'TOPIC');
            }
            if ($topic) {
                $say->( $handle, RPL_TOPIC, $self->topics->{$chan} );
                my $nick = $handle->{nick};
                $self->_intern_topic($nick, $chan, $topic);
                $self->event('daemon_topic' => $nick, $chan, $topic);
            } else {
                $say->( $handle, RPL_NOTOPIC, $chan, 'No topic is set' );
            }
        },
        'privmsg' => sub {
            my ($irc, $msg, $handle) = @_;
            my ($chan, $text) = @{$msg->{params}};
            unless ($chan) {
                return $need_more_params->($handle, 'PRIVMSG');
            }
            my $nick = $handle->{nick};
            if ($nick eq '*') {
                warn 'Nick was not set.';
            }
            $self->_intern_privmsg($nick, $chan, $text);
            $self->event('daemon_privmsg' => $nick, $chan, $text);
        },
        'notice' => sub {
            my ($irc, $raw, $handle) = @_;
            my ($chan, $msg) = @{$raw->{params}};
            unless ($msg) {
                return $need_more_params->($handle, 'NOTICE');
            }
            my $nick = $handle->{nick};
            $self->_intern_notice($nick, $chan, $msg);
            $self->event('daemon_notice' => $nick, $chan, $msg);
        },
        'list' => sub {
            my ($irc, $raw, $handle) = @_;
            my ($chans, $msg) = @{$raw->{params}};
            $self->_intern_list($handle, $chans);
        },
        who => sub {
            my ($irc, $msg, $handle) = @_;
            my ($name) = @{$msg->{params}};

             unless ( $self->channels->{$name} ) {
                 # TODO: ZNC calls '*'.
                 # AEIS should process it.
                debugf("The channel is not listed: $name");
                $say->( $handle, RPL_ENDOFWHO(), 'END of /WHO list');
                return;
                # return $need_more_params->($handle, 'WHO'); # TODO
             }

            $say->( $handle, RPL_WHOREPLY(), $name, $handle->{user}, $handle->{hostname}, $handle->{servername}, $handle->{nick},"H:1", $handle->{realname});
            $say->( $handle, RPL_ENDOFWHO(), 'END of /WHO list');
        },
        ping => sub {
            my ($irc, $msg, $handle) = @_;
            $say->( $handle, 'PONG', $msg->{params}->[0]);
        },
    );
    return $self;
}

sub _server_comment {
    my ($self, $nick) = @_;
    return sprintf '%s!~%s@%s', $nick, $nick, $self->servername;
}

sub _send_chan_msg {
    my ($self, $nick, $chan, @args) = @_;
    # send join message
    my $handle = $self->channels->{$chan}->{handles}->{$nick};
    my $comment = sprintf '%s!%s@%s', $nick, $handle->{user} || $nick, $handle->{servername} || $self->servername;
    my $raw = mk_msg_ex($comment, @args);
    debugf("_send_chan_msg: %s", $raw);
    $raw .= $CRLF;
    if ($self->is_channel_name($chan)) {
        for my $handle (values %{$self->channels->{$chan}->{handles}}) {
            next unless $handle->{nick};
            next if $handle->{nick} eq $nick;
            next if $self->spoofed_nick->{$handle->{nick}};
            $handle->push_write($raw);
        }
    } else {
        # private talk
        # TODO: TOO SLOW
        my $handle = $self->nick2handle->{$chan};
        if ($handle) {
            $handle->push_write($raw);
        }
    }
}

sub run {
    my $self = shift;
    tcp_server $self->{host}, $self->{port}, sub {
        my ($fh, $host, $port) = @_;
        my $handle = AnyEvent::Handle->new(
            on_error => sub {
                my ($handle) = @_;
                $self->event('on_error' => $handle);
            },
            on_eof => sub {
                my ($handle) = @_;
                $self->event('on_eof' => $handle);
                # TODO: part from each channel
                if (my $nick = $handle->{nick}) {
                    delete $self->nick2handle->{$nick};
                }
                delete $self->handles->{refaddr($handle)};
            },
            fh => $fh,
        );
        $handle->{nick} = '*';
        $handle->on_read(sub {
            $handle->push_read(line => sub {
                my ($handle, $line, $eol) = @_;
                my $msg = parse_irc_msg($line);
                $self->handle_msg($msg, $handle);
            });
        });
        $self->handles->{refaddr($handle)} = $handle;
    }, $self->prepared_cb();
}

sub handle_msg {
    my ($self, $msg, $handle) = @_;
    my $event = lc($msg->{command});
       $event =~ s/^(\d+)$/irc_$1/g;
    debugf("%s %s", $event, $msg);
    $self->event($event, $msg, $handle);
}

# -------------------------------------------------------------------------

sub add_spoofed_nick {
    my ($self, $nick) = @_;
    $self->{spoofed_nick}->{$nick} = 1;
}


# -------------------------------------------------------------------------

sub daemon_cmd_join {
    my ($self, $nick, $chan, $msg) = @_;
    return if $self->channels->{$chan}->{handles}->{$nick};
    $self->add_spoofed_nick($nick);
    $self->_intern_join($nick, $chan, $self->nick2handle->{$nick});
}

sub daemon_cmd_kick {
    my ($self, $kicker, $chan, $kickee, $comment) = @_;
    $self->_intern_kick($kicker, $chan, $kickee, $comment);
}

sub daemon_cmd_topic {
    my ($self, $nick, $chan, $topic) = @_;
    $self->_intern_topic($nick, $chan, $topic);
}

sub daemon_cmd_part {
    my ($self, $nick, $chan, $msg) = @_;
    $self->_intern_part($nick, $chan, $msg);
}

sub daemon_cmd_privmsg {
    my ($self, $nick, $chan, $msg) = @_;
    $self->_intern_privmsg($nick, $chan, $msg);
}

sub daemon_cmd_notice {
    my ($self, $nick, $chan, $msg) = @_;
    debugf('%s', [$nick, $chan, $msg]);
    $self->_intern_notice($nick, $chan, $msg);
}

# -------------------------------------------------------------------------

sub _intern_list {
    my ($self, $handle, $chans) = @_;

    my $nick = $handle->{nick};
    my $comment = $self->_server_comment($nick);
    my $send = sub {
        my $raw = mk_msg_ex($comment, @_) . $CRLF;
        $handle->push_write($raw);
    };
    my $send_rpl_list = sub {
        my $chan = shift;
        $send->(RPL_LIST, $nick, $chan, scalar keys %{$self->channels->{$chan}->{handles}},  ':'.($self->topics->{$chan} || ''));
    };
    $send->(RPL_LISTSTART, $nick, 'Channel', ':Users', 'Name');
    if ($chans) {
        for my $chan (split /,/, $chans) {
            if ($self->channels->{$chan}) {
                $send_rpl_list->($chan);
            }
        }
    } else {
        my $channels = $self->channels;
        while (my ($chan, $val) = each %$channels) {
            $send_rpl_list->($chan);
        }
    }
    $send->(RPL_LISTEND, $nick, 'End of /LIST');
}

sub _intern_privmsg {
    my ($self, $nick, $chan, $text) = @_;
    $self->_send_chan_msg($nick, $chan, 'PRIVMSG', $chan, $text);
}

sub _intern_notice {
    my ($self, $nick, $chan, $text) = @_;
    debugf('%s', [$nick, $chan, $text]);
    $self->_send_chan_msg($nick, $chan, 'NOTICE', $chan, $text);
}

sub _intern_topic {
    my ($self, $nick, $chan, $topic) = @_;
    $self->topics->{$chan} = $topic;
    $self->_send_chan_msg($nick, $chan, 'TOPIC', $chan, $self->topics->{$chan});
}

sub _intern_join {
    my ($self, $nick, $chan, $handle) = @_;
    $self->channels->{$chan}->{handles}->{$nick} = $handle;
    $self->_send_chan_msg($nick, $chan, 'JOIN', $chan);
}

sub _intern_part {
    my ($self, $nick, $chan, $msg) = @_;
    $msg ||= $nick;

    # send part message
    $self->_send_chan_msg($nick, $chan, 'PART', $chan, $msg);
    delete $self->channels->{$chan}->{handles}->{$nick};
}

# /KICK <channel> <user> [<comment>]
# use this line in /kick: $self->event('daemon_kick' => $kicker, $chan, $kickee, $comment);
sub _intern_kick {
    my ($self, $kicker, $chan, $kickee, $comment) = @_;

    # TODO: implement
    # TODO: oper check
    my $handle = $self->channels->{$chan}->{handles}->{$kicker};
    my $cmt_irc = sprintf '%s!%s@%s', $kicker, $handle->{user} || $kicker , $handle->{servername} || $self->servername;
    my $raw = mk_msg_ex($cmt_irc, 'KICK', $chan, $kickee, $comment) . $CRLF;
    for my $handle (values %{$self->channels->{$chan}->{handles}}) {
        $handle->push_write($raw);
    }
    delete $self->channels->{$chan}->{handles}->{$kickee};
}

# -------------------------------------------------------------------------

sub is_channel_name {
    my ( $self, $string ) = @_;
    my $cchrs = $self->{channel_chars};
    $string =~ /^([\Q$cchrs\E]+)(.+)$/;
}

sub mk_msg_ex {
    my ( $prefix, $command, @params ) = @_;
    my $msg = "";

    $msg .= defined $prefix ? ":$prefix " : "";
    $msg .= "$command";

    my $trail;
    debugf("%s", \@params);
    if ( @params >= 2 ) {
        $trail = pop @params;
    }

    # FIXME: params must be counted, and if > 13 they have to be
    # concationated with $trail
    map { $msg .= " $_" } @params;

    $msg .= defined $trail ? " :$trail" : "";

    return $msg;
}

1;
__END__

=head1 NAME

AnyEvent::IRC::Server - An event based IRC protocol server API

=head1 SYNOPSIS

  use AnyEvent::IRC::Server;

=head1 DESCRIPTION

AnyEvent::IRC::Server is

=head1 ROADMAP

    - useful for XIRCD
    -- authentication

    - useful for public irc server
    -- anti flooder
    -- limit nick length
    -- detect nick colision
    -- support /kick
    -- mode support
    -- who support

=head1 DEBUGGING

You can trace events by L<Object::Event>'s feature.

Use the environment variable B<PERL_OBJECT_EVENT_DEBUG>

    export PERL_OBJECT_EVENT_DEBUG=2

=head1 AUTHOR

Kan Fushihara E<lt>default {at} example.comE<gt>

Tokuhiro Matsuno

=head1 SEE ALSO

=head1 LICENSE

This library is free software; you can redistribute it and/or modify



( run in 1.409 second using v1.01-cache-2.11-cpan-5b529ec07f3 )