# $Id: .sircrc.pl,v 1.56 2006-09-12 09:09:56-04 roderick Exp roderick $

# XXX
#     - bind ^w delete_previous_word
#     - have autoop follow nick changes
#     - have autoop follow drops, otherwise an oppable person can drop
#       and a different one can take their nick

package mypack;

use strict;
use lib qw(/usr/local/src/pm/sirc/lib /usr/local/src/sirc);
use sigtrap qw(die untrapped normal-signals);

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

# Supply dummy definitions for -c testing.
BEGIN {
    eval "
    	sub main::addhook { }
    " unless $::version || $::version;
}

use FileHandle		  ();
use POSIX		qw(mktime strftime);
use Proc::SyncExec	qw(sync_popen_noshell);
use RS::Handy		qw(data_dump plural shuffle);
use Sirc::Autoop	qw(@Autoop %Autovoice);
use Sirc::Chantrack	qw(%Chan_op %Chan_user chantrack_check chantrack_show);
use Sirc::Kick		qw(kbtmp);
use Sirc::LckHash	  ();
use Sirc::LimitMan	  ();
use Sirc::Silent	  ();
use Sirc::Status	  ();
use Sirc::URL		  ();
use Sirc::Util		qw(add_hook addcmd addhook arg_count_error
			    ban_pattern deltimer describe docommand doset
			    have_ops have_ops_q ieq msg newtimer notice
			    optional_channel
			    remhook settable_boolean settable_int sl tell
			    tell_error tell_question timer userhost
			    xgetarg xtell xrestrict);
use Time::HiRes		  ();

require 'tryexec.pl';

use vars qw($Debug $Init_done $Is_efnet $nickserv_nick);

$nickserv_nick ||= 'nickserv';

if (!$Init_done) {
    srand;
    $Debug = 0;
    settable_boolean('rs_debug', \$Debug);

    $::username = $::nick;
    doset 'ircname', $::nick; #"$::nick (finger for commands)";
    #doset 'finger', 'Commands (via /msg or by saying "<mynick>, <command>"): '
    #	    	    	. ' tune';
    doset 'userinfo', '';
    doset 'eight_bit', 'on';
    doset 'url_browser', 'browse-url %s &';

    docommand '^alias autovoice /reval show \%Autovoice';
    docommand '^alias finger ctcp $0 finger';
    docommand '^alias google /m purl google for';
    docommand '^alias n names';
    docommand '^alias pe /msg #perl';
    docommand '^alias perltopic topic #perl'
    	    	. ' No WWW/CGI/FAQ questions, bold colons, public aways,'
    	    	. ' kewlt0k, u/ne1/plz, or singing.';
    docommand '^alias showdis /reval show \@disconnect';
    docommand '^alias weather /m purl weather kcho';

    @::listenport = (30000..30100);

    @Sirc::Status::Status_element = @Sirc::Status::Status_element_compact;

    $nickserv_nick ||= 'nickserv';
}

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

# Disable standard bolding.

sub main::tell {
    my $s = shift;
    $s =~ s/^\cb(\*.\*)\cb/$1/;
    $s =~ s/^\*\cb([^E])\cb\*/*$1*/;
    $::silent || main::print($s);
}
*Sirc::Util::tell = \&main::tell;

sub debug {
    xtell 'debug ' . join '', @_
	if $Debug;
}

sub show {
    xtell 'show ' . data_dump @_;
}

sub main::hook_rs_raw {
    xtell "from $::who!$::user\@$::host @_";
}

#sub do_disconnect {
#    xtell "Disconnecting from $::server";
#    close $::S;
#    delete $::buffer{$::S};
#    $::connected = 0;
#    &::dohooks("disconnect");
#}
#
#sub main::sl {
#    no strict 'refs';
#    if (!print $::S $_[0]."\n") {
#	tell_error "Error writing to server: $! ($_[0])";
#	do_disconnect;
#    }
#}
#*Sirc::Util::sl = \&main::sl;

# simple commands and command redefinitions -----------------------------------

sub main::cmd_quit {
    $::args ||= $::nick;
    docommand "/quit $::args";
}
addcmd 'quit';
docommand '^alias bye quit';
docommand '^alias exit quit';
docommand '^alias signoff quit';

sub main::cmd_list {
    optional_channel or return;
    docommand "/list $::args";
}
addcmd 'list';
docommand '^alias l list';

sub main::cmd_names {
    optional_channel or return;
    docommand "/names $::args";
}
addcmd 'names';

sub main::cmd_reload_module {
    my @module = split ' ', $::args;
    if (!@module) {
	tell_question 'No modules specified';
	return;
    }

    for my $module (@module) {
	my $file = $module;
	$file =~ s-::-/-g;
	$file .= '.pm';
	if (!defined do $file) {
	    tell_error "Error loading $module: "
		. ($@ ? $@ : $! ? $! : 'did not return a defined value');
	}
    }
}
addcmd 'reload_module';

# Reverse /msg.

sub main::cmd_rev {
    optional_channel or return;
    my ($channel, $s) = split ' ', $::args, 2;
    msg $channel, reverse $s;
}
addcmd 'rev';

sub main::cmd_think {
    optional_channel or return;
    my ($channel, $s) = split ' ', $::args, 2;
    describe $channel, ". o O ($s)";
}
addcmd 'think';

#sub mode_runner {
#    my (@mode) = @_;
#    unless (@mode <= 4) {
#	tell_error "Too many modes passed to mode_runner (@mode)";
#	return;
#    }
#    my $atonce = int(4 / @mode);
#
#    optional_channel or return;
#    my ($channel, @nick) = split ' ', $::args;
#
#    while (@nick) {
#	my @this = splice @nick, 0, $atonce;
#	my @mode = @mode;
#	my $mode = '';
#	my $dir = '';
#	for (1..$atone) {
#	    $
#	    $mode .=shift @mode;
#	my $dir = $mode =~ /^-/;
#
#	$mode = '';
#	while (@this_nick < 4) {
#	    $mode .= join '', @mode;
#

sub voice {
    my $direction = shift;
    optional_channel or return;
    my ($channel, @nick) = split ' ', $::args;
    while (@nick) {
	my @this = splice @nick, 0, 4;
	docommand  "mode $channel $direction" . 'v' x @this . " @this";
    }
}

sub main::cmd_voice {
    voice '+';
}
addcmd 'voice';

sub main::cmd_devo {
    voice '-';
}
addcmd 'devo';

sub main::cmd_nick {
    my @arg = split ' ', $::args;

    if (@arg != 1) {
	tell_question '/nick takes 1 arg';
	return;
    }
    elsif (length $arg[0] > 9) {
	tell_question 'New nick too long';
	return;
    }
    else {
	sl "nick $arg[0]";
    }
}
addcmd 'nick';

sub now_playing {
    my ($response_to) = @_;
    my ($r);

    if (open TUNE, "$ENV{HOME}/.xmms/now-playing") {
	my $tune = <TUNE>;
	chomp $tune;
	if ($tune ne '') {
	    $r = "Tune spinning:  $tune";
	    my $since = (stat TUNE)[9];
	    if ($since) {
		my $diff = time - $since;
		if ($diff > 60 * 60) {
		    $r .= " (for the last " . elapsed($diff) . ")";
		}
	    }
	}
	close TUNE;
    }

    if (!defined $r) {
	$r = "I can't figure out what's playing.";
    }

    notice $response_to, $r;
}

sub maybe_command_me {
    my ($response_to, $cmd) = @_;

    $cmd =~ s/^\s+//;
    $cmd =~ s/\s*\?*\s*$//;
    $cmd = lc $cmd;

    if ($cmd eq 'help') {
	notice $response_to,
	    qq(Try "/finger $::nick".)
	    . "  PS:  I'm not actually a bot, it's just a souped-up sirc."
    }
    elsif ($cmd eq 'tune') {
	now_playing $response_to;
    }
    else {
	return;
    }

    if ((!ieq($::who, $::nick) || !ieq($response_to, $::nick))
	    && open CMD_LOG, ">>$ENV{HOME}/.sirc.cmdlog") {
	printf CMD_LOG "%s from %-9s to %-9s %s\n",
	    strftime('%Y-%m-%d %H:%M:%S', localtime),
	    $::who, $response_to, $cmd;
	close CMD_LOG;
    }
}

sub main::hook_rs_command_me_public {
    my ($channel, $msg) = @_;

    $msg =~ s/^\s*\Q$::nick\E[,:;\s]+//i
	or return;
    maybe_command_me $channel, $msg;
}
addhook 'public', 'rs_command_me_public';

sub main::hook_rs_command_me_msg {
    my ($msg) = @_;
    maybe_command_me $::who, $msg;
}
addhook 'msg', 'rs_command_me_msg';

sub blah {
    my $fmt = shift;
    $::args =~ /\S/ || optional_channel || return;
    my @arg = split ' ', $::args;
    unless (@arg == 1) {
	tell_question "0 or 1 args expected, got [$::args]";
	return;
    }
    my ($channel) = @arg;

    my $word = `word`;
    if ($? || $word !~ /\S/) {
	tell_error "Can't get word (return $?)";
	return;
    }
    chomp $word;
    describe $channel, sprintf $fmt, ucfirst $word;
}

for ([blah => 'exclaims, "%s!"'],
    	['blah?' => 'asks, "%s?"'],
    	['blah.' => 'states, "%s."'],
    	['blah!' => 'screams, "%s!!!"']) {
    my ($name, $fmt) = @$_;
    no strict 'refs';
    *{ "main::cmd_$name" } = sub { blah $fmt, @_ };
    addcmd $name;
}

sub rot13 {
    local $_ = shift;
    tr/a-zA-Z/n-za-mN-ZA-M/;
    return $_;
}

sub main::cmd_rot13 {
    tell "rot13: ", rot13 $::args;
}
addcmd 'rot13';

# main::print() enhancements --------------------------------------------------

use vars qw($do_nohighlight $do_timestamp
	    $do_silentstamp $silentstamp_timer
	    $old_print);

unless ($Init_done) {
    settable_boolean 'rs_nohighlight',		\$do_nohighlight;
    settable_boolean 'rs_timestamp',		\$do_timestamp;
    settable_int 'rs_silentstamp', \$do_silentstamp, sub { $_[1] >= 0 };
}

BEGIN {
    $old_print = \&main::print;
}

sub main::print {
    if ($do_silentstamp) {
	my $s = localtime;
	timer $do_silentstamp, sub {
	    local $do_silentstamp = 0;
	    xtell $s;
	}, $silentstamp_timer ||= newtimer;
    }
    my @arg = @_;
    $arg[0] =~ s/\cC\d{0,2}(,\d{1,2})?//g;
    $arg[0] = sprintf "%02d:%02d:%02d %s", (localtime)[2,1,0], $arg[0]
	if $do_timestamp;
    $arg[0] =~ tr/\cB\cV\037//d
	if $do_nohighlight;

    return $old_print->(@arg);
}

use vars qw($Log_format $Log_rotate_timer);
$Log_format = "sirc.%Y-%m-%d.log" unless $Init_done;

sub log_rotate {
    $::logfile = strftime $Log_format, localtime;

    my $dir = '';
    my @part = split /\//, $::logfile;
    pop @part;
    for (@part) {
	$dir .= '/' . $_;
	unless (mkdir($dir, 0777) || $! == POSIX::EEXIST()) {
	    tell_error "Not rotating log, can't mkdir $dir ($!)";
	    return;
	}
    }

    docommand "set log on";
    my @then = localtime;
    @then[0..2] = (15, 0, 0);
    $then[3]++;
    my $then = mktime @then;
    timer $then - time, \&log_rotate, $Log_rotate_timer ||= newtimer;
    debug "new log file $::logfile, next change at ", scalar localtime $then;
}

# various kinds of audible signals --------------------------------------------

use vars qw($beep $beep_fh);

if (!$Init_done) {
    $beep = 1;
    settable_boolean 'rs_beep', \$beep;
}

sub beep {
    my ($type) = @_;

    return unless $beep;

    my $dir = '/usr/local/share/sound';
    my $file = $type > 1
		? 'quake/enforcer-sight3.wav'
		: 'quake/items-r_item2.wav';
    system "desktop-sound \Q$dir/$file\E";
    return if $? == 0;

    if (!$beep_fh) {
	unless (open BEEP_TTY, '>/dev/tty') {
	    tell_error "Disabling beeping, can't open /dev/tty ($!)";
	    doset 'rs_beep', 'off';
	    return;
	}
	my $old = select BEEP_TTY;
	$| = 1;
	select $old;
	$beep_fh = 1;
    }
    unless (print BEEP_TTY "\a") {
	tell_error "Disabling beeping, can't write to /dev/tty ($!)";
	doset 'rs_beep', 'off';
    }
}

BEGIN {
    *bell = \&beep;
}

# message logging -------------------------------------------------------------

use vars qw($msg_log_active @msg $msg_file $msg_opened);

sub log_msg_ignore {
    my ($who, $msg) = @_;

    return 1 if ieq($who, $::nick)
		    # infobots
		    || ieq($who, 'a')
		    || ieq($who, 'b')
		    || ieq($who, 'bot')
		    || ieq($who, 'chanserv')
		    || ieq($who, 'cpan')
		    || ieq($who, 'i-bot')
		    || ieq($who, 'laotse')
		    || ieq($who, $nickserv_nick)
		    || ieq($who, 'purl')
}

sub log_msg {
    my ($dir, $who, $msg) = @_;

    return if !$msg_log_active;
    return if log_msg_ignore $who, $msg;

    my $s = sprintf "%s %s %-9s %s\n",
		strftime('%Y-%m-%d %H:%M:%S', localtime),
		$dir, $who, $msg;
    push @msg, $s;
    main::dostatus;

    if (!$msg_opened) {
	if (open MSG, ">>$msg_file") {
	    $msg_opened = 1;
	    my $old = select MSG;
	    $| = 1;
	    select $old;
	}
	else {
	    tell_error "Disabling message logging, can't append to $msg_file ($!)";
	    $msg_opened = -1;
	}
    }

    return if $msg_opened < 0;

    unless (print MSG $s) {
	tell_error "Error writing to $msg_file ($!)";
	close MSG;
	$msg_opened = undef;
    }
}

sub main::hook_rs_msglog_send {
    my ($who, $msg) = @_;
    log_msg '>', $who, $msg unless $who =~ /^[#&]/;
}

sub main::hook_rs_msglog {
    my ($msg) = @_;
    log_msg '<', $::who, $msg;
}

sub main::hook_rs_msglog_status {
    $_[0] .= " " . (0+@msg) . " message" . plural 0+@msg
	if @msg;
}

if (!$Init_done) {
    $msg_log_active = 1;
    $msg_file = "$ENV{HOME}/.sirc.msglog";
    addhook 'send_text', 'rs_msglog_send';
    addhook 'msg', 'rs_msglog';
    addhook 'status', 'rs_msglog_status';
    docommand '^alias msgdel /reval @msg = (); main::dostatus';
    docommand '^alias msgshow /reval xtell "message log:"; print @msg';
}

# talk private messages and public ones which mention my name -----------------

use vars qw($festival $do_festival);

if (!$Init_done) {
    $do_festival = 1;
    settable_boolean 'rs_talk', \$do_festival;
}

sub say {
    my ($type, $s) = @_;

    return unless $do_festival;
    beep $type;
    return;

    unless (defined $festival) {
	require Festival::Client;
	unless ($festival = Festival::Client->new) {
	    tell_error "Error running festival: $!";
	    $festival = '';
	}
    }

    if ($festival) {
	local $SIG{PIPE} = 'IGNORE';
	local $SIG{__WARN__} = sub {
	    warn $_[0] unless $_[0] =~ /^Error writing to festival server:/;
	};
	if (!$festival->say($s)) {
	    #tell_error "Error writing to festival: $!";
	    # Try to restart and say it again.
	    undef $festival;
	    say($s);
	}
    }
}

sub maybe_say {
    my $s = shift;
    say 1, $s if $s =~ /roderick|\brod\b|\broder\b/i;
}

sub main::hook_rs_speak_msg {
    my ($msg) = @_;
    say 2, $msg unless log_msg_ignore($::who, $msg) || $msg =~ /^\s*autovoice\b/i;
}
addhook 'msg', 'rs_speak_msg';

sub main::hook_rs_speak_public {
    my ($channel, $msg) = @_;
    maybe_say $msg;
}
addhook 'public', 'rs_speak_public';

sub main::hook_rs_speak_action {
    my ($dest, $action) = @_;
    maybe_say "$::who $action";
}
addhook 'action', 'rs_speak_action';

# slightly improved /ignoring -------------------------------------------------

# The standard one isn't case insensitive!
BEGIN { undef &main::ignored }
sub main::ignored {
  foreach (@::ignore) {
    return 1 if $_[0] =~ /^${_}$/i;
  }
  return '';
}

# This /ignore supplies a pattern if you use a nick.  XXX add escape mechanism

sub main::cmd_ignore {
    my $spec = xgetarg;
    if ($spec ne '' && $spec ne '-' && $spec !~ /[!@]/) {
	my $remove = $spec =~ s/^-// ? '-' : '';
	userhost $spec, sub {
	    my $pat = ban_pattern $::who, $::user, $::host;
	    docommand "/ignore $remove$pat";
	}, sub {
	    docommand "/ignore $remove$spec";
	}
    }
    else {
	docommand "/ignore $spec";
    }
}
addcmd 'ignore';
docommand '^alias ig ignore' if !$Init_done;

# This can't hook msg because that never runs for ignored messages.

my $ignore_notice_verbose;
if (!$Init_done) {
    $ignore_notice_verbose = 1;
    settable_boolean 'rs_ignore_notice_verbose', \$ignore_notice_verbose;
}

sub main::hook_rs_send_ignore_notice {
    my $cmd = shift;
    return unless $cmd eq 'PRIVMSG';
    my ($dest, $rest) = split ' ', shift, 2;
    return unless ieq $dest, $::nick;
    #return if $rest =~ /^:\001/;	# don't send for CTCP
    return unless main::ignored "$::who!$::user\@$::host";
    xtell "ignored: $::who!$::user\@$::host $rest"
	if $ignore_notice_verbose;
    sl "NOTICE $::who :You are being ignored.";
}
addhook 'raw_irc', 'rs_send_ignore_notice';

# kick people who send public auto-away messages ------------------------------

use vars qw($debug_autoaway_kick %autoaway_kick_watch);
$debug_autoaway_kick = 0;

sub debug_autoaway_kick {
    xtell 'aak ' . join '', @_
	if $debug_autoaway_kick;
}

sub main::hook_rs_autoaway_kick_watch {
    my ($nick, $channel, $text) = @_;

    debug_autoaway_kick "seeing kick of $nick on $channel";
    $nick = lc $nick;
    $channel = lc $channel;
    $autoaway_kick_watch{$channel}{$nick} = 1
	if exists $autoaway_kick_watch{$channel}{$nick};
}
addhook 'kick', 'rs_autoaway_kick_watch';

sub detect_autoaway {
    my ($nick, $channel, $text) = @_;

    return unless $Chan_op{$channel}{$::nick};
    return if $Chan_op{$channel}{$nick};
    return unless $Is_efnet && lc($channel) eq '#perl';

    return if $text !~ /\b((auto.?)?(away|gone|dead))\b/i;
    return if !defined $2
	    	&& $text !~ /\b(
				auto(.?away|ma[tg]ic(ally)?)?
			       |idle
			       |inactiv(e|ity)
    	    	    	    )\b/xi;
    return if $text =~ /\b(not|non|isn.?t)\b/i;	# "not automatic"

    if (exists $autoaway_kick_watch{lc $channel}{lc $nick}) {
	debug_autoaway_kick "skipping delayed kick for $nick on $channel";
    }
    else {
	debug_autoaway_kick "scheduling delayed kick for $nick on $channel";
	$autoaway_kick_watch{lc $channel}{lc $nick} = 0;
	timer 30, sub {
	    if (delete $autoaway_kick_watch{lc $channel}{lc $nick}) {
		debug_autoaway_kick "skipping kick of $nick on $channel";
	    }
	    else {
		kbtmp $channel, $nick, "Please disable your public auto-away.";
		notice $nick,
		    'The ban is temporary, to thwart an auto-rejoin.'
		    . '  You can rejoin the channel in 10 seconds.'
    	    	    . '  To disable Bitchx\'s public away, put'
		    . ' "/set send_away off" in your ~/.bitchxrc.'
	    }
	};
    }
}

sub main::hook_rs_autoaway_action {
    my ($dest, $action) = @_;
    detect_autoaway $::who, $dest, $action if 1;
}
addhook 'action', 'rs_autoaway_action';

# kick for auto-rejoin --------------------------------------------------------

use vars qw($autorejoin_kick_debug
	    $autorejoin_kick
	    $autorejoin_kick_threshold
    	    $autorejoin_kick_clean_interval
	    %autorejoin_kick_time
    	    %autorejoin_kick_channel);

if (!$Init_done) {
    $autorejoin_kick_debug		= 0;
    $autorejoin_kick			= 1;
    $autorejoin_kick_threshold		= 0.75;
    $autorejoin_kick_clean_interval	= $autorejoin_kick_debug ? 15 : 300;
    tie %autorejoin_kick_time, 'Sirc::LckHash';
    tie %autorejoin_kick_channel, 'Sirc::LckHash';

    settable_boolean 'rs_autorejoin_kick_debug', \$autorejoin_kick_debug;
    settable_boolean 'rs_autorejoin_kick', \$autorejoin_kick;
    settable_int
	'rs_autorejoin_kick_threshold',
	\$autorejoin_kick_threshold,
    	sub { $_[1] > 0 };
}

sub debug_autorejoin_kick {
    xtell 'ark ' . join '', @_
	if $autorejoin_kick_debug;
}

sub main::hook_rs_autorejoin_kick_kick {
    my ($nick, $channel, $text) = @_;
    return unless $autorejoin_kick_channel{$channel};
    return unless $Chan_op{$channel}{$::nick};
    my $now = Time::HiRes::time;
    $autorejoin_kick_time{"$channel $nick"} = $now;
    debug_autorejoin_kick "kick of $nick on $channel at $now";
}
addhook 'kick', 'rs_autorejoin_kick_kick';

sub main::hook_rs_autorejoin_kick_join {
    return unless $autorejoin_kick;
    my $channel = shift;
    return unless $autorejoin_kick_channel{$channel};
    return unless $Chan_op{$channel}{$::nick};
    my $nick = $::who;
    my $kick_time = $autorejoin_kick_time{"$channel $nick"};
    return unless $kick_time;
    my $now = Time::HiRes::time;
    my $diff = $now - $kick_time;
    debug_autorejoin_kick "join of $nick at $now (diff $diff)";
    return if $diff > $autorejoin_kick_threshold;
    kbtmp $channel, $nick, '10 second ban to thwart auto-rejoin';
}
addhook 'join', 'rs_autorejoin_kick_join';

sub autorejoin_clean {
    timer $autorejoin_kick_clean_interval, \&autorejoin_clean;
    my $keep_time = Time::HiRes::time - $autorejoin_kick_threshold * 100;
    my @old = grep { $autorejoin_kick_time{$_} < $keep_time }
    	    	keys %autorejoin_kick_time;
    return unless @old;
    debug_autorejoin_kick "cleaning @old";
    delete @autorejoin_kick_time{@old};
}
autorejoin_clean;

# For testing only, really!

sub main::hook_rs_autorejoin {
    my ($nick, $channel, $text) = @_;
    return unless lc($nick) eq lc($::nick);
    sl "join $channel";
}

# re-take an in-use nick ------------------------------------------------------

my $retake_nick;
my $retake_nick_time = 60;
my $retake_nick_timer;

sub main::hook_rs_retake_nick_taken {
    my $curnick = xgetarg;
    my $badnick = xgetarg;
    $::skip = 1 if defined($retake_nick) && $badnick eq $retake_nick;
}
addhook '433', 'rs_retake_nick_taken';

sub retake_nick {
    $retake_nick = shift if @_;
    if ($retake_nick eq '' || $::nick eq $retake_nick) {
	$retake_nick = undef;
	return;
    }
    sl "NICK $retake_nick";
    timer $retake_nick_time, \&retake_nick, $retake_nick_timer ||= newtimer;
}

sub main::cmd_retake_nick {
    retake_nick $::args;
}
addcmd 'retake_nick';

sub main::hook_retake_purl_msg {
    notice $::who,
	"I am not purl, I am squatting on the name until she shows up.";
}

sub main::cmd_retake_purl {
    $beep = 0;
    $msg_log_active = 0;
    retake_nick 'purl';
    addhook 'msg', 'retake_purl_msg';
}
addcmd 'retake_purl';

# LIZ_PHAIR++ spam ------------------------------------------------------------

sub elapsed {
    my $s = shift;
    my ($m, $i);

    die if $s < 0;
    return '0 seconds' if $s == 0;
    for ([60*60*24, 'day'], [60*60, 'hour'], [60, 'minute'], [1, 'second']) {
	my ($secs, $name) = @$_;
	my $i = int($s / $secs);
	if ($i) {
	    $s -= $i * $secs;
	    $m .= sprintf ", %d %s%s", $i, $name, plural $i;
	}
    }
    return substr $m, 2;
}

use vars qw($do_phair $phair_file $phair_threshold);
if (!$Init_done) {
    $phair_file = "$ENV{HOME}/.sirc.phair";
    $phair_threshold = 60*60*24*1;
    $do_phair = 0;
    settable_boolean 'rs_phair', \$do_phair;
}

sub main::hook_rs_phair {
    return unless $do_phair;
    my $cmd = shift;
    return unless $cmd eq 'PRIVMSG';
    my ($dest, $msg) = split ' ', shift, 2;
    return if ieq $dest, $::nick;
    return unless $msg =~ /\bLiz[\s_]*Phair\s*\)?\s*\+\+/i;
    my $now = time;
    debug "seeing phair at $now";
    if (open PHAIR, "+<$phair_file") {
	my $last = <PHAIR>;
	chomp $last;
	if ($last > 0) {
	    my $w = $::who;
	    $w = 'skrewtape' if ieq $w, 'purl';
	    my $diff = $now - $last;
	    debug "phair difference $diff (" . elapsed($diff) . ")";
	    if ($diff < $phair_threshold) {
		msg $dest, "$w-- " . elapsed($diff)
		    . " since last increment, show some restraint!";
	    }
	    elsif (0) {
		msg $dest,
		    "$w++ " . elapsed($diff) . " since last increment";
	    }
	}
	unless (seek(PHAIR, 0, 0) && truncate(PHAIR, 0)) {
	    tell_error "can't rewind/truncate $phair_file: $!";
	    close PHAIR;
	    return;
	}
    }
    elsif (!open PHAIR, ">$phair_file") {
	tell_error "can't write to $phair_file: $!";
	return;
    }
    print PHAIR "$now\n"	or tell_error "can't print to $phair_file: $!";
    close PHAIR			or tell_error "error closing $phair_file: $!";
}
addhook 'raw_irc', 'rs_phair';

# op me via an Eggdrop --------------------------------------------------------

# This is controlled via %autoop_self.  Its keys are channels, values are
# references to arrays of references of arrays of (bot, userhost_pattern,
# password) tuples.  Eg:
#
#    %autoop_self = (
#        '#perl' => [
#	    ['rookery',  '^rookery@fishing.w3works.com$', $pw],
#	    ['sanguine', '^sanguine@adsl-.*.bellatlantic.net$', $pw],
#    	 ],
#    );

use vars qw(%autoop_self);

if (!$Init_done) {
    tie %autoop_self, 'Sirc::LckHash';
}

# Try to autoop me on $CHANNEL.

sub try_autoop_self {
    my ($c, $r) = @_;
    my ($bot, $uh_pat, $password, $real_nick) = @$r;

    if ($Chan_op{$c}{$::nick} ) {
	debug "skipping $c/$bot op, already opped";
    }
    elsif (!$Chan_user{$c}{$bot}) {
	debug "skipping $c/$bot op, bot not present";
    }
    elsif (!$Chan_op{$c}{$bot}) {
	debug "skipping $c/$bot op, bot not opped";
    }
    else {
	userhost $bot, sub {
	    if ("$::user\@$::host" !~ /$uh_pat/) {
		tell_error "userhost for bot $bot ($::user\@$::host)"
			    . " doesn't match pattern ($uh_pat)";
	    }
	    else {
		debug "requesting op from $bot";
		local $msg_log_active = 0;
		# ident every time since I use TOR
		msg $bot, "ident $password $real_nick"
		    if defined $real_nick;
		msg $bot, "op $password";
	    }
	};
    }
}

sub autoop_self {
    if (@_ != 2) {
	tell_question "0 or 1 args expected, got [$::args]";
	return;
    }
    my ($c, $nick) = @_;
    return if $Chan_op{$c}{$::nick};
    return unless $autoop_self{$c};
    for my $r (@{ $autoop_self{$c} }) {
	next unless ieq $nick, $r->[0];
	debug "will try to autoop on $c via $nick";
	timer 5, sub { try_autoop_self $c, $r };
    }
}

# Op myself when the bot gets opped.  I used to try to op myself when I
# joined, too, but +op runs in that case.

add_hook '+op', \&autoop_self;

sub main::cmd_autoop_self {
    optional_channel or return;
    my ($channel, @nick) = split ' ', $::args;
    if (!@nick) {
    	my $rl = $autoop_self{$channel};
	@nick = map { $_->[0] } @$rl if $rl;
	if (!@nick) {
	    tell_question "no bots known on $channel";
	    return;
	}
    }
    debug "try autoop on $channel with bots: @nick";
    autoop_self $channel, $_ for @nick;
}
addcmd 'autoop_self';

# reconnect when disconnected -------------------------------------------------

use vars qw(@server_list @disconnect $connect_time);

$connect_time = 0 if !$Init_done;

{
my $reconnect_active = 0;
sub reconnect {
    return if $reconnect_active;
    $reconnect_active = 1;

    push @disconnect, "from $::server at " . localtime;
    unshift @server_list, $::server0
	unless grep { lc($_) eq lc($::server0) } @server_list;

    # Always start at the beginning to get preferred servers when
    # possible.
    my @s = @server_list;

    # If I connected then disconnect right away, though (full server or
    # the like), start with the next server.
    if (time - $connect_time < 15) {
	for (my $i = 0; $i < $#s; $i++) {
	    if ($s[$i] eq $::server) {
		push @s, splice @s, 0, 1+$i;
		last;
	    }
	}
    }

    xtell "Looping through servers: @s";
    while (!$::connected) {
	for (@s) {
	    docommand "server $_";
	    if ($::connected) {
		$connect_time = time;
		last;
	    }
	}
    }

    $reconnect_active = 0;
} }

sub main::hook_rs_disconnect {
    reconnect;
}
addhook 'disconnect', 'rs_disconnect';

# Sometimes you'll get disconnected and reconnect before the network
# realizes you're gone, so your old self will be there taking your nick.
# That causes sirc to do a blocking prompt, which prevents the reconnect
# from going through, so I choose a semi-random nick instead.

{

my $orig_nick;
use vars qw(%nick_pref);
%nick_pref = (
    roderick	=> 'rs',
    rs		=> 'ROSCH',
);

sub main::hook_rs_nick_taken {
    return if $::connected > 1;
    my $bad = $::nick;
    $orig_nick = $bad if !defined $orig_nick;
    if (exists $nick_pref{lc $bad}) {
	$::nick = $nick_pref{lc $bad}
    }
    else {
	$::nick = $orig_nick;
	chop $::nick while length $::nick > 7;
	$::nick .= '-' . ('A' .. 'Z')[rand 26];
    }
    xtell "Nick `$bad' already taken, trying `$::nick'";
    sl "NICK $::nick";
    main::dostatus();
    docommand "retake_nick $orig_nick";
    $::skip = 1;
}

}
addhook '433', 'rs_nick_taken';

sub from_server_desc {
    $::who eq $::myserver ? '' : " (from $::who)";
}

sub main::hook_rs_cannot_join_channel {
    my $nick = xgetarg;
    my $channel = xgetarg;
    $::args =~ s/^://;

    $::skip = 1;
    tell_question "Can't join $channel: $::args" . from_server_desc
		    . " (rejoin queued)";

    timer 60, sub {
	docommand "join $channel" if !$Chan_user{$channel};
    };
}
addhook '437', 'rs_cannot_join_channel'; # "Nick/channel is temp. unavailable"
addhook '471', 'rs_cannot_join_channel'; # channel full
# XXX You need a way to kill an existing rejoin.  When you have it you
# can add these:
#addhook '473', 'rs_cannot_join_channel'; # invite-only
#addhook '474', 'rs_cannot_join_channel'; # banned
#addhook '475', 'rs_cannot_join_channel'; # key

# I might lose my connection but not notice it.  Watch for server
# messages.  If none come for a long time, send something to the
# server.
#
# This used to be all I needed, as the TCP socket would break.  When
# using TOR this isn't always the case, so if there's still nothing from
# the server after a while, disconnect.

{

my $last_receive		= time;
my $last_receive_interval	= 60;
my $last_receive_timeout	= 60 * 2;
my $last_receive_break		= 60 * 10;

sub main::hook_rs_last_receive {
    $last_receive = time;
}
addhook 'raw_irc', 'rs_last_receive';

sub main::hook_rs_ignore_time {
    debug 'ignoring time response';
    remhook '391', 'rs_ignore_time';
    $::skip = 1;
}

sub test_last_receive {
    my $s = time - $last_receive;
    if (!$::connected) {
	debug "test_last_receive $s not connected";
	reconnect;
    }
    elsif ($s > $last_receive_break) {
	tell_error "No data from server in $s seconds, disconnecting";
	docommand "disconnect";
    }
    elsif ($s > $last_receive_timeout) {
	debug "test_last_receive $s sending TIME";
	addhook '391', 'rs_ignore_time';
	timer 15, sub { remhook '391', 'rs_ignore_time' };
	sl 'TIME';
    }
    timer $last_receive_interval, \&test_last_receive;
}
timer $last_receive_interval, \&test_last_receive;

}

# Set nick but only if it's currently the default.

sub config_nick {
    my ($nick) = @_;

    $::nick = $nick
	if $::nick eq ($ENV{SIRCNICK} || $ENV{IRCNICK} || $ENV{LOGNAME});
}

sub config_anonymous {
    my ($nick) = @_;

    config_nick $nick;
    %nick_pref = ();
    doset 'ircname', $::nick;
    $::username = $::nick;
}

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

# Work around sirc bug which causes it not to send all the /userhosts on
# join (or something) by sending other data to help flush them through.

use vars qw($send_notices_count $send_notices_timer);

sub send_notices {
    notice $::nick, ++$send_notices_count;
    timer 4, \&send_notices, $send_notices_timer;
}

sub main::cmd_send_notices {
    if ($send_notices_count) {
	xtell "Disabling notice send";
	$send_notices_count = 0;
	deltimer $send_notices_timer;
    }
    else {
	$send_notices_timer = newtimer;
	send_notices;
    }
}
addcmd 'send_notices';

# remaining initializations ---------------------------------------------------

use vars qw(@auto_join $nickserv_pw);

# XXX Isn't there a better way to do this than hooking into motd?
# ircii provides /on connect.

sub main::hook_rs_motd {
    docommand "mode $::nick +i";
    for (@auto_join) {
	docommand "join $_"
	    if $::nick !~ /^xxx/i;
    }
    msg $nickserv_nick, "identify $nickserv_pw"
	if defined $nickserv_pw;
}
addhook '376', 'rs_motd';

if (!$Init_done) {
    use vars qw(@efnet_ignore);
    do '.sircrc.pw';
    tell_error "error reading .sircrc.pw: $@"
	if $@;

    doset 'rs_nohighlight',	'on';
    doset 'rs_silentstamp',	300;

    $::server = undef
	if $::server eq 'irc.primenet.com' || $::server eq 'irc.debian.org';

    if ($::server =~ /\.(freenode\.net|openprojects\.net|debian\.org)$/i) {
    	config_freenode();
	# daily logs
	$Log_format = "$ENV{HOME}/tmp/log/freenode/%Y/%Y-%m-%d.log";
	log_rotate;
    }

    if ($::server =~ /\.dapcentral\.org$/i) {
	config_dapcentral();
    }

    elsif ($::server =~ /^jones(\.argon\.org)?$/i && $::port == 6668) {
	@auto_join = ('#mis');
    }

    elsif ($::server =~ /^(jones(\.argon\.org)?|localhost)$/i) {
	@auto_join = ('#infobot');
	doset 'silent', 'off';
    }

    elsif ($::server =~ /\.dal\.net$/i) {
    	config_dalnet();
    }

    elsif (ieq($::server, 'irc.infobot.org')
	   || ieq($::server, 'irc.perl.org')
	   || ieq($::server, 'irc.rhizomatic.net')
	   || ieq($::server, 'irc.grou.ch')
	   || ieq($::server, 'rhizo.mati.ca')) {
	config_magnet();
    }

    elsif (defined $::server) {
    	config_anonymous $::nick;
    }

    else {
	$Is_efnet = 1;
	$autorejoin_kick_channel{'#perl'} = 1;
	@auto_join = ('#poe', '#pound-perl.pm', '#perl');
	docommand "limitman #perl";
	@server_list = shuffle qw(
	    irc.easynews.com
	    irc.choopa.net
    	    irc.mindspring.com
	    irc.servercentral.net
	    irc.blackened.com
	    irc.mzima.net
	    irc.blessed.net
	    irc.nac.net
	    irc.dks.ca
	);
	# needs ident:
	    # irc.easynews.com
	    # irc.choopa.net
	# didn't work with tor:
    	    # irc.mindspring.com
	    # irc.servercentral.net
	    # irc.blackened.com
	    # irc.mzima.net
	    # irc.blessed.net
	    # irc.nac.net
	    # irc.dks.ca
	    # irc.kagmir.ca
	    # irc.igs.ca
	    # irc.he.net
	    # irc.colosolutions.net
	# old:
	    # irc.arcti.ca
	    # irc.mpls.ca
	    # irc.aloha.net
	    # irc.vrfx.com
	    # irc.qeast.net
	    # irc.arcti.ca
	    # irc.colorado.edu
	    # irc.east.gblx.net
	    # irc.west.gblx.net
	    # irc.carrier1.net.uk
	    # irc.rt.ru
	    # irc.etsmtl.ca
	    # irc.lightning.net	no connection
	    # irc.prison.net	no idlers

	push @::ignore, @efnet_ignore;
	$::server0 = $::server = $server_list[0];

	# daily logs
	$Log_format = "$ENV{HOME}/tmp/log/efnet/%Y/%Y-%m-%d.log";
	log_rotate;

	# current participants and their delays:
	#      2 me
	#      6 a-mused
	#	   8 Masque
	#     12 petey
	#     15 Fletch_
	#
	# config for new people:
	#     use Sirc::Autoop qw(%Autovoice);
	#     $Autovoice{'#perl'} = undef;
	#     doset 'autovoice_delay', <number>;
	$Autovoice{'#perl'} = undef;
	doset 'autovoice_delay', 2;
    }
}

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

# Put this last so it can see all lexicals.  XXX There's a bug in Perl
# which prevents this from actually seeing the lexicals, so I've mostly
# switched to using globals instead.

sub main::cmd_reval {
    debug "reval $::args";
    return if xrestrict;
    eval $::args;
    if ($@) {
	chomp $@;
	tell_error "eval error: $@";
    }
}
addcmd 'reval';

$Init_done = 1;

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