#!/usr/bin/perl -w

######################################################################
# This is Cheetah's IRC Tools Version 0.9
# Written by Cheetah <cheetah@cwru.edu>
# Originally based, in part, on:
#   auto-op.pl by Marc Quinton@stna.dgac.fr / quinquin/IRCnet
#   auto-reop.pl (author unknown)
# Those bits I think have been rewritten several times since, but credit
# still goes to them for getting me started.
#
# This script performs several distinct tasks:
# 1) Automatically gaining ops in channels via eggdrop bots
#      (or any other bot that uses the same op syntax)
# 2) Automatically opping other people in channels
# 3) Getting your preffered nicks, as entered in the server list
# 4) Getting /whois info on /notify users when they come online
#      Note that recent versions of xchat have this built in, so this
#      functionality is turned off by default
# 5) Auto-cycling channels when you're the only one and you don't have ops
# 6) Setting safe channel modes (+stn) when you join a new channel
# 7) It routes "ISON: unknown command" messages to /dev/null for
#      stupid servers that don't support it.
#
# The first 2 are controled by config files in your ~/.xchat dir
# The /whois function can be enabled/disabled by setting the variable
# $whois_new_online_users, near the end of this file
# The channel cycling can be controlled with $cycle_dead_channels
# The channel mode setting can be controlled with $set_safe_chanmodes
#
# Preferred Nicks:
# The script will find your 3 desired nicks in your xchat.conf file and try
# to keep the most preferrable of these.  Note that in order for this to
# work properly, you MUST add these nicks to your /notify list before you
# load the script, or things will go VERY haywire.
#   TODO: recent xchat versions now have perl support for examining the notify
#   list.  Check for the nicks at load time and add them if necessary.
#
# Eggdrop Opping:
# Eggdrop opping is done via two lists:
# 1) A list of bots, their hostmask, and the password
# 2) A list of channels and the bots on those channels
# Use /addbot, /listbots, and /reloadbots to manipulate the first list
# The bot hostmask is expressed as a perl regular expression
# use /setchanbots, /listchanbots, and /reloadchanbots for the second
# To avoid redundant opping, you should list the bots for each
# channel with bots that are on the largest number of your channels first
# The script will check every 15 seconds to see if you need ops and will
# /msg enough eggdrops asking for ops that, if all respond, you will have
# ops everywhere you can get it.  The script will cycle through the list
# of bots in each channel if it finds you still not opped the next time around
#
# Auto-opping
# Auto-opping is done by a list of nick!user@host regular expressions
# mapped to channel lists.  So for each person you want to autoop, add
# a regular expression for their nick!user@host and a list of channels
# to autoop them in to the autoop list.  If you use * as a channel list, then
# they will be autooped in all channels.
# You can manipulate the autoop list with /addautoop, /listautoop, and
# /reloadautoop
#
# Deleting stuff from the lists:
# Currently the only way to delete items from any of the lists is manual
# editing of the data files.  These are fairly self-evident files
# with one line per list item.
# The bots, channel bots, and autoop lists are stored, respectively, in the
# bots.dat, chan_bots.dat, and auto_op.dat files in your ~/.xchat directory.
# To delete items, open the desired list in your favorite text editor,
# delete the appropriate lines, and then use the /reloadX command for the
# list(s) you modified (e.g. /reloadbots).
#
#############################################################################

package CIT;

sub cpmsg
	{ foreach (@_) { IRC::print("\0035CIT:\t$_\003\n"); } }
sub cpsmsg
	{ my $s = shift; foreach (@_) { IRC::print_with_channel("\0035CIT:\t$_\003\n", $s, $s); } }

sub cperr
	{ foreach (@_) { IRC::print("\0034CIT:\t$_\003\n"); } }

sub load_auto_op
{
	# load the auto-op data
	if ( ! open(OPDAT,"<" . $auto_op_dat_file) )
	{
		cperr("Can't open auto-op data file ($auto_op_dat_file)!");
		return;
	}
	undef %auto_op;
	my $n = 0;
	my $opline = "";
	while ($opline = <OPDAT>)
	{
		chop($opline);
		my $opregex = "";
		my $opchannels = "";
		my @chans = ();
		($opregex,$opchannels) = split(" ", $opline);
		@chans = split(",", $opchannels);
		foreach my $chan (@chans)
			{ $auto_op{$opregex}->{lc($chan)} = 1; }
		$n++;
	}
	cpmsg("Loaded $n auto-op regexes");
	$auto_op_init = 1;
	close(OPDAT);
}

sub load_bots
{
	if ( ! open (BOTSDAT,"<" . $bots_dat_file) )
	{
		cperr("Can't open bots data file ($bots_dat_file)!");
		return;
	}
	undef %bots;
	my $n = 0;
	my $botline = "";
	my $bothost = "";
	my $botpass = "";
	while ($botline = <BOTSDAT>)
	{
		chop($botline);
		my $botname = "";
		($botname,$bothost,$botpass) = split(" ", $botline);
		$bots{$botname}->{"host"} = $bothost;
		$bots{$botname}->{"pass"} = $botpass;
		$n++;
	}
	$cur_ask_bot = 0;
	cpmsg("Loaded $n bot entries");
	$bots_init = 1;
	close(BOTSDAT);
}

sub load_chan_bots
{
	if ( ! open (CHANBOTSDAT,"<" . $chan_bots_dat_file) )
	{
		cperr("Can't open chan bots data file ($chan_bots_dat_file)!");
		return;
	}
	undef %chan_bots;
##	undef %chan_bot_cur;
	my $n = 0;
	my $chanline = "";
	my @botlist = ();
	my $channame;
	while ($chanline = <CHANBOTSDAT>)
	{
		chop($chanline);
		$channame = "";
		my $bots;
		($channame, $bots) = split(" ", $chanline);
		@botlist = split(",", $bots);
		$channame = lc($channame);
		$chan_bots{$channame} = [@botlist];
##		$chan_bot_cur{$channame} = 0;
		$n++;
	}
	update_chan_bots();
	cpmsg("Loaded $n channel bot-op entries");
	$chan_bots_init = 1;
	close(CHANBOTSDAT);
}

sub update_chan_bots
{
	# we do a reverse mapping: bot names -> channels
	# this is for removing redundant op commands
	undef %bot_chans;
	foreach $channame (keys %chan_bots)
	{
		my $tbot;
		foreach $tbot (@{$chan_bots{$channame}})
		{
			push @{$bot_chans{$tbot}}, $channame;
		}
	}
}

sub load_nickgrab
{
	# parse xchat.conf to find preferred nicks
	if (not open(XCC, "<$xcbase/xchat.conf"))
	{
		cperr("Can't open $xcbase/xchat.conf");
		return;
	}
	%preferrednicks = ();
	my $cline;
	# cpmsg("Loading Preferred Nicks");
	while ($cline = <XCC>)
	{
		if ($cline =~ /^nickname(.) = (.*)$/)
		{
			$preferrednicks{$2} = $1;
			# we need it to be in the notify list for this to work
			#IRC::command("/notify $2");
			# cpmsg("Nick '$2' Priority $1");
		}
	}
	cpmsg("Loaded Preferred Nicks: " . join ', ',
		sort {$preferrednicks{$a} <=> $preferrednicks{$b}} keys %preferrednicks);
	close(XCC);
}

sub add_auto_op
{
	my $param = join('', @_);
	# clean excess spaces
	$param =~ s/\s+/ /g;
	# split it up
	my $newregex = "";
	my $newchans = "";
	($newregex,$newchans) = split(" ", $param);
	# usage info
	if ((lc($param) eq "help") or ($newregex eq "") or
			($newchans !~ /(^\*$)|(^[#&])/))
	{
		cpmsg("/ADDAUTOOP <regex> <channels>");
		cpmsg("  <regex> is a perl regular expression");
		cpmsg("  <channels> is a comma separated list, e.g. #a,#b,#c");
		return 1;
	}
	my @chans = split(",", $newchans);
	foreach my $chan (@chans)
		{ $auto_op{$newregex}->{$chan} = 1; }
	cpmsg("Adding auto-op for $newregex in $newchans");
	save_auto_op();
	return 1; 
}

sub add_bot
{
	my $param = join('', @_);
	# clean excess spaces
	$param =~ s/\s+/ /g;
	if (lc($param) eq "help")
	{
		cpmsg("/ADDBOT <botname> <bothost> <password>");
		cpmsg("  <botname> is just a name to keep track of the bot");
		cpmsg("  <bothost> is a perl regex for the nick!user\@host of the bot");
		cpmsg("  <password> is your password on the eggdrop");
		return 1;
	}
	# split it up
	my ($botname,$bothost,$botpass) = split(" ", $param);
	if (($botname eq "") or ($bothost eq "") or ($botpass eq ""))
	{
		cperr("You need more options, try /addbot help");
		return 1;
	}
	$bots{$botname}->{"host"} = $bothost;
	$bots{$botname}->{"pass"} = $botpass;
	cpmsg("Adding bot $botname at $bothost");
	save_bots();
	return 1;
}

sub set_chan_bots
{
	my $param = join('', @_);
	# clean excess spaces
	$param =~ s/\s+/ /g;
	if (lc($param) eq "help")
	{
		cpmsg("/SETCHANBOTS <channel> <botlist>");
		cpmsg("  <channel> is the channel name");
		cpmsg("  <botlist> is a space separated list of bots that will op you");
		return 1;
	}
	my ($channel, @bots) = split(/[ ,]/, $param);
	if (($channel eq "") or ($#bots < 0))
	{
		cperr("You need more parameters, try /setchanbots help");
		return 1;
	}
	$chan_bots{$channel} = [@bots];
##	$chan_bot_cur{$channel} = 0;
	my $botlist = join(",", @bots);
	cpmsg("Set bots for $channel to $botlist");
	update_chan_bots();
	save_chan_bots();
	return 1;
}

sub list_auto_op
{
#	load_auto_op() if ($auto_op_init == 0);
	cpmsg("Auto-op list");
	my $n = 1;
	my $userregex = "";
	foreach $userregex (keys %auto_op)
	{
		my $chans = join(",", keys %{$auto_op{$userregex}});
		cpmsg("$n $userregex $chans");
		$n++;
	}
	cpmsg("End of auto-op list");
	return 1;
}

sub list_bots
{
	cpmsg("Bot list");
	my $n = 1;
	foreach $botname (sort keys %bots)
	{
		my $bothost = $bots{$botname}->{"host"};
		cpmsg("$n $botname $bothost");
		$n++;
	}
	cpmsg("End of bot list");
	return 1;
}

sub list_chan_bots
{
	cpmsg("Channel bots list");
	my $n = 1;
	foreach $channel (sort keys %chan_bots)
	{
		my $chanlist = join(",", @{$chan_bots{$channel}});
		cpmsg("$n $channel $chanlist");
		$n++;
	}
	cpmsg("End of channelbots list");
	return 1;
}

sub save_auto_op
{
	if ( ! open(OPDAT, ">" . $auto_op_dat_file) )
	{
		cperr("Can't open auto-op data file ($auto_op_dat_file)!");
		return;
	}
	my $userregex;
	foreach $userregex (keys %auto_op)
	{
		my $chans = join(",", keys %{$auto_op{$userregex}});
		print OPDAT "$userregex $chans\n";
	}
	close(OPDAT);
	cpmsg("Saved auto-op data");
}

sub save_bots
{
	if ( ! open(BOTSDAT, ">" . $bots_dat_file) )
	{
		cperr("Can't open bots data file ($bots_dat_file)!");
		return;
	}
	my $botname;
	foreach $botname (keys %bots)
	{
		my $host = $bots{$botname}->{"host"};
		my $pass = $bots{$botname}->{"pass"};
		print BOTSDAT "$botname $host $pass\n";
	}
	close(BOTSDAT);
	cpmsg("Saved bot data");
}

sub save_chan_bots
{
	if ( ! open(CHANBOTSDAT, ">" . $chan_bots_dat_file) )
	{
		cperr("Can't open channel bots data file ($chan_bots_dat_file)!");
		return;
	}
	my $channel;
	foreach $channel (keys %chan_bots)
	{
		my $chanlist = join(",", @{$chan_bots{$channel}});
		print CHANBOTSDAT "$channel $chanlist\n";
	}
	close(CHANBOTSDAT);
	cpmsg("Saved channel bots data");
}

sub build_chanlist
{
	my $channels = $_[0];
	%$channels = ();
	my @chanlist = IRC::channel_list();
	# grab flag info for each user on each channel
	my $server = IRC::get_info(3);
	my $mynick = IRC::get_info(1);
	my $channel;

	for ($n = 0; $n <= $#chanlist; $n += 3)
	{
		# list is grouped to channel, server, user, only grab channel windows
		next unless (($chanlist[$n+1] eq $server) && ($chanlist[$n] =~ /^[#&]/));
		$channel = lc($chanlist[$n]);
		next if (defined $channels->{$channel});
		my @userlist = IRC::user_list($channel, $server);
		my $m;
		for ($m = 0; $m <= $#userlist; $m += 5)
		{
			my $user = $userlist[$m];
			$channels->{$channel}->{$user}->{"host"} = $userlist[$m+1];
			$channels->{$channel}->{$user}->{"op"} = $userlist[$m+2];
			$channels->{$channel}->{$user}->{"voice"} = $userlist[$m+3];
			# the fifth element is a ':' = separator/garbage
		}
	}
}	

sub check_ops_cmd
{
	my $server = IRC::get_info(3);
	my $mynick = IRC::get_info(1);
	my ($opt) = @_;
	my %channels;

	# check to see if we should even run this:
	# if this fails, the timer won't be re-triggered
	my $stillserver = 0;
	foreach my $tserv (IRC::server_list())
		{ $stillserver = 1 if ($tserv eq $server); }
	return 1 if (not $stillserver);
	if ((time() - $last_check_ops{$server}) < $check_ops_delay)
		{ return 1 if ($opt); }
	$last_check_ops{$server} = time();
	build_chanlist(\%channels);
	
	# now we have the server/channel/user/info tree, check ops
	my $skippedbots = get_bot_ops($server, $mynick, \%channels);
	give_auto_ops($server, $mynick, \%channels);
	
	# if we skipped any bots, repeat the check soon ignoring delay
	IRC::command("/timer $bot_resp_delay /checkops") if ($skippedbots);
	return 1;
}

sub check_all_ops
{
	# go through op sequence for each server
	foreach my $server (IRC::server_list())
	{
		# normally check ops right away
		my $delay = 1;
		if (not defined $last_check_ops{$server})
		{
			cpsmsg($server, "Initializing op checking for $server");
			$last_check_ops{$server} = 0;
			# if it's a new server, wait a bit before checking ops
			$delay = $check_ops_delay;
		}
		IRC::command_with_server("/timer $delay /checkops 1", $server);
	}
	IRC::add_timeout_handler($check_ops_delay * 1000, "CIT::check_all_ops");
}

sub find_bot
{
	my $trybot = $_[0];
	my $check_chan = $_[1];
	my $channels = $_[2];

	my $bothost = $bots{$trybot}{"host"};
	foreach my $botnick (keys %{$channels->{$check_chan}})
	{
		my $botaddr = $channels->{$check_chan}{$botnick}{"host"};
		# accept only if it's the bot and it's opped
		return $botnick
			if (("$botnick\!$botaddr" =~ m/$bothost/i) and
				($channels->{$check_chan}{$botnick}{"op"}));
	}
	return undef;
}

sub get_bot_ops
{
	my $server = $_[0];
	my $mynick = $_[1];
	my $channels = $_[2];
	my %needopchans;
	my $numopchans = 0;
	my %botnicks;
	# find channels I need ops in
	foreach $channame (keys %$channels)
	{
		if (not $channels->{$channame}->{$mynick}->{"op"})
		{
			$needopchans{$channame} = 1;
			$numopchans++;
		}
	}
	return if ($numopchans == 0);
	
	# pick a bot set of bots that will op us in each channel, but don't do
	# any redundant asks (so we may need to go through this more than once)
	
	# sort bot list decreasing by # of channels covered so that we're more
	# likely to get a minimal set
	my @botlist = sort {$#{$bot_chans{$b}} <=> $#{$bot_chans{$a}}} keys %bots;
	my $first_bot = $cur_ask_bot;
	my $done = 0;
	my @askchans;
	my $skippedbots = 0; # did we skip anything?
	
	# if we skip any bots, we want the next pass to start where we
	# started skipping, we use this var to track it
	my $endcurbot = -1;
	
	BOT: for (my $botnum = 0; $botnum <= $#botlist; ++$botnum)
	{
		$cur_ask_bot = ($first_bot + $botnum) % ($#botlist + 1);
		my $trybot = $botlist[$cur_ask_bot];
		# to avoid redundant asking, we don't ask a bot if any of its
		# chans have already been asked for
		ECHAN: foreach my $check_chan (@{$bot_chans{$trybot}})
		{
			# chan already asked ...
			if (defined $needopchans{$check_chan}
					and $needopchans{$check_chan} == 0)
			{
				++$skippedbots;
				$endcurbot = $cur_ask_bot if $endcurbot < 0;
				next BOT;
			}
		}
		# look at each chan this bot should be in
		CHAN: foreach my $check_chan (@{$bot_chans{$trybot}})
		{
			next CHAN unless ($needopchans{$check_chan});
			# try to find the bot
			$botnicks{$trybot} = find_bot($trybot, $check_chan, $channels)
				if (!defined $botnicks{$trybot});
			if (defined $botnicks{$trybot})
			{
				# bot found, this chan will be opped
				$needopchans{$check_chan} = 0;
				push @askchans, $check_chan;
				$numopchans--;
			}
		} # CHAN: for each chan this bot covers
		
		# if we've got everything, stop looking
		last BOT if $numopchans == 0;

	} # BOT: for each bot
	
	$cur_ask_bot = $endcurbot if $endcurbot >= 0;

	cpmsg(
		"Asking for ops in @{[join(', ',@askchans)]} "
		. "from @{[join(', ',grep {$botnicks{$_}} keys %botnicks)]}"
	)
		if (@askchans);
	
	# got the ask list, start asking
	ASKBOT: foreach my $askbot (keys %botnicks)
	{
		if (!defined $bots{$askbot})
		{
			# should NEVER get here!!!
			cperr("bot $botname doesn't exist, do you need to reload the bots?");
			next ASKBOT;
		}
		next ASKBOT if (!$botnicks{$askbot});
		my $botnick = $botnicks{$askbot};
		my $pass = $bots{$askbot}{"pass"};
		# display ask source above
		# cpmsg("  Asking $askbot @{[$askbot ne $askbot?qq[($botnick) ]:qq()]}for ops");
		# we use a raw to prevent output
		IRC::command_with_server("/quote PRIVMSG $botnick :OP $pass", $server);
	}
	
	return $skippedbots;
}

sub give_auto_ops
{
	my ($server, $mynick, $channels) = @_;
	CHANNEL: foreach my $channel (keys %$channels)
	{
		my $meop = $channels->{$channel}->{$mynick}->{"op"};
		next CHANNEL unless ($meop);
		NICK: foreach my $nick (keys %{$channels->{$channel}})
		{
			my $host = $channels->{$channel}->{$nick}->{"host"};
			my $opped = $channels->{$channel}->{$nick}->{"op"};
			next NICK if ($opped);
			foreach my $match (keys %auto_op)
			{
				if (("$nick\!$host" =~ m/$match/i) and
						($auto_op{$match}->{$channel} or $auto_op{$match}->{"*"}))
				{
					cpmsg("Auto-opping $nick in $channel");
					IRC::command_with_server("/mode $channel +o $nick", $server);
				}
			}
		}
	}
}

sub autocycle_checkchan
{
	my ($chan, $serv) = @_;
	my @list = IRC::user_list($chan, $serv);
	my $me = IRC::get_info(1);
	if ($#list <= 9)
	{
		# we get this *before* xchat gets to parse the user parting,
		# so we ignore the person that's not me
		while ($#list >= 0)
		{
			my ($cnick,$caddr,$cop,$cvoice,$cfoo) = splice(@list,0,5);
			next if ($cnick ne $me);
			# don't cycle if I'm opped!
			if ($cop == 0)
			{
				cpmsg("Auto-reop: I'm the last on $chan, no ops, try re-op $chan");
				IRC::command("/leave $chan");
				IRC::command("/join $chan");
			}
		}
	}
}

sub autocycle_pk_handler
{
	return 0 if (not $cycle_dead_channels);

	my $line = join('', @_);
	# auto cycle to regain ops
	$line =~ m/:(.+?)\!(.+?) (PART) (.*)/
		or $line =~ m/:(\S+?)\!(\S+?) (KICK) (\S*) (\S*) :(.*)/;
	my $part_nick;
	if ($3 eq "PART")
		{ $part_nick = $1; }
	else
		{ $part_nick = $5; }
	my $chan = $4;
	my $serv = IRC::get_info(3);
	my $me = IRC::get_info(1);
	# if this is me who leave chan, just quit
	return undef if ($me eq $part_nick);
	autocycle_checkchan($chan, $server);
	return 0;
}

sub autocycle_q_handler
{
	return 0 if (not $cycle_dead_channels);
	
	my $line = join('', @_);
	# auto cycle to re-claim ops
#	$line =~ /:(.+?)\!(.+?) QUIT :(.*)/;
	my @channels = IRC::channel_list();
	# list is in groups of 3: channel, server, nick
	CHANNEL: while (@channels)
	{
		my ($chan, $serv, $nick) = splice(@channels, 0, 3);
		# only process channel items, not server windows
		next CHANNEL unless ($chan =~ /^[#&]/);
		autocycle_checkchan($chan, $server);
	}
	# return (undef) : continue, return 1 : last plugin (hook) !
	return 0;
}

sub request_nick
{
	my ($nick, $server) = splice(@_, 0, 2);
	if (($requested_nick{$server}->{nick} ne $nick) or
			($requested_nick{$server}->{time} + $check_ops_delay < time()))
	{
		IRC::command_with_server("/nick $nick", $server);
		$requested_nick{$server}->{nick} = $nick;
		$requested_nick{$server}->{time} = time();
		return 1;
	}
	return 0;
}

# this does the nick setting for nickgrab stuff
# params: $currentnick, \%nicksonline
sub do_nickgrab
{
	# forget it if we're not interested in grabbing nicks
	if (not $use_nickgrab)
		{ return; }
	my $server = $_[0];
	my $nick = $_[1];
	my %nicksonline = %{$_[2]};
	# don't do nickgrab on dalnet servers
	if ($server =~ /dal.?net/)
		{ return; }
	my $newnick = "";
	foreach (keys %preferrednicks)
	{
		if (
				$preferrednicks{$_} and
				$preferrednicks{$nick} and
				($preferrednicks{$_} < $preferrednicks{$nick})
			)
		{
			if (! $nicksonline{lc($_)})
			{
				$newnick = $_;
			}
		}
	}
	if ($newnick)
	{
		my $tret = request_nick($newnick, $server);
		if ($tret > 0)
		{
			cpmsg("Changing nick from ${nick}($preferrednicks{$nick}) to " .
				"${newnick}($preferrednicks{$newnick})");
		}
	}
}

sub nickgrab_notify_handler
{
	# notify handler
	
	my $line = join('', @_);
	$line =~ m/:(.+) 303 (.+) :(.*)/;
	# lastnicksonline and preferrednicks are globals
	my $server = $1;
	my $nick = $2;
	my %nicksonline = map { lc($_) => 1 } split(/ /, $3);

	do_nickgrab($server, $nick, \%nicksonline);
	
	%{$lastnicksonline{$server}} = %nicksonline;
	# return (undef) : continue, return 1 : last plugin (hook) !
	return 0;
}

sub whois_notify_handler
{
	my $line = join('', @_);
	$line =~ m/:(.+) 303 (.+) :(.*)/;
	# lastnicksonline and preferrednicks are globals
	my $server = $1;
	my $nick = $2;
	my %nicksonline = map { lc($_) => 1 } split(/ /, $3);

	# /whois new online notifies
	if ($whois_new_online_users)
	{
		foreach (keys %nicksonline)
		{
			if (! defined $whoisnicksonline{$server}->{$_})
			{
				cpmsg("Getting WHOIS for new online user $_");
				IRC::command("/whois $_");
			}	
		}
	}

	%{$whoisnicksonline{$server}} = %nicksonline;
	# return (undef) : continue, return 1 : last plugin (hook) !
	return 0;
}

sub nickgrab_quit_handler
{
	# handles grabbing prefered nicks when the owner of that nick quits

	my $line = join('', @_);
	$line =~ /:(.+?)\!(.+?) QUIT :(.*)/;
	my $nick = $1;
	my $mynick = IRC::get_info(1);
	my $server = IRC::get_info(3);
	# copy, not reference!
	my %nicksonline = %{$lastnicksonline{$server}};
	# remove the entry for the quit nick
	delete $nicksonline{lc($nick)};
	# do the nick grab
	do_nickgrab($server, $mynick, \%nicksonline);
	# set the lastnicksonline to the current value
	%{$lastnicksonline{$server}} = %nicksonline;

	# return (undef) : continue, return 1 : last plugin (hook) !
	return 0;
}

sub nickgrab_nickchange_handler
{
	# handles grabbing prefered nicks when the owner of that nick changes
	# their nick

	my $line = join('', @_);
	$line =~ /:(.+?)\!(.+?) NICK :(.*)/;
	my $oldnick = $1;
	my $newnick = $3;
	my $mynick = IRC::get_info(1);
	my $server = IRC::get_info(3);
	# copy, not reference!
	my %nicksonline = %{$lastnicksonline{$server}};
	# remove the entry for the quit nick
	delete $nicksonline{lc($oldnick)};
	# add entry for new nick
	$nicksonline{lc($newnick)} = 1;
	# do the nick grab
	do_nickgrab($server, $mynick, \%nicksonline);
	# set the lastnicksonline to the current value
	%{$lastnicksonline{$server}} = %nicksonline;

	# return (undef) : continue, return 1 : last plugin (hook) !
	return 0;
}

sub isonfail_handler
{
	my $line = join('', @_);
	# ignore ison failure
	if ($line =~ m/:(.+) 421 (.+) ISON :Unknown command/o) {
		return 1
	}
}	

#sub blankline_ignore
#{
#	my $line = join('', @_);
#	return 1 if ($line =~ /^\s*$/);
#}

sub safe_chanmode_handler
{
	return 0 if (not $set_safe_chanmodes);
	
	my $line = join('', @_);
	$line =~ /:(.+) 315 (.+) (.+) :End of \/WHO list./;
	my $nick = $2;
	my $chan = $3;
	# only act for channels
	return 0 unless (($chan =~ /^#/) or ($chan =~ /^&/));
	my $me = IRC::get_info(1);
	my $serv = IRC::get_info(3);
	# check to make sure we're still in the chan
	my @chans = IRC::channel_list();
	my $ok = 0;
	while ($#chans >= 0)
	{
		my ($lchan, $lserv, $lnick) = splice(@chans, 0, 3);
		$ok = 1, last if (($lchan eq $chan) and ($lserv eq $serv));
	}
	return 0 unless ($ok);
	my @users = IRC::user_list($chan, $serv);
	if ((($#users + 1) / 5) < 2)
	{
		# I'm first user, set modes
		cpmsg("First user in $chan, setting modes");
		IRC::command("/mode $chan +stn");
	}
	return 0;
}

sub mode_handler
{
	# :Cheetah!cheetah@cheetah.STUDENT.CWRU.Edu MODE #sctalk -o Cheetah
	my $line = join('', @_);
	my ($nick, $user, $host, $chan, $modes, $extra) =
		($line =~ /:(.+)\!(.+)\@(.+) MODE (\S+) (\S+)(.*)/);
	# clean up extra: remove leading/trailing spaces
	$extra =~ s/^ *(.*) *$/$1/;
	my $modeplus = 1;
	my @modechars = split('', $modes);
	my @extras = split(' ', $extra);
	MODECHAR: foreach (@modechars)
	{
		my $mchg = '';
		my $mparam = '';
		if ($_ eq '+')
			{ $modeplus = 1; }
		elsif ($_ eq '-')
			{ $modeplus = 0; }
		elsif ($_ =~ /[bdhklov]/i)
		{
			if ($modeplus == 1)
				{ $mparam = shift @extras; }
		}
		if (($_ eq 'o') and ($modeplus == 0))
		{
			# cpmsg("checking for reop in $chan");
			# person got deopped, re-check ops, don't piss about delays
			# however, we need to let xchat process the mode first, so we
			# do this with a zero timer
			IRC::command("/timer 1 /checkops");
			# if we sent the check once, we don't need to keep looking ...
			last MODECHAR;
		}
	}
}

sub connect_handler
{
	# because xchat doesn't mark a server as known & connected until AFTER it
	# sees the end of MOTD so ... we catch the 001 numeric, and then add a
	# timer to check everything in a couple seconds, after which xchat should
	# have parsed it all & whatnot because xchat doesn't know server info,
	# etc. yet, we have to parse it from the line.  We wait a few seconds
	# because we don't want to have to worry about the multiple server
	# numerics that exist for end of MOTD, and thus we catch 001, and thus we
	# don't know how long it'll take for the motd to arrive.
	
	my $line = join('', @_);
	my ($server, $nick) = ($line =~ /^:(\S+) 001 (\S+)/);

	IRC::add_timeout_handler(2000, "CIT::check_all_ops");

	# initialize our nick request data
	$requested_nick{$server}->{nick} = $nick;
	$requested_nick{$server}->{time} = time();
	
	return 0;
}

sub unload_handler
{
	save_auto_op() if ($auto_op_init == 1);
	save_bots() if ($bots_init == 1);
	save_chan_bots() if ($chan_bots_init == 1);
	cpmsg("Unloaded Cheetah's IRC Tools $cit_version");
}


$xcbase = IRC::get_info(4);

$auto_op_dat_file = "$xcbase/auto_op.dat";
$bots_dat_file = "$xcbase/bots.dat";
$chan_bots_dat_file = "$xcbase/chan_bots.dat";
%lastnicksonline = ();
%preferrednicks = ();
%whoisnicksonline = ();
$check_ops_delay = 30;
$bot_resp_delay = 15;

# options
$whois_new_online_users = 0; # use x-chat's built-in feature for this instead
$cycle_dead_channels = 1;
$set_safe_chanmodes = 1;
$use_nickgrab = 1;

$cit_version = "0.9";

IRC::register("Cheetah's IRC Tools", $cit_version,
	"CIT::unload_handler", "");
# cpmsg("Loading Cheetah's IRC Tools $cit_version");

IRC::add_message_handler("PART", "CIT::autocycle_pk_handler");
IRC::add_message_handler("KICK", "CIT::autocycle_pk_handler");
IRC::add_message_handler("QUIT", "CIT::autocycle_q_handler");
IRC::add_message_handler("QUIT", "CIT::nickgrab_quit_handler");
IRC::add_message_handler("303", "CIT::nickgrab_notify_handler");
IRC::add_message_handler("303", "CIT::whois_notify_handler");
IRC::add_message_handler("NICK", "CIT::nickgrab_nickchange_handler");
IRC::add_message_handler("421", "CIT::isonfail_handler");
IRC::add_message_handler("315", "CIT::safe_chanmode_handler");
IRC::add_message_handler("001", "CIT::connect_handler");

#IRC::add_message_handler("INBOUND", "CIT::blankline_ignore");

# catch deops and re-op if appropriate
IRC::add_message_handler("MODE", "CIT::mode_handler");

IRC::add_command_handler("ADDAUTOOP", "CIT::add_auto_op");
IRC::add_command_handler("LISTAUTOOP", "CIT::list_auto_op");
IRC::add_command_handler("RELOADAUTOOP", "CIT::load_auto_op");
IRC::add_command_handler("ADDBOT", "CIT::add_bot");
IRC::add_command_handler("LISTBOTS", "CIT::list_bots");
IRC::add_command_handler("RELOADBOTS", "CIT::load_bots");
IRC::add_command_handler("SETCHANBOTS", "CIT::set_chan_bots");
IRC::add_command_handler("LISTCHANBOTS", "CIT::list_chan_bots");
IRC::add_command_handler("RELOADCHANBOTS", "CIT::load_chan_bots");
# this does bot and auto op checking, to remove the need to build a
# channel info list twice
IRC::add_command_handler("CHECKOPS", "CIT::check_ops_cmd");

load_auto_op();
load_bots();
load_chan_bots();
load_nickgrab();

check_all_ops();

cpmsg("Loaded Cheetah's IRC Tools $cit_version");

# make script evaluate to true
1;
