#!/usr/bin/perl -w

# This is a simple IRC bot based on the example in the POE cookbook

# Things:
# + tracking logins/logouts/aways/last active
#   check per-server botnick instead of global one
#   how to deal with Not Talking To Myself stuff?
use strict;

use POE;
use POE::Component::IRC;
use POE::Component::Client::HTTP;
use GDBM_File;
use Fcntl;
use Storable;

use IO::File;

use HTTP::Request;
use HTML::TokeParser;
use URI::Escape;
use POSIX;

# Create the private directory
sub PRIVDIR{ "$ENV{'HOME'}/.officebot" }

-d PRIVDIR or mkdir PRIVDIR, 0700;

use vars qw( %config );

# load the config file, which is just more perl
do( PRIVDIR . "/botrc" ) or die "botrc: " . ("$@"?"$@":("$!"?"$!":"???"));

$config{'debug'} = $ENV{'DEBUG'} if $ENV{'DEBUG'};
$config{'debug'} ||= "";
$config{'triggers'} ||= [];

print STDERR "Running in DEBUG mode\n" if $config{'debug'};
print STDERR "Config:\n" if $config{'debug'};
map { print STDERR "$_ = $config{$_}\n" } sort keys %config if $config{'debug'};
print STDERR "End of config\n" if $config{'debug'};

# These messages are used to respond when the bot doesn't know what
# you've asked it.
my @noclue = (
			  "Huh?",
			  "No idea what you're talking about.",
			  "Does not compute.",
			  "Please. I'm not human.",
			 );

# Create the component that will represent an IRC network.
POE::Component::IRC->new("office");

# Default name
$config{'botnick'} ||= "abusebot" . $config{'debug'};

# Create the bot session.  The new() call specifies the events the bot
# knows about and the functions that will handle those events.
POE::Session->new (
				   _start => \&bot_start,
				   _stop => \&bot_stop,
				   irc_connected => \&on_server,
				   irc_disconnected => \&on_disconnect,
				   irc_001    => \&on_connect,
				   irc_433 => \&on_nickused,
				   irc_353 => \&on_names,
				   irc_public => \&on_privmsg,
				   irc_msg => \&on_privmsg,
				   irc_error => \&on_error,

				   irc_join => \&on_join,
				   irc_part => \&on_part,
				   irc_quit => \&on_quit,

				   irc_nick => \&on_nick,

				   irc_ctcp_action => \&on_action,

				   abuse => \&abuse_response,

				   pirate => \&pirate_response,

				   _default => \&_default,
				   signals => \&signals,
                  );

sub _default {
    return unless $config{'debug'};

    if ( $_[ARG0] =~ /^irc_(.*)$/ ) {
        print "IRC $1 received\n";
    } else {
        print "Default caught an unhandled $_[ARG0] event.\n";
    }
    print "Parameters: @{$_[ARG1]}\n" if defined $_[ARG1];

    0;                          # so we don't trap signals
}

sub signals {
    my ( $kernel, $heap, $sig ) = @_[KERNEL, HEAP, ARG0 ];
    print STDERR "Received SIG$sig\n";

    # send a message to all channels about leaving
    my @channels = keys %{$heap->{channel_data}};
    $kernel->call( office=>'privmsg', \@channels, "AIE! dying on SIG$sig!" );

    for my $chan ( keys %{$heap->{channel_data}}) {
        $kernel->call( office => part => $chan );
    }

    $kernel->call( ua => 'shutdown' );
    $kernel->call( office => 'shutdown' );

    0;
}

POE::Component::Client::HTTP->spawn(
									Agent => "abusebot/1.0",
									Alias => "ua",
									Proxy => $ENV{'http_proxy'},
									NoProxy => $ENV{'no_proxy'},
                                   );

# this might be nice, but I can't work it right now.
#$poe_kernel->sig( INT => 'signals' );

# The bot session has started.  Register this bot with the "office"
# IRC component.  Select a nickname.  Connect to a server.
sub bot_start {
    my $kernel  = $_[KERNEL];
    my $heap    = $_[HEAP];
    my $session = $_[SESSION];

    $kernel->post( office => register => "all" );

    do_connects( $kernel );
}

# This actually does the connects. You can specify a list of servers;
# if you don't, it'll connect to all the servers it knows about. This
# is also used as a reconnect routine if a server drops the bot
# connection.
sub do_connects {
    my $kernel = shift;
    my @servers = @_;

    if ( !@servers ) {
        @servers = @{$config{'servers'}};
    } else {
        my @srvs = @servers;
        @servers = ();
        for my $srv ( @srvs ) {
            for my $srvp ( @{$config{'servers'}}) {
                if ( $srvp->{server} eq $srv or grep /^$srv$/, @{$srvp->{aliases}}) {
                    push @servers, $srvp;
                }
            }
        }
    }

    for my $srv ( @servers ) {
        print STDERR "Firing connection to " . $srv->{server} . "\n" if $config{debug};

        $srv->{botnick} ||= $config{'botnick'};

        $kernel->post( office => connect =>
                       {
                        Nick => $srv->{botnick},
                        Username => $config{'botnick'},
                        Ircname => "Waider's abusive POE::Component::IRC bot",
                        Server => $srv->{server},
                        Password => $srv->{password},
                        Port => $srv->{port} || 6667,
                        UseSSL => $srv->{ssl} || 0,
                       }
                     );
    }
}

# When we die... best save those messages, in case there was anything
# important!
sub bot_stop {
    my $heap = $_[HEAP];
}

# The bot has successfully connected to a server; however, we wait for
# the 001 message before doing anything.
sub on_server {
    my $server = $_[ARG0];
    print STDERR "Connected to server $server\n" if $config{'debug'};
}

# Actually connected. Let's join some channels (per config file)
sub on_connect {
    my $server = $_[ARG0];
    my @channels;

    print STDERR "Welcomed to server $server\n" if $config{'debug'};
    # Check what we should be doing on this server
    my @servers = @{$config{'servers'}};

    # The server might be aliased. Really, we should do this with DNS
    # hackery or something.
    for my $srv ( @servers ) {
        $srv->{aliases} ||= [];
        if ( $srv->{server} eq $server or grep /^$server$/, @{$srv->{aliases}}) {
            print STDERR "Found config for this server\n" if $config{'debug'};
            @channels = @{$srv->{channels}};
            last;
        }
    }

    if ( !@channels ) {
        print STDERR "No channels specified for $server.\n";
    } else {
        for my $channel ( @channels ) {
            my %channel_data;
            $channel_data{$channel->{name}} = { password => $channel->{password}};
            $_[HEAP]->{channel_data} = \%channel_data;

            print STDERR "Joining " . $channel->{name} . "\n" if $config{'debug'};
            $_[KERNEL]->post( office => join => $channel->{name}, ($channel->{password})||"" );
        }
    }
}

# Woah! Someone's using our nick!
sub on_nickused {
    my $kernel = $_[KERNEL];
    my $server = $_[ARG0];

    # frob the name and try again
    for my $srv ( @{$config{'servers'}}) {
        if ( $srv->{server} eq $server or grep /^$server$/, @{$srv->{aliases}}) {
            $srv->{botnick} ||= $config{botnick};
            print STDERR
              "Whoops, someone's already called " . $srv->{botnick} . " here on $server\n"
                if $config{'debug'};

            $srv->{botnick} .= "_";
            last;
        }
    }

    print STDERR "Reconnecting to $server\n" if $config{debug};
    do_connects( $kernel, $server );
}

# woop, someone changed their name!
sub on_nick {
    my ( $heap, $sender, $new ) = @_[HEAP, ARG0, ARG1 ];

    my ( $oldnick ) = $sender =~ m/^(.*?)!/;
    $oldnick = quotemeta( $oldnick );

    print STDERR "$oldnick is now known as $new\n" if $config{'debug'};

    my %channels = %{$heap->{channel_data}};
    for my $channel ( keys %channels ) {
        if ( defined( $channels{$channel}->{names} )) {
            my @users = grep !/^$oldnick$/, @{$channels{$channel}->{names}};
            push @users, $new;
            $channels{$channel}->{names} = \@users;
        }
    }
}

# React to an action on the channel. We only act on messages that
# refer directly to the bot.
sub on_action {
    my ( $kernel, $heap, $who, $where, $msg ) = @_[ KERNEL, HEAP, ARG0, ARG1, ARG2 ];
    my $nick = ( split /!/, $who )[0];

    brane( $kernel, $heap, $who, $where, "$nick $msg" );
    return;
}

# Private messages to the bot get responses directed right back at the
# sender; other than that, they're handled by the same code.
sub on_privmsg {
    my ( $kernel, $heap, $who, $where, $arg ) = @_[KERNEL, HEAP, ARG0, ARG1, ARG2];

    brane( $kernel, $heap, $who, $where, $arg );
}

sub brane {
    my ( $kernel, $heap, $who, $where, $msg ) = @_;
    my $nick = ( split /!/, $who )[0];
    my $target = ( $where->[0] eq $config{'botnick'} ) ? $nick : $where->[0];
    my ( $reply, $action );
    my $channel = $where->[0];
    my %channels = %{$heap->{channel_data}};
    my @names;

    # this is awful
    if ( defined( $channel ) and defined( $channels{$channel}->{names})) {
        @names = @{$channels{$channel}->{names}};
    } else {
        push @names, $target;
    }

    my $ts = scalar(localtime);
    logmessage( $heap, $channel, "<" . $nick . ">", $msg );

    if ( $channel eq $config{'botnick'} ) {
        $msg = $config{'botnick'} . ": " . $msg;
    }

    # try to use the trigger config to handle the input
    my @triggers = @{$config{'triggers'}};
    for my $chat ( 0..$#triggers ) {
        my $regexp = $triggers[$chat]->[0];
        $regexp =~ s/%b/$config{'botnick'}/g; # XXX fixme $srv->botnick

        print STDERR "checking [$msg]\n  against [$regexp]\n"
          if $config{'debug'};

        # verify that the regexp is sane
        eval { $msg =~ /$regexp/ };
        if ( $@ ) {
            print STDERR "Broken regexp $regexp\n($@)\n";
            next;
        }

        if ( $msg =~ /$regexp/i ) {
            my @dollar = $msg =~ /$regexp/i;
            $reply = $triggers[$chat]->[1];

            if ( ref( $reply ) eq "ARRAY" ) {
                $reply = $reply->[int(rand(scalar(@{$reply})))]
            } elsif ( ref( $reply ) eq "CODE" ) {
                # danger will robinson
                eval {
                    $reply = &{$reply}( $msg, $nick, $where, @dollar );
                };
                if ( $@ ) {
                    $reply = "coderef returned $@";
                }
                # response directed to a particular channel
                if ( ref( $reply ) eq "ARRAY" ) {
                    $target = $reply->[1];
                    $reply = $reply->[0];
                }
            } elsif ( ref( $reply )) {
                $reply = 'I don\'t yet understand ' . ref( $reply ) .
                  ' triggers';
            }

            if ( defined( $reply )) {
                $reply =~ s/\%n/$nick/g;
                # could make this smarter
                $reply =~ s/\%1/$dollar[0]/g;
                $reply =~ s/\%2/$dollar[1]/g;

                # if it starts with "/me" then it's an action
                if ( $reply =~ s@^/me\s*@@ ) {
                    $action = $reply;
                    undef $reply;
                }

                # if it starts with "/msg $nick" it's a privmsg
                if ( $reply =~ s@^/msg $nick\s*@@ ) {
                    $target = $nick;
                }
            }

            # bail out on first match
            last;
        }
    }

    if ( !defined( $reply ) and !defined( $action )) {
        # See if the bot is being addressed
        if ( $msg =~ /^($config{'botnick'}:\s*)+/ or grep /^$config{'botnick'}$/, @{$where} ) {
            $msg =~ s/^($config{'botnick'}:\s*)//;

            # commands the bot understands
            if ( $msg =~ /reload$/ ) {
                # largely stolen from man perlfunc(1)
                my $return;
                unless ( $return = do( PRIVDIR . "/botrc" )) {
                    $reply = "$nick: couldn't parse botrc: $@" if $@;
                    $reply = "$nick: couldn't do botrc: $!"
                      unless defined( $return ) or $reply;
                    $reply = "$nick: couldn't run botrc"
                      unless $return or $reply;
                }
                $reply ||= "$nick: done!";
            } elsif ( $msg =~ /restart$/ ) {
                # ooog
                my $pid = fork();
                if ( $pid ) {
                    exit;
                } else {
                    # fixme: detach here
                    exec $0;
                }
            } elsif ( $msg =~ /^join\s+(.*)$/ ) {
                my $newch = $1;
                my $pass = "";
                if ( $newch =~ /^(.*?)\s+(.*)$/) {
                    $newch = $1;
                    $pass = $2;
                }
                if ( defined( $heap->{channel_data}->{$newch} )) {
                    $reply = "I'm already there!";
                } else {
                    $reply = "Ok!";
                    $kernel->post( office => join => $newch, $pass );
                    $heap->{channel_data}->{$newch} = {};
                }
            } elsif ( $msg =~ /^leave$/ ) {
                $kernel->post( office => part => $target );
                delete $heap->{channel_data}->{$target};
                print STDERR "Leaving $target\n" if $config{debug};
            } elsif ( $msg =~ /^channels/ ) {
                $reply = "I'm on the following channels:\n ";
                $reply .= join( "\n ", sort keys %{$heap->{channel_data}}) . "\nAnd that's it.";
            } elsif ( $msg =~ /^(find|where is|where's)\s+(\w+)/ ) {
                my $user = $2;
                my @chan = finduser( $heap, $user );
                if ( @chan ) {
                    $reply = "$user is on the following channels:\n ";
                    $reply .= join( "\n ", sort @chan ) . "\nAnd that's it.";
                } else {
                    $reply = "I can't find $user on any of my channels!";
                }
            } else {
                $reply = "$nick: " . $noclue[int(rand( scalar( @noclue)))];
            }
        }
    }

    reply( $kernel, $target, $reply ) if $reply;
    action( $kernel, $target, $action ) if $action;
}

sub reply {
    my ( $kernel, $to, $msg ) = @_;
    my $sent = 0;

    if ( !ref( $to )) {
        $to = [ $to ];
    }

    # allow multi-line messages
    my @lines = split( /[\r\n]+/, $msg );
    for my $ch ( @{$to } ) {
        if ( $ch ne $config{'botnick'}) {
            for $msg ( @lines ) {
                $kernel->post( office => 'privmsg', $ch, $msg );
            }
            $sent = 1;
        } else {
            print STDERR "Refusing to talk to myself!\n";
        }
    }

    print STDERR "No destination for message!\n" unless $sent;
}

sub action {
    my ( $kernel, $to, $msg ) = @_;

    if ( !ref( $to )) {
        $to = [ $to ];
    }

    if ( grep /^$config{'botnick'}$/, @{$to} ) {
        print STDERR "Not sending a message to myself!\n" if $config{'debug'};
        #	$to = CHANNEL;
        return;
    }

    $kernel->post( office => 'ctcp', $to, "ACTION " . $msg );
}

sub abuse_response {
    my ( $kernel, $heap, $request_packet, $response_packet) = @_[KERNEL, HEAP, ARG0, ARG1 ];
    my $req = $request_packet->[0];
    my $res = $response_packet->[0];

    my $content = $res->content;
    my $abuse = "";
    my $victim = shift @{$heap->{abuseme}} if defined( $heap->{abuseme});
    my $channel;
    if ( defined( $victim )) {
        ( $victim, $channel ) = ( $victim->[0], $victim->[1] );
    }
    $channel ||= $victim;       # send it as a private message
    $channel ||= $config{'botnick'}; # last resort; forces it to go to the default channel
    if ( $content ) {
        my $p = HTML::TokeParser->new( \$content );
        $p->get_tag( "font" );
        $abuse = $victim . ": " if $victim;
        $abuse .= $p->get_trimmed_text;
    }

    if ( $abuse ) {
        reply( $kernel, $channel, $abuse );
    } else {
        action( $kernel, $channel, "abuses" . ( $victim ? " $victim" : " people at random" ) . ".");
    }
}

sub pirate_response {
    my ( $kernel, $heap, $request_packet, $response_packet) = @_[KERNEL, HEAP, ARG0, ARG1 ];
    my $req = $request_packet->[0];
    my $res = $response_packet->[0];

    my $content = $res->content;
    my $pirate = "";
    my $victim = shift @{$heap->{pirate}} if defined( $heap->{pirate});
    my $channel;
    if ( defined( $victim )) {
        ( $channel ) = ( $victim->[0], $victim->[1] );
    }

    return unless $channel;

    if ( $content ) {
        if ( $config{'debug'}) {
            print STDERR "PIRACY!\n";
            print $content . "\n";
        }
        my $p = HTML::TokeParser->new( \$content );
        $p->get_tag( "h3" );
        $pirate .= $p->get_trimmed_text;

        $pirate =~ s/^The pirate speaks,"//;
        $pirate =~ s/"$//;
    } else {
        $pirate = "error: " . $res->code . " " . $res->message if $config{'debug'};
    }

    if ( $pirate ) {
        reply( $kernel, $channel, $pirate );
    } else {
        reply( $kernel, $channel, "AAR!" );
    }
}

sub on_join {
    my ( $kernel, $heap, $who, $channel ) = @_[ KERNEL, HEAP, ARG0, ARG1 ];
    my $nick = ( split /!/, $who )[0];

    print STDERR "$nick has joined $channel\n" if $config{'debug'};
    logmessage( $heap, $channel, $nick, "has joined $channel" );

    # hacky
    if ( defined( $heap->{messages}->{$nick})) {
        brane( $kernel, $heap, $who, [ $config{'botnick'} ], "any messages?" );
    }

    my ( %channels, @names );
    %channels = %{$heap->{channel_data}} if $heap->{channel_data};
    if ( defined( $channels{$channel} )) {
        @names = @{$channels{$channel}->{names}} if $channels{$channel}->{names};
    }
    push @names, $nick unless grep /^$nick$/, @names;
    $channels{$channel}->{names} = \@names;
    $heap->{channel_data} = \%channels;
}

sub on_quit {
    my ( $kernel, $heap, $who ) = @_[ KERNEL, HEAP, ARG0, ARG1 ];
    do_part( $kernel, $heap, $who );
}

sub on_part {
    my ( $kernel, $heap, $who, $channel ) = @_[ KERNEL, HEAP, ARG0, ARG1 ];
    $channel =~ s/^(.*?) :.*$/$1/;
    do_part( $kernel, $heap, $who, $channel );
}

sub do_part {
    my ( $kernel, $heap, $who, $channel ) = @_;
    my $nick = ( split /!/, $who )[0];
    my @channels = keys %{$heap->{channel_data}};

    if ( defined( $channel)) {
        @channels = ( $channel );
    } else {
        # irc_quit
        @channels = keys %{$heap->{channel_data}}; # XXX potentially fatal
        $channel = join( ", ", @channels );
    }

    print STDERR "$nick has left $channel\n" if $config{'debug'};
    logmessage( $heap, $channel, $nick, "has left $channel" );

    return if ( $nick eq $config{botnick});

    # remove from all channels
    for $channel ( @channels ) {
        my ( %channels, @names );
        %channels = %{$heap->{channel_data}} if $heap->{channel_data};
        if ( defined( $channels{$channel} )) {
            @names = @{$channels{$channel}->{names}} if $channels{$channel}->{names};
        }
        @names = grep !/^$nick$/, @names;
        $channels{$channel}->{names} = \@names;
        $heap->{channel_data} = \%channels;
    }
}

sub on_disconnect {
    print STDERR "Eeep! Disconnected from $_[ARG0]\n";

    # let's reconnect!
    do_connects( $_[KERNEL], ); #  $_[ARG0] ); xxx
}

sub on_error {
    print STDERR "Eeep! Error: " . $_[ARG0] . "\n";
}

sub on_names {
    my ( $kernel, $heap, $server, $detail ) = @_[KERNEL, HEAP, ARG0, ARG1];
    my ( $channel, $names ) = $detail =~ /^. (.*?) :(.*)$/;
    if ( !defined( $names )) {
        print STDERR "parse failed for $detail\n";
        return;
    }
    my @names = split( /\s+/, $names );
    for my $name ( @names ) {
        $name =~ s/^@//;
        print STDERR "Checking messages for $name\n" if $config{'debug'};
        if ( defined( $heap->{messages}->{$name} )) {
            reply( $kernel, $name, "I have messages for you.\nTo get them, do \"/msg $config{'botnick'} any messages?\"");
        }
    }
    my %channels;
    %channels = %{$heap->{channel_data}} if $heap->{channel_data};
    $channels{$channel}->{names} = \@names;
    $heap->{channel_data} = \%channels;
}

# Run the bot until it is done.
$poe_kernel->run();
exit 0;

# Log a message
#
# figures out which logifle based on the channel specified.
sub logmessage {
    my ( $heap, $channel, $nick, $message ) = @_;
    my %channels = %{$heap->{channel_data}};
    my $logfile = $channels{$channel}->{logfile};

    if ( !defined( $logfile )) {
        $logfile = new IO::File;
        $logfile->open( ">>" . PRIVDIR . "/$channel.log" );
    }

    print $logfile "[" . scalar(localtime) . "] $nick $message\n";
}

sub finduser {
    my ( $heap, $nick ) = @_;
    my %channels = %{$heap->{channel_data}};
    my @channels;

    for my $channel ( keys %channels ) {
        if ( defined( $channels{$channel}->{names})) {
            if ( grep /^$nick$/, @{$channels{$channel}->{names}}) {
                use Data::Dumper;
                print STDERR "adding channel...\n";
                print STDERR Dumper $channels{$channel};
                print STDERR "\n";
                push @channels, $channel
                  unless $channels{$channel}->{password}; # don't list passworded channels
            }
        }
    }

    @channels;
}
