#!/usr/bin/perl
#-w
BEGIN {unshift @INC,'.'}
use strict;
use Net::IRC;


##################################################
#   	    	    config
##################################################

open CFG,"config" or die "Can't open config: $!";
open JLIST, "jlist" or print "Can't open jlist: $!";
my %config;
my @channels;
my @jlist;
my $section;

while(<CFG>) {
    chop;
    if (/---/) { 
	$section++;
	next; 
    }
    my ($param, $value) = split '=';
    if ($param =~ /channels/) {
        @channels=split ',',$value;
	next;
    }
    $config{$param}=$value;
}
close CFG;

while(<JLIST>) {
    chop;
    my ($full, $short, $id)=split ',';
    push @jlist, {full=>$full, short=>$short, id=>$id}; 	
}
close JLIST;

##################################################
#   	    	    setup etc
##################################################

my %seen;
my %msgs;
my @chans;
my $coordtime;
my $coordchan;

if (open MSGS,"./messages") {
    while (<MSGS>) {
    	my ($from,$to,$body,$time) = split '\001';
	push @{$msgs{$to}}, {from=>$from,time=>$time,body=>$body};
    }
} else {
    output("Couldn't open ./messages: $!");
}


my $irc = new Net::IRC;

output("Connecting to $config{server}:$config{port}...");



my $conn = $irc->newconn(Server   => $config{server},
			 Port     => $config{port},
			 Nick     => $config{nick},
			 Ircname  => $config{realname},
			 Username => $config{username},
			 )
    or die "Unable to connect to $config{server}:$config{port}";

$conn->add_handler('cping', 	\&on_ping);
$conn->add_handler('cversion',  \&on_version);
$conn->add_handler('msg',   	\&on_msg);
$conn->add_handler('public',	\&on_public);
$conn->add_handler('caction',	\&on_action);
$conn->add_handler('join',  	\&on_join);
$conn->add_handler('part',  	\&on_part);
$conn->add_handler('topic', 	\&on_topic);
$conn->add_handler('nick', 	\&on_nick);
$conn->add_handler('mode', 	\&on_mode);
$conn->add_handler('quit', 	\&on_quit);
$conn->add_handler('kick', 	\&on_kick);

$conn->add_global_handler('disconnect', \&on_disconnect);
$conn->add_global_handler(376, \&on_connect);
$conn->add_global_handler(433, \&on_nick_taken);

use IO::Socket::UNIX;
my $sockfile="/tmp/botsock";
unlink $sockfile;
my $external=new IO::Socket::UNIX (Type=>SOCK_STREAM,
				   Listen=>1,
				   Local=>$sockfile,
				   ) or die $!;
chmod 0666,$sockfile;

$irc->addfh($external, \&do_external, "r");

while(1) {
    $irc->do_one_loop;
    if ($coordtime) {
	if (time > $coordtime){
	    $conn->join($coordchan);
	    $coordtime='';
	    $coordchan='';
	}
    }
}



##################################################
#   	    	    handlers
##################################################

#
# bot maintenance events
#
sub on_connect {
    my $self = shift;
    
    for (@channels) { joinchan($self,$_) }
    output("Connected.");
}
sub on_nick_taken {
    my $self = shift;

    $self->nick(substr($self->nick, -1) .
    	        substr($self->nick, 0, length($self->nick)-1));
}
sub on_disconnect {
	my ($self, $event) = @_;

	output("Disconnected.");
	if ($config{reconnect} != -1) {
	    output("Reconnecting in $config{reconnect} seconds...");
    	    sleep($config{reconnect});
	    @chans=();
	    output("Connecting to $config{server}:$config{port}...");
    	    $self->connect();
        }
	else { exit(0); }
}

#
# command parsing
#
sub on_msg {
    my ($self, $event) = @_;
    see($self, $event->from);
    
    output(sprintf "Received command \'%s\' from %s", ($event->args)[0],
    	    	    $event->nick);
    my ($cmd, $args) = split ' ',($event->args)[0],2;
    
    if ($cmd eq 'msg') {
    	my ($recipientnick, $msgbody) = split ' ',$args,2;
	if ($recipientnick eq '' || $msgbody eq '') {
	    $self->privmsg($event->nick, "Syntax: msg <nick> <body>");
	    return;
	}
    	my $recipient = getJfromnick($recipientnick);
    	if ($recipient) {
    	    my %msg = (from=>$event->nick, time=>time, body=>$msgbody);
    	    push @{$msgs{$recipient}}, \%msg;
	    $self->privmsg($event->nick, "Message stored.");
	    writemsgs($self) or do {
	    	$self->privmsg($event->nick, "Couldn't save message file - if".
		      " I crash before doing so your message will be lost :(");
	    };
    	} else {
    	    $self->privmsg($event->nick,
	    	    	   "Sorry, I don't know who \'$recipientnick\' is.\n");
    	}
    }
    elsif ($cmd eq $config{opsword}) {
    	for (@chans) {
    	    $self->mode($_, "+o", $event->nick);
	}
    }
    elsif ($cmd eq 'seen') {
    	if ($args eq '') {
	    $self->privmsg($event->nick, "Syntax: seen <nick>");
	    return;
	}
	if (my $jfound = getJfromnick($args)) {
	    if ($seen{$jfound}) {
	    	$self->privmsg($event->nick,
		    	       "$jfound was last seen ".localtime $seen{$jfound}.".");
	    } else {
	    	$self->privmsg($event->nick,
		    	       "I don't remember when I last saw $jfound.");
	    }
	}
	else {
	    $self->privmsg($event->nick, "Sorry, I don't know who \'$args\' is.");
	}
    }
    elsif ($cmd eq 'say') {
    	my ($chan, $msg) = split ' ',$args,2;
	if ($chan eq '' || $msg eq '') {
	    $self->privmsg($event->nick, "Syntax: say <channel> <message>");
	    return;
        }
	if (!onchan($chan)) {
	    $self->privmsg($event->nick, "I'm not on $chan");
	    return;
	}
        $self->privmsg($chan, $msg);
	openlog($chan);
        printf LOG "%s %s PRIVMSG %s :%s\n", time, $self->nick."!*@*", $chan,
	    $msg;
    	close LOG;
    }
    elsif ($cmd eq 'me') {
    	my ($chan, $msg) = split ' ',$args,2;
	if ($chan eq '' || $msg eq '') {
	    $self->privmsg($event->nick, "Syntax: me <channel> <message>");
	    return;
        }
	if (!onchan($chan)) {
	    $self->privmsg($event->nick, "I'm not on $chan");
	    return;
	}
        $self->privmsg($chan, "\001ACTION $msg\001");
	openlog($chan);
        printf LOG "%s %s PRIVMSG %s :\001ACTION %s\001\n",
	   	time, $self->nick."!*@*", $chan, $msg;
    	close LOG;
    }
    elsif ($cmd eq 'join') {
    	if ($args) {
	    joinchan($self, $args);
	} else {
	    $self->privmsg($event->nick, "Syntax: join <channel>");
	}
    }
    elsif ($cmd eq 'part') {
    	if ($args) {
	    leavechan($self,$args);
	} else {
	    $self->privmsg($event->nick, "Syntax: part <channel>");
	}
    }
    elsif ($cmd eq 'nick') {
    	if ($args) {
    	    $self->nick($args);
	} else {
	    $self->privmsg($event->nick, "Syntax: nick <newnick>");
	}
    }
    elsif ($cmd eq 'coord') {
    	# FIXME:  he currently doesn't respond during the coord delay!
	# (does this really matter?)
    	my ($chan, $secs) = split ' ',$args;
	if ($chan eq '' || $secs eq '') {
	    $self->privmsg($event->nick, "Syntax: coord <channel> <secs>");
	    return;
	}
	$coordtime=time+$secs;
	$coordchan=$chan;
        $self->part($chan);
    }
    elsif ($cmd eq 'rebike') {
    	$self->quit($args);
    }
    elsif ($cmd eq 'quit') { # [sic]
	# sucky way of making him not reconnect
    	$config{reconnect} = -1;
	logend($_) for (@chans);
    	$self->quit($args);
    }
    elsif ($cmd eq 'adduser'){
	my($full, $short, $id)= split ' ', $args;
	if ($full eq '' || $short eq '' || $id eq '') {
	    $self->privmsg($event->nick, "Syntax: adduser <fullnick> <nickbase> <ID>");
	    return;
	}
	push @jlist, {full=>$full, short=>$short, id=>$id};
	if(open JLIST, ">jlist") {
	    $self->privmsg($event->nick, "User $full added");
	    foreach my  $user (@jlist) {
		print JLIST $user->{full},",",$user->{short},",",$user->{id},"\n";
	    }
	    close JLIST;
	}
    }
    else { $self->privmsg($event->nick, "*boggle*"); }
}

sub do_external {
    print "do_external\n";
    my $e=$external->accept;
    my $line;
    $e->read ($line, 1024) or return;
	chomp;
	my ($nick,$chan,$msg)=split/ /,$line,3;
    print "nick:$nick chan:$chan msg:$msg\n";
    return unless onchan($chan);
    return unless $nick=~/^[ -~]+$/;
    return unless $msg=~/^[ -~]+$/;
	my $self=$conn;
	$msg="<$nick> $msg";
	$self->privmsg($chan, $msg);
	openlog($chan);
	printf LOG "%s %s PRIVMSG %s :%s\n", time, $self->nick."!*@*", $chan,
	$msg;
	close LOG;
    print $e "OK\n";
}

#
# ctcp stuff
#
sub on_ping {
    my ($self, $event) = @_;
    see($self, $event->from);

#    output("ping: ".join ' ',$event->args);
    $self->ctcp_reply($event->nick, "all my CTCP stuff is b0rk.  blame Net::IRC");
#    $self->ctcp_reply($event->nick, "PING :".($event->args)[0]);
}
sub on_version {
    my ($self, $event) = @_;
    see($self, $event->from);

#    output("ping: ".join ' ',$event->args);
    $self->ctcp_reply($event->nick, "VERSION chronosbot v2.01");
}


#
# these are just for logging purposes
#
sub on_public {
    my ($self, $event) = @_;
    see($event->from);
    showmsgs($self,$event->from);
    logline('PRIVMSG',$event);
}
sub on_action {
    my ($self, $event) = @_;
    see($event->from);
    showmsgs($self,$event->from);
    logline('ACTION',$event);
}
sub on_topic {
    my ($self, $event) = @_;
    see($event->from);
    showmsgs($self,$event->from);
    logline('TOPIC',$event) if ($event->to)[0];
}
sub on_mode {
    my ($self, $event) = @_;
    see($event->from);
    showmsgs($self,$event->from);
    logline('MODE',$event);
}
sub on_part {
    my ($self, $event) = @_;
    see($event->from);
    showmsgs($self,$event->from);
    logline('PART',$event);
    # if it's the bot that's parting, write a LOGEND as well
    logend($event->to) if ($event->nick eq $self->nick);
}
sub on_join {
    my ($self, $event) = @_;
    see($event->from);
    showmsgs($self,$event->from);
    logline('JOIN',$event);
}
sub on_nick {
    my ($self, $event) = @_;
    see($event->from);
    # show msgs to the *new* nick
    my ($newdesc) = (split '!',($event->args)[0])[0].'!'.
    	    	    (split '!',$event->from)[1];
    showmsgs($self,$newdesc);
    logline('NICK',$event);
}
sub on_kick {
    my ($self, $event) = @_;
    see($event->from);
    showmsgs($self,$event->from);
    logline('KICK',$event);
    $self->join(($event->args)[0]) if (($event->to)[0] eq $self->nick);
}
sub on_quit {
    my ($self, $event) = @_;
    see($event->from);
    logline('QUIT',$event);
}




##################################################
#    other functions - most of these are ugly
##################################################

sub openlog {
    my $chan = lc shift;
    my ($day,$month,$year) = (localtime)[3,4,5];
    my $filename=sprintf ">>$config{logdir}/$chan-%04d%02d%02d.raw",
    	$year+1900, $month+1, $day;
    open LOG,$filename or do {
    	output("Unable to open $filename for writing: $!");
    	return;
    };
}

sub logbegin {
    openlog($_[0]);
    printf LOG time." LOGBEGIN\n";
    close LOG;
}

sub logend {
    openlog($_[0]);
    printf LOG time." LOGEND\n";
    close LOG;
}

# convert back to raw format all the information that Net::IRC has
# so meticulously parsed for us.  *sigh*.
sub logline {
    my ($type,$event)=@_;

    # these *should* go to all channels the originating user is
    # on.  however, they don't, they go to every channel the
    # *bot* is on.  this sucks; it's because I'm lazy.
    # version 1 had the same problem though, and no-one ever
    # complained too loudly, from what I recall.
    if ($type eq 'NICK') {
    	for (@chans) {
	    openlog($_);
    	    printf LOG "%s %s NICK :%s\n", time, $event->from,
	    	($event->args)[0];
	    close LOG;
	}
	return;
    }
    if ($type eq 'QUIT') {
    	for (@chans) {
	    openlog($_);
    	    printf LOG "%s %s QUIT :%s\n", time, $event->from,
	    	($event->args)[0];
	    close LOG;
	}
	return;
    }
    
    # the rest go to an individual channel, so figure out which and open it
    my $chan;
    if ($type eq 'KICK') { $chan = ($event->args)[0] }
    else { $chan = ($event->to)[0] }
    openlog($chan);
    
    # output chronos-stamp and optionally censor line
    printf LOG "%s%s ", (($event->args)[0]=~/cen247/)?'CENSORED ':'', time;

    # now the formatted line
    if ($type eq 'PRIVMSG') {
	printf LOG "%s PRIVMSG %s :%s\n", $event->from, ($event->to)[0],
	    ($event->args)[0];
    }
    if ($type eq 'ACTION') {
	printf LOG "%s PRIVMSG %s :\001ACTION %s\001\n", $event->from,
	    ($event->to)[0], ($event->args)[0];
    }
    if (($type eq 'TOPIC') and ($event->to)[0]) {
        printf LOG "%s TOPIC %s :%s\n", $event->from, ($event->to)[0],
	    ($event->args)[0];
    }
    if ($type eq 'MODE') {
        printf LOG "%s MODE %s %s\n", $event->from, $event->to,
	    join(' ',$event->args);
    }
    if ($type eq 'PART') {
    	printf LOG "%s PART %s :%s\n", $event->from, ($event->to)[0],
	    ($event->args)[0];
    }
    if ($type eq 'JOIN') {
    	printf LOG "%s JOIN :%s\n", $event->from, ($event->to)[0];
    }
    if ($type eq 'KICK') {
    	printf LOG "%s KICK %s %s :%s\n", $event->from, ($event->args)[0],
	    $event->to, ($event->args)[1];
    }
    
    close LOG;
}

# check $_[0] (*!*@*) against the J list and return
# that J's full nick base if found
sub recognise {
    my ($jnick,$jstuff) = split '!',$_[0];
    my ($jid,$jaddr) = split '@',$jstuff;
    
    for (0..$#jlist) {
    	if ($jlist[$_]{id} eq '*' || $jid =~ /~?$jlist[$_]{id}/i) {
	    return $jlist[$_]{full} if ($jnick =~ /\A$jlist[$_]{full}/i ||
	    	    	    	        $jnick =~ /\A$jlist[$_]{short}/i);
	}
    }
}

# find the first J in the J list that the nick $_[0] matches
sub getJfromnick {
    for (0..$#jlist) {
    	return $jlist[$_]{full} if ($_[0] =~ /\A$jlist[$_]{full}/i ||
	    	    	    	    $_[0] =~ /\A$jlist[$_]{short}/i);
    }
}

# if $jdesc (*!*@*) is a recognised J, set their last-seen time to
# now
sub see {
    my ($jdesc) = @_;
    
    return if (index($jdesc,'!') < 0);
    my ($nick) = split '!',$jdesc;
    my $jfound;
    if ($jfound = recognise($jdesc)) {
    	$seen{$jfound} = time;
    }
}

# if $jdesc (*!*@*) is a recognised J, show them their messages
sub showmsgs {
    my ($self,$jdesc) = @_;
    
    return if (index($jdesc,'!') < 0);
    my ($nick) = split '!',$jdesc;
    my $jfound;
    my $suck=0;
    return if (!($jfound = recognise($jdesc)));
    for (@{$msgs{$jfound}}) {
    	output("Showing message to $jfound");
	$self->privmsg($nick, "Message from ".$_->{from}." - \'".
	    	       $_->{body}."\' - ".localtime $_->{time}."\n");
	$suck=1;
    }
    splice @{$msgs{$jfound}};
    writemsgs($self) if ($suck == 1);
}

# save messages to file
sub writemsgs {
    my $self = shift;
    
    open MSGS,">./messages" or do {
    	output("Couldn't write to ./messages: $!");
    	return 0;
    };
    for my $to (keys %msgs) {
    	for (@{$msgs{$to}}) {
	    printf MSGS "%s\001%s\001%s\001%s\n", $_->{from}, $to,
	    	    	    	    	    $_->{body}, $_->{time};
	}
    }
    close MSGS;
    return 1;
}

# write a string to stdout with a prepended timestamp.
sub output {
    my ($sec,$min,$hour) = localtime time;
    printf "[%02d:%02d:%02d] $_[0]\n",$hour,$min,$sec;
}

# return 1 if given channel is in the channel list
# there's gotta be a better way of doing this!
sub onchan {
    for (@chans) {
    	return 1 if ($_ eq $_[0]);
    }
    return 0;
}

# join a channel, opening its logfile and adding to the list of
# currently-joined channels
sub joinchan {
    my ($self,$chan) = @_;
    
    $self->join($chan);
    if (onchan($chan)) {
        output("Hmm... joining $chan, but it's already in the list");
	return;
    } else {
    	logbegin($chan);
	push @chans,$chan;
    }
}

# leave a channel, closing its logfile and removing it from the
# list of currently-joined channels
sub leavechan {
    my ($self,$chan) = @_;
    
    $self->part($chan);
    if (onchan($chan)) {
	# there's gotta be a better way of doing this, too!
	my @suck = @chans;
	@chans = ();
	for (@suck) { push @chans,$_ unless ($_ eq $chan) }
    } else {
        output("Hmm... trying to leave $chan, but it's not in the list");
    }
}

