#!/usr/bin/perl
#
#  ICBM -- Internet Citizens' Band Messenger
#  A small Perl ICB client by Phil Stracchino  <alaric@caerllewys.net>
#
#  ICBM is free software.  You can redistribute it and/or modify it under
#  the terms of the GNU Lesser General Public License as published by the
#  Free Software Foundation; either version 3 of the License, or (at
#  your option) any later version.  If you use it, you are requested and
#  encouraged, but by no means required, to support its further development
#  by means of a small donation.  If you modify it, you are politely
#  requested to contribute your modifications back.


my $version : shared = '1.6.0 ("Time after time")';

#
#  Requires:

use 5.6.0;
use strict;
use Config;

# there's no point continuing if Perl doesn't have ithreads
$Config{useithreads} || die "ICBM requires a Perl 5.6.0 or later interpreter compiled with ithreads support.";

use threads;
use threads::shared;
use Getopt::Long;
use Time::Local qw( timelocal_posix timegm_posix );

# we may have to install one or more of the following modules
use POSIX;
use Socket;
use Curses;
use Term::ANSIColor;
use Term::ReadKey;
use Pod::Usage;
use utf8;

use lib qw(/usr/share/icb/);
use Net::ICB qw(:client);


# Configuration files etc

my $datadir			= defined $ENV{ICBM_DATA} ? $ENV{ICBM_DATA} : sprintf("%s/.icbm", $ENV{HOME});
my $socketdir			= $datadir.'/SOCKETS';

my $commandfile			= sprintf ("%s/commands", $datadir);
my $defaultsfile		= sprintf ("%s/defaults", $datadir);
my $colorfile			= sprintf ("%s/colors",   $datadir);
my $deflogfile			= sprintf ("%s/ICBM.log", $ENV{HOME});

# Command fifo
my $icbmfifo			= sprintf ("%s/cmdfifo.%d", $socketdir, $$);

# Initial sleep time (default 2s)

my $sleeptime			= 10;
my $init_warn			= 0;

# Logging subsystem variables
my $logsocket    		    = sprintf ("%s/logsock.%d", $socketdir, $$);
our $logfile			    : shared = '';
our $logging			    : shared = 0;
our $socket_active		    : shared = 0;
our $log_sem			    : shared = 0;
our $log_delayed_start		: shared = '';

# Packet buffers

our @logbuffer			    : shared = ();
our @pagebuffer			    : shared = ();

# General thread-global variables

our %options			    : shared;
our %colors			        : shared;
our %attr			        : shared;
our %ansi			        : shared;
our $use_color			    : shared;
our $cur_nick			    : shared;
our $cur_group			    : shared;
our $cur_mod			    : shared;
our $verbose			    : shared;
our $input_thread_running 	: shared = 0;
our $log_thread_running		: shared = 0;
our $last_color_set		    : shared = 0;
our $timeval			    : shared = 0;
our %tabhist			    : shared;
our $write_size			    : shared = 5;
our $read_size			    : shared;
our ($rows, $cols)          : shared;
our @page			        : shared = (0,0,0,0);		# pagesize, count, pause flag, replay pagesize

our @replayctr			    : shared = (0,0);
our $replaying              : shared = 0;

our $interpret_tilde_as_delete	: shared = 1;
our $reload_hookfile		: shared = '';
our @delhook_cmd		    : shared = ();
our @tid			        : shared = ();
our $exit_state			    : shared = 0;
our $quitmsg			    : shared = '';

our $output_sem			    : shared;


our %color_names : shared = ('black',	COLOR_BLACK,
                             'red',	COLOR_RED,
                             'green',	COLOR_GREEN,
                             'yellow',	COLOR_YELLOW,
                             'blue',	COLOR_BLUE,
                             'magenta',	COLOR_MAGENTA,
                             'cyan',	COLOR_CYAN,
                             'white',	COLOR_WHITE);

our @color_set : shared = ('normal', 'ownmsg', 'personal', 'persfrom', 'hilight',
                           'hilightfrom', 'who_hdr', 'nickname', 'hostmask', 'modstar',
                           'userflags', 'idletime', 'logintime', 'beep', 'beepfrom',
                           'status', 'alert', 'warning', 'error', 'more', 'sbrkt',
                           'abrkt', 'pbrkt', 'statline', 'output', 'debug', 'encrypted');

our @builtins : shared = ('alias', 'aliases', 'correct', 'delcmd', 'delhook', 'display', 'exec', 'exit',
                          'grep', 'hilight', 'load', 'log', 'out', 'pagesize', 'quit', 'deltilde', 'redraw',
                          'replay', 'revoke', 'set', 'setcolor', 'setup', 'show', 'timestamps', 'unalias',
                          'uncorrect', 'unlight', 'urls', 'version');

# Thread-global client settings

our $cmdchar			    : shared = '/';
our $escchar			    : shared = '\\';

our $beep_on_error		    : shared = 1;
our $beeps			        : shared = 1;
our $messages			    : shared = 0;
our $cc_msg_list		    : shared = 0;
our $echo_outgoing		    : shared = 1;
our $display_server_messages	: shared = 0;
our $modwarn			    : shared = 1;
our $report_load_errors		: shared = 1;
our $report_load_success	: shared = 1;
our $report_load_warnings	: shared = 1;
our $report_correction_set	: shared = 1;
our $report_url_capture		: shared = 0;
our $tab_del_on_error		: shared = 1;
our $readhistsize		    : shared = 500;
our $writehistsize		    : shared = 500;
our $timestamps_active		: shared = 0;
our $timeformat			    : shared = "%H:%M:%S";
our $logtimeformat		    : shared = "%H:%M:%S %Z %b %d %Y";
our $timestampformat		: shared = "%H:%M:%S";
our $query			: shared = '';

our @hilightnicks 		: shared = ();
our %corrections		: shared;

# Thread-global scripting data

our %usercmds			: shared;
our %aliases			: shared;
our @urllist			: shared = ();
our %trig			: shared;


# I'd have preferred to do this with a hash of hashes, but Perl won't allow
# a hash of hashes to be shared between threads.  Which means we have to have
# separately enumerated hashes.  "Oh, bugger."  Ah well, I'll abstract it in
# the code as best I can.  Still, it simplifies the code somewhat to do it
# this way, but I'd sooner have a single monolithic data structure.

our %hooks_alert		: shared;
our %hooks_awol			: shared;
our %hooks_beep			: shared;
our %hooks_boot			: shared;
our %hooks_connect		: shared;
our %hooks_error		: shared;
our %hooks_group		: shared;
our %hooks_idleboot		: shared;
our %hooks_join			: shared;
our %hooks_leave		: shared;
our %hooks_memo			: shared;
our %hooks_modgain		: shared;
our %hooks_modloss		: shared;
our %hooks_modpass		: shared;
our %hooks_modwarn		: shared;
our %hooks_newpriv		: shared;
our %hooks_nickchg		: shared;
our %hooks_notify		: shared;
our %hooks_openmsg		: shared;
our %hooks_ping			: shared;
our %hooks_presend		: shared;
our %hooks_privmsg		: shared;
our %hooks_rawmsg		: shared;
our %hooks_rename		: shared;
our %hooks_status		: shared;
our %hooks_trigger		: shared;
our %hooks_url  		: shared;
our @hooktypes			: shared = ('alert', 'awol', 'beep', 'boot', 'connect', 'error', 'group',
                                            'idleboot', 'join', 'leave', 'memo', 'modgain', 'modloss',
                                            'modpass', 'modwarn', 'newpriv', 'nickchg', 'notify', 'openmsg',
                                            'ping', 'presend', 'privmsg', 'rawmsg', 'rename', 'status',
                                            'trigger', 'url');


# Encryption-related variables
# (pre-initialized to default Blowfish cipher)
our $encryption_avail		: shared = 0;
our $encryption			: shared = 0;
our $cipher			: shared = 'Blowfish';
our %ciphertext			: shared;
our %session_keys		: shared;

# Modules required for encryption support
# We conditionally load these if and only if ALL are present

use Module::Load::Conditional qw[can_load check_install requires];

$Module::Load::Conditional::VERBOSE = 1;
my $crypt_mods = {'Crypt::DH::GMP' => undef,
		  'Crypt::CBC' => undef,
		  'Crypt::Blowfish' => undef,
		  'Digest::SHA' => undef,
		  'MIME::Base64' => undef,
		  'Compress::Zlib' => undef};

if (can_load(modules => $crypt_mods, verbose => 1))
{
    $encryption_avail = 1;
}

# These do not need to be shared between threads

my ($DH_public, $DH_public2,
    $DH_private, $DH_secret);

# crypt control constants
# '%%%' should be a fairly safe marker; I'd prefer to use chr(1<x<32),
# but the icb server won't pass them through

our $EICB_CRYPT_PREFIX	: shared = '%%%';
my $EICB_DH_INIT	= 1;
my $EICB_DH_REPLY	= 2;
my $EICB_DH_REPLY2	= 3;
my $EICB_SESSION_KEY	= 4;
my $EICB_MISSING_KEY	= 5;
my $EICB_SESSION_END	= 6;
my $EICB_CANNOT_ENCRYPT	= 9;

# This is the table of 48-bit primes for Diffie-Hellman key exchange.
# They are static, so they need not be shared.  Do not use primes
# larger than 48 bits.  The primegen tool is provided for generating
# new primes tables, but YOU CAN ONLY EXCHANGE KEYS with users who
# have the same primes table that you do.  So, unless you have a
# SPECIFIC REASON why you NEED to replace the primes table, don't.
#
# We'll load this table from $datadir/primes in a moment, if encryption
# is enabled.

my @EICB_PRIMES;


# Resize flags
our $sizechanged		: shared = undef;
our @resized			: shared = (0,0,0);

# Other globals which either cannot directly be shared between
# threads or do not need to be shared between threads

my $ret = 0;
my $mod_warned = 0;

my (%servers, @altnicks, @defnicks,
    $input_thread, $output_thread, $status_thread, $logging_thread, $fifo_thread,
    $output_window, $input_window, $status_line, $connection);


###### end of global data section


$options{server} = 'chime';			# set the default server first


if (-e $datadir && !(-d _))
{
    die "ERROR:  $datadir already exists, but is not a directory";
}

unless (-d $datadir)
{
    mkdir ($datadir, 0700) || die "ERROR:  Could not create data directory $datadir";
}

if (-e $socketdir && !(-d _))
{
    die "ERROR:  $socketdir already exists, but is not a directory";
}

unless (-d $socketdir)
{
    mkdir ($socketdir, 0700) || die "ERROR:  Could not create socket directory $socketdir";
}

$options{debug} = 0;


preload($defaultsfile) if (-f $defaultsfile);	# read early settings file if present

if ($encryption_avail && $encryption)
{
    if (my $primefile = find_primes_file())
    {
        open (PRIMES, $primefile);
        while (<PRIMES>)
        {
            chomp($_);
            push (@EICB_PRIMES, $_) if (/^(\d+)$/);
        }
        close (PRIMES);

        if ((scalar @EICB_PRIMES) < 16)
        {
            print STDERR "WARNING: Insufficient primes found; disabling encryption\n";
            $encryption_avail = $encryption = 0;
            $init_warn = 1;
        }
    }
    else
    {
        print STDERR "WARNING: Primes table not found, disabling encryption\n" if ($encryption);
        $encryption_avail = $encryption = 0;
        $init_warn = 1;
    }
}

sleep($sleeptime) if ($options{debug} || $init_warn);

if (GetOptions(\%options,
               'nick=s'		=> \@altnicks,
               'server=s',
               'group=s',
               'host=s',
               'port=i',
               'icbserverdb|db=s',
               'color|turner',
               'who',
               'list',
               'alacritty|a',
               'help|usage|?',
               'man',
               'version'))
{
    $options{clear} = 1 if ($options{password});
    mainswitch();
}
else
{
    pod2usage(-message => "\nICBM version $version\n",
              -exitstatus => -1,
              -verbose => 1);
    $ret = -1;
}

if ($input_thread_running == -1)
{
    print "\n\tICBM FATAL ERROR: ICB server connection died unexpectedly!\n\n";
    $ret = -2;
}

exit ($ret);



###### beginning of subroutines


sub mainswitch
{

    if ($options{version})
    {
        pod2usage(-message => "\nICBM version $version\nA threaded Perl ICB client\n",
                  -exitstatus => 0,
                  -verbose => 0);
    }
    elsif ($options{help})
    {
        pod2usage(-message => "\nICBM version $version\nA threaded Perl ICB client\n",
                  -exitstatus => 0,
                  -verbose => 1);
    }
    elsif ($options{man})
    {
        pod2usage(-exitstatus => 0,
                  -verbose => 2);
    }
    else
    {
        readservers();

        if ($options{list})
        {
            listservers();
        }
        else
        {
            remove_stale_sockets();

            my $server = $options{server} || $options{defserver};
            my $host = $options{host} ? $options{host}
                                      : $server ? $servers{$server}->{host}
                                                : $options{defhost};
            my $port = $options{port} ? $options{port}
                                      : $server ? $servers{$server}->{port}
                                                : $options{defport};
            my $host_ok = 0;
            if ($host)
            {
                if ($host =~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/)
                {
                    if ($1 < 256 && $2 < 256 && $3 < 256 && $4 < 256)
                    {
                        $host_ok = 1;
                    }
                    else
                    {
                        print "\n\tERROR: $host does not appear to be a valid IP address.\n\n";
                    }
                }
                elsif ($host =~ /\w+/)
                {
                    $host_ok = 1;
                }
            }
            else
            {
                $host_ok = 1;
            }

            if ($host_ok)
            {
                if ($options{who})
                {
                    who_nologin($host, $port);
                }
                else
                {
                    connect_server($host, $port);
                }
            }
        }
    }
}


sub logerr
{
    open (OUT, ">>/tmp/icbm.$$.out");
    select((select(OUT), $| = 1)[0]);
    print OUT $_[0];
    close (OUT);
}


sub remove_stale_sockets
{
    opendir(DIR, $socketdir);
    my @files = grep(/\.\d+$/, readdir (DIR));
    closedir (DIR);

    if (scalar @files)
    {
        foreach my $file (@files)
        {
            my $pid = $1 if ($file =~ /(\d+)$/);
            unless ( -d "/proc/$pid")
            {
                print "Removing stale socket/FIFO $socketdir/$file\n";
                unlink ("$socketdir/$file");
            }
        }
    }
}


sub who_nologin
{
    my ($host, $port) = @_;

    $connection = Net::ICB->new(cmd  => 'w',
                                host => $host,
                                port => $port);
    die "Could not connect to server" unless ($connection);

    if ($options{color})
    {
        if (-f $colorfile)
        {
            set_colors();
        }
        else
        {
            set_default_colors();
        }
        $use_color = 1;
    }
    else
    {
        $use_color = 0;
    }

    while (my ($type, @packet) = $connection->readmsg())
    {
        if ($type eq $M_EXIT)
        {
            $connection->close;
        }
        else
        {
            my $subtype = shift(@packet);
            if ($subtype eq 'wl')
            {
                if ($packet[0] eq 'm')
                {
                    printfcolor('normal', '%s', '  ');
                    printfcolor('modstar', '%s', '*');
                }
                else
                {
                    printfcolor('normal', '%s', '   ');
                }
                printfcolor ('nickname', "%-12s", $packet[1]);
                printfcolor ('normal', '%s', ' ');
                printfcolor ('idletime', "%10s", calc_idletime($packet[2]));
                printfcolor ('normal', '%s', ' ');
                printfcolor ('logintime', "%8s", calc_logintime($packet[4]));
                printfcolor ('normal', '%s', ' ');
                printfcolor ('hostmask', "%s", $packet[5]);
                printfcolor ('hostmask', "@%s", $packet[6]);
                printfcolor ('normal', '%s', ' ');
                printfcolor ('userflags', "%s\n", $packet[7]);
            }
            elsif ($subtype eq 'wh')
            {
                printfcolor ('who_hdr',
                              "   %-12s %10s %7s  %s\n",
                              'Nickname',
                              'Idle',
                              'Sign-On',
                              'Account');
            }
            elsif ($subtype eq 'co')
            {
                printfcolor ('who_hdr', "%s\n", $packet[0]);
            }
            else
            {
                printfcolor ('status', "USER LISTING FROM: %s\n", "@packet");
            }
        }
    }
    terminate();
}


sub connect_server
{
    my ($host, $port) = @_;
    my ($connected) = 0;
    my ($user, $group, $pass, $type, @msg);

    srand(time()^($$+($$<<15)));
    $user = eval { getlogin() } || (getpwuid($>))[0] || "user".substr(rand(), 2, 5);
    $group = $options{group} || $options{defgroup} || 1;
    push (@altnicks, $ENV{ICBNAME}) if (defined($ENV{ICBNAME}));
    push (@altnicks, @defnicks, $user);
    $cur_nick = $altnicks[0];

    initscr();
    if (has_colors() && $options{color})
    {
        start_color();
        $use_color = 1;
        $last_color_set = 0;
        if (-f $colorfile)
        {
            set_colors();
        }
        else
        {
            set_default_colors();
        }
    }
    else
    {
        $use_color = 0;
    }
    cbreak();
    noraw();
    noecho();
    nonl();
    
    if (defined $options{alacritty})
    {
        printf('%c[?1007l', 27);
        printf('%c[?1049l', 27);
    }

    # preallocate $write_size lines as an input window
    # leave one line for status line
    # the rest is the output window

    $read_size = LINES() - ($write_size + 1);

    $output_window = newwin ($read_size, 0, 0, 0);
    scrollok ($output_window, 1);
    idlok ($output_window, 1);

    $input_window = newwin ($write_size, 0, $read_size + 1, 0);
    scrollok ($input_window, 1);
    idlok ($input_window, 1);

    $status_line = newwin (1, 0, $read_size, 0);
    set_init_status ($output_window,
                      $input_window,
                      $status_line,
                      $options{server});

    $sizechanged = 0;
    
    # set signal handlers
    $SIG{KILL}  = \&terminate;
    $SIG{QUIT}  = \&terminate;
    $SIG{TERM}  = \&terminate;
    $SIG{INT}   = \&terminate;

# This doesn't work because, as far as I can tell, we NEVER RECEIVE SIGWINCH 
#    $SIG{WINCH} = \&set_resize_flag;
    
    foreach my $line (split_line('', 2, sprintf("ICBM version %s\n", $version)))
    {
        icb_print ($output_window, 'status', "%s\n", $line);
    }

    icb_print ($output_window, 'status', "Encryption %s.\n",
               $encryption ? 'enabled'
                           : $encryption_avail ? 'available but not enabled'
                                               : 'disabled');

    icb_print ($output_window, 'status', "Trying to connect to %s ICB server as ", $options{server});
    foreach my $anick (@altnicks)
    {
        $cur_nick = $anick;
        icb_print ($output_window, 'status', "%s... ", $anick);

        $connection = $options{password}
                    ? Net::ICB->new( host   => $host,
                                     port   => $port,
                                     user   => $user,
                                     nick   => $anick,
                                     group  => $group,
                                     passwd => $options{password})
                    : Net::ICB->new( host   => $host,
                                     port   => $port,
                                     user   => $user,
                                     nick   => $anick,
                                     group  => $group);

        next unless ($connection);

        ($type, @msg) = $connection->readmsg();
        die "Bad protocol" unless ($type eq $M_PROTO);
        ($type, @msg) = $connection->readmsg();
        if ($type eq $M_LOGINOK)
        {
            $connected = 1;
            last;
        }
    }

    if ($connected)
    {
        icb_print ($output_window, 'status', "Connected.\n");
        $ENV{'ICBNAME'}  = $cur_nick;       # Guaranteeing $commandfile can test cur_nick, for logfile names based on nick (for instance)
        $ENV{'ICBGROUP'} = $group;

        eval {
            load($commandfile);
        } if (-f $commandfile);

        do_hooks('connect');

        set_status ($output_window,
                    $input_window,
                    $status_line);

        if (defined $options{alacritty})
        {
            redrawwin ($output_window);
            redrawwin ($status_line);
            redrawwin ($input_window);
        }

        $logging_thread = threads->new (\&logger,
                                        $output_window,
                                        $input_window,
                                        $status_line,
                                        $connection);
        push (@tid, $logging_thread->tid());

        select(undef, undef, undef, 0.01) until ($socket_active == 1);

        $input_thread = threads->new (\&talk,
                                      $output_window,
                                      $input_window,
                                      $status_line,
                                      $connection);
        push (@tid, $input_thread->tid());

        $output_thread = threads->new(\&listen,
                                      $output_window,
                                      $input_window,
                                      $status_line,
                                      $connection);
        push (@tid, $output_thread->tid());

        $status_thread = threads->new (\&status,
                                       $output_window,
                                       $input_window,
                                       $status_line,
                                       $connection);
        push (@tid, $status_thread->tid());

        $fifo_thread = threads->new (\&fifocmd,
                                     $output_window,
                                     $input_window,
                                     $status_line,
                                     $connection);
        push (@tid, $fifo_thread->tid());


        $input_thread->join;
        $fifo_thread->join;
        $output_thread->join;
        $status_thread->join;
        $logging_thread->join;
        $connection->close();

        unlink ($icbmfifo) if (-p $icbmfifo);
        unlink ($logsocket) if (-p $logsocket);

        endwin();
        system("stty sane");
        system("stty erase ");
    }
    else
    {
        endwin();
        system("stty sane");
        print "\n\tConnection to ICB server at $host:$port failed.\n\n";
    }
}

sub set_timestamps_active # This function exists solely so external scripts can control $timestamps_active.
{
    if ( defined($_[0]) && $_[0] )
    {
        $timestamps_active = 1;
    }
    else
    {
        $timestamps_active = 0;
    }
}

sub terminate
{
    $exit_state = 1;
    $input_thread_running = 0;
    if (defined ($connection))
    {
        $connection->close;
    }

    $fifo_thread->join if (defined ($fifo_thread));
    $input_thread->join if (defined ($input_thread));
    $output_thread->join if (defined ($output_thread));
    $status_thread->join if (defined ($status_thread));
    $logging_thread->join if (defined ($logging_thread));
    unlink($logsocket) if (-S $logsocket);
    unlink($icbmfifo) if (-p $icbmfifo);

    endwin();
    system("stty sane");

    printf("\n\t%s\n\n", $quitmsg) if (length ($quitmsg));

    exit (0);
}


sub resize_output_window
{
    my $window = $_[0];
    my ($rows, $cols);
    
    getmaxyx($rows, $cols);
    $read_size = $rows - ($write_size + 1);
    Curses::wresize($window, $read_size, $cols);

    endwin($window);
    redrawwin($window);
    refresh($window);
    $resized[1] = 1;
    check_clear_resize_flags();
    return;
}


sub resize_status_line
{
    my $window = $_[0];
    my ($rows, $cols);
    
    getmaxyx($rows, $cols);
    $read_size = $rows - ($write_size + 1);
    
    mvwin($window, $read_size, 0);
    redrawwin($window);
    refresh($window);
    $resized[2] = 1;
    check_clear_resize_flags();
    return;
}


sub resize_input_window
{
    my $window = $_[0];
    my ($rows, $cols);
    
    getmaxyx($rows, $cols);
    $read_size = $rows - ($write_size + 1);
    
    mvwin($window, $read_size+1, 0);
    redrawwin($window);
    refresh($window);
    $resized[0] = 1;
    check_clear_resize_flags();
    return;
}


sub set_resize_flag
{
    @resized = (0,0,0);
    $sizechanged = 1;
#    log_send("Received a SIGWINCH") if ($log_sem);

}


sub check_clear_resize_flags
{
    if ($resized[0] && $resized[1] && $resized[2])
    {
        @resized = (0,0,0);
        $sizechanged = 0;
        beep();
    }
}


sub fifocmd
{
    my ($output_window, $input_window, $status_line, $connection) = @_;
    my ($fifodata, $rin, $rout);

    if (mkfifo($icbmfifo, oct('0600')))
    {
        icb_debug ($output_window, 1, "fifocmd() opening command FIFO", $icbmfifo);

		# We have to use sysopen() here rather than 'open (FIFO, $icbmfifo);'
		# because on many preinstalled Perls, open() does NOT use O_NONBLOCK.
		# This causes breakage, as in the open() call never returns until and
		# unless something writes to the other end of the pipe.

        sysopen (FIFO, $icbmfifo, O_RDONLY|O_NONBLOCK);

        icb_debug ($output_window, 1, "Command FIFO %s is open", $icbmfifo);

        while ($input_thread_running > 0)
        {
            $rin = '';
            vec($rin, fileno(FIFO), 1) = 1;
            select($rout = $rin, undef, undef, 0.01);
            if (vec($rout, fileno(FIFO), 1))
            {
                sysread(FIFO, $fifodata, 65535);
                close(FIFO);
                sysopen (FIFO, $icbmfifo, O_RDONLY|O_NONBLOCK);
                next if ($fifodata eq '/x' || $fifodata eq '/exit' || $fifodata eq '/quit');
                handle_input($fifodata,
                             $output_window,
                             $input_window,
                             $status_line,
                             $connection) if (length($fifodata));
            }
            threads->yield if ($input_thread_running > 0);
        }
        icb_debug ($output_window, 1, "Input thread stopped; fifocmd() exiting", $icbmfifo);
        close (FIFO);
        unlink ($icbmfifo);
    }
    else
    {
        icb_print ($output_window, 'status', "Could not open %s as FIFO", $icbmfifo);
        icb_print ($output_window, 'status', "Remote control functionality will not be available.");
    }
}


sub logger
{
    my ($output_window, $input_window, $status_line, $connection) = @_;


    socket(SOCKET, PF_UNIX, SOCK_DGRAM, 0) || die "Cannot open socket in logger";
    bind(SOCKET, sockaddr_un($logsocket))  || die "Cannot bind socket in logger";
    $socket_active = 1;

    select(undef, undef, undef, 0.1) until ($input_thread_running == 1);
    $log_thread_running = 1;

    while ($input_thread_running > 0)
    {
        my $rin = '';
        my ($rout, $logdata);

        vec($rin, fileno(SOCKET), 1) = 1;
        select($rout = $rin, undef, undef, 0.1);
        if (vec($rout, fileno(SOCKET), 1))
        {
            recv(SOCKET, $logdata, 1024, 0);
            if (length $logdata > 0)
            {
                my ($cmd, $file, $path);
                ($cmd, $file, undef) = split(/\s+/, $logdata, 3);

                if ($cmd eq '%START%')
                {
                    my $valid = 1;
                    my $p;

                    $logfile = $file ? ($p = rindex($file, '/') == -1) ? sprintf ("%s/%s", $ENV{HOME}, $file)
                                                                       : $file
                                     : $deflogfile;
                                         
                    $p = rindex($logfile, '/');
                    $path = substr($logfile, 0, $p);
                    if (-d $path && (-w _ || -W _))
                    {
                        if (-e $logfile)
                        {
                            $valid = (-f _ && (-w _ || -W _)) ? 1 : 0;
                            if ($valid)
                            {
                                open (LOG, ">>$logfile") || ($valid = 0);
                                if ($valid)
                                {
                                    select((select(LOG), $| = 1)[0]);
                                    $logging = 1;
                                }
                                else
                                {
                                    icb_print ($output_window, 'sbrkt',  "%s", "[=");
                                    icb_print ($output_window, 'error',  "%s", "ERROR");
                                    icb_print ($output_window, 'sbrkt',  "%s", "=]");
                                    icb_print ($output_window, 'status', " Could not open logfile %s and don't know why.\n", $logfile);
                                }
                            }
                            else
                            {
                                icb_print ($output_window, 'sbrkt',  "%s", "[=");
                                icb_print ($output_window, 'warning', "%s", "Warning");
                                icb_print ($output_window, 'sbrkt',  "%s", "=]");
                                icb_print ($output_window, 'status', " Logfile %s is not writeable.\n", $logfile);
                            }                    
                        }
                        else
                        {
                            open (LOG, ">>$logfile") || out("Logfile open failed.");
                            select((select(LOG), $| = 1)[0]);
                            $logging = 1;
                        }
                    }
                    else
                    {
                        $valid = 0;
                        icb_print ($output_window, 'sbrkt',  "%s", "[=");
                        icb_print ($output_window, 'warning', "%s", "Warning");
                        icb_print ($output_window, 'sbrkt',  "%s", "=]");
                        icb_print ($output_window, 'status', " Directory %s is not writeable.\n", $path);
                    }
                    $log_sem = $logging;
                }
                elsif ($cmd eq '%STOP%')
                {
                    close (LOG);
                    $log_sem = $logging = 0;
                }
                else
                {
                    print LOG $logdata;
                }
            }
        }
        threads->yield;
    }

    if ($logging)
    {
        printf LOG ("Closing logfile on session termination at %s\n",
                    strftime($logtimeformat, localtime(time())));
        close (LOG);
    }

    $log_thread_running = 0;
    shutdown(SOCKET, 2);
    unlink ($logsocket);
}


sub log_start
{
    $logfile = $_[0] || $deflogfile;

    if ($log_thread_running)
    {
        log_stop() if ($logging == 1);

        $log_sem = -1;
        log_send('%START% '.$logfile);
        select(undef, undef, undef, 0.1) while ($log_sem == -1);

        if ($logging)
        {
            icb_print ($output_window, 'sbrkt',  "%s", "[=");
            icb_print ($output_window, 'status', "%s", "Logging");
            icb_print ($output_window, 'sbrkt',  "%s", "=]");
            icb_print ($output_window,
                       'status',
                       " Opening logfile %s at %s\n",
                       $logfile,
                       strftime($logtimeformat, localtime(time())));
        }
        else
        {
            icb_print ($output_window, 'sbrkt',  "%s", "[=");
            icb_print ($output_window, 'warning', "%s", "Warning");
            icb_print ($output_window, 'sbrkt',  "%s", "=]");
            icb_print ($output_window, 'status', " Log start to '%s' failed.\n", $logfile);
        }
    }
    else
    {
        $log_delayed_start = $logfile;
    }
}


sub log_stop
{
    if ($logging)
    {
        icb_print ($output_window, 'sbrkt',  "%s", "[=");
        icb_print ($output_window, 'status', "%s", "Logging");
        icb_print ($output_window, 'sbrkt',  "%s", "=]");
        icb_print ($output_window,
                    'status',
                    " Logfile %s closed at %s\n",
                    $logfile,
                    strftime($logtimeformat, localtime(time())));
        $log_sem = -1;
        log_send('%STOP%');
        select(undef, undef, undef, 0.1) while ($log_sem == -1);
    }
    else
    {
        icb_print ($output_window, 'sbrkt',  "%s", "[=");
        icb_print ($output_window, 'warning', "%s", "Warning");
        icb_print ($output_window, 'sbrkt',  "%s", "=]");
        icb_print ($output_window, 'status', "%s", " Logging is not active: Cannot stop.");
    }
}


sub log_send
{
    my ($buf) = @_;
    my $tid = (threads->self)->tid();

    unless ($tid == 1)
    {
        send(SOCKET, $buf, 0);
    }
}


sub talk
{
    my ($output_window, $input_window, $status_line, $connection) = @_;
    my ($ret, $ptr, $histptr, $tabptr, $intab) = (0,0,0,0,0);
    my ($temp, $buffer) = ('','');
    my @cmdbuffer = ();
    my @tabhist = ();
    my $color = 'output';
    my ($key, $y, $x, $hl, $tl);


    $input_thread_running = 1;
    socket(SOCKET, PF_UNIX, SOCK_DGRAM, 0)    || die "Input thread cannot open socket";
    connect(SOCKET, sockaddr_un($logsocket))  || die "Input thread cannot connect to log socket";

    select(undef,undef,undef,0.01) until ($log_thread_running);
    log_start($log_delayed_start) if (length $log_delayed_start && $log_thread_running);

    if ($use_color)
    {
        attron ($input_window, COLOR_PAIR($colors{$color})) if ($colors{$color});
        attron ($input_window, A_BOLD) if ($attr{$color} & 1);
        attron ($input_window, A_REVERSE) if ($attr{$color} & 2);
    }
    refresh($input_window);

    ReadMode 3;
    binmode STDIN, ':utf8';
    
    while ($input_thread_running > 0 && !$ret)
    {
        until (defined($key = ReadKey(0))) 	# wait for input
        {
            resize_input_window($input_window) if ($sizechanged);
        }

        if ($key == ERR)
        {
            threads->yield;
        }
        elsif ($key eq KEY_RESIZE)
        {
# This ALSO doesn't work because, as far as I can tell, we NEVER RECEIVE KEY_RESIZE 
            set_resize_flag();
            threads->sleep;
        }
        elsif (($key eq "\n") or ($key eq "\r"))			# return/newline
        {
            if (length($buffer))					# ignore empty input except for paging
            {
                if (substr($buffer,0,1) eq '!' || substr($buffer,0,1) eq '^')
						# command-history recall and/or substitution
                {
                    my $success = 1;
                    my $temp = $buffer;
                    if ($hl)
                    {
                        my ($pat, $orig, $repl, $junk) = split (/\^/, $buffer);
                        if ($junk)
                        {
                            $success = 0;
                        }
                        else
                        {
                            if ($pat)					# recall
                            {
                                my ($matches, @matches);
                                $pat = substr($pat, 1);
                                $matches = @matches = grep(/^$pat/, @cmdbuffer);
                                if ($matches)
                                {
                                    $buffer = $matches[$matches-1];
                                }
                                else
                                {
                                    $success = 0;
                                }
                            }

                            if ($orig && $repl && $success)		# substitution
                            {
                                $buffer = $cmdbuffer[$hl-1] unless ($pat);
                                if ($buffer =~ /$orig/)
                                {
                                    $buffer =~ s/$orig/$repl/g;
                                }
                                else
                                {
                                    $success = 0;
                                }
                            }
                            elsif ($orig || $repl)
                            {
                                $success = 0;
                            }
                        }
                    }
                    else
                    {
                        $success = 0;
                    }

                    if ($success)
                    {
                        lock ($output_sem);
                        erase($input_window);
                        addstr($input_window, $buffer);
                        refresh($input_window);
                        $ptr = length($buffer);
                        curs_to_ptr($input_window, $ptr);
                    }
                    else
                    {
                        do_beep();
                    }
                }
                else							# line to go out
                {
                    push (@cmdbuffer, $buffer);
                    shift(@cmdbuffer) while (($hl = @cmdbuffer) > $writehistsize);
                    $intab = $tabptr = $histptr = $ptr = 0;
                    lock ($output_sem);
                    erase($input_window);
                    refresh($input_window);
                    curs_to_ptr($input_window, $ptr);
                    $buffer =~ s/\s+$//;
                    $ret = handle_input($buffer,
                                         $output_window,
                                         $input_window,
                                         $status_line,
                                         $connection) if (length($buffer));
                    $temp = $buffer = "";
                }
            }
            elsif ($page[2])
            {
                unpause();
            }
        }
        elsif (ord($key) == 1)						# home
        {
            lock ($output_sem);
            $ptr = 0;
            curs_to_ptr($input_window, $ptr);
        }
        elsif (ord($key) == 2)						# prev word
        {
            if ($ptr == 0)
            {
                do_beep();
            }
            else
            {
                my $p;
                if ($ptr >= length($buffer))
                {
                    $p = rindex($buffer, ' ');
                    $p = 0 if ($p < 0);
                    while ($p >0 && !(substr($buffer, $p) =~ /\S+/))
                    {
                        $p = rindex($buffer, ' ', $p-1);
                        if ($p <= 0)
                        {
                            $p = 0;
                            last;
                        }
                    }
                }
                else
                {
                    $p = rindex($buffer, ' ', $ptr);
                    $p = 0 if ($p < 0);
                    while ($p > 0 && !(substr($buffer, $p, $ptr - $p) =~ /\S+/))
                    {
                        $p = rindex($buffer, ' ', $p-1);
                        if ($p <= 0)
                        {
                            $p = 0;
                            last;
                        }
                    }
                }
                $p++ unless ($p == 0);
                $ptr = $p;
                lock ($output_sem);
                curs_to_ptr($input_window, $ptr);
                refresh($input_window);
            }
        }
        elsif (ord($key) == 4 || ($interpret_tilde_as_delete == 1 && ord($key) == 126))                       #del
        {
            if ($ptr < (length($buffer)))
            {
                substr($buffer,$ptr,1) = '';
                lock ($output_sem);
                erase($input_window);
                addstr($input_window, $buffer);
                refresh($input_window);
                curs_to_ptr($input_window, $ptr);
            }            
            else
            {
                do_beep();
            }
        }
        elsif (ord($key) == 5)						# end
        {
            lock ($output_sem);
            $ptr = length($buffer);
            curs_to_ptr($input_window, $ptr);
        }
        elsif (ord($key) == 6)						# next word
        {
            if ($ptr >= length($buffer))
            {
                do_beep();
            }
            else
            {
                my $p;
                if ($ptr == 0)
                {
                    $p = index($buffer, ' ');
                }
                else
                {
                    $p = index($buffer, ' ', $ptr);
                }

                if ($p < 0)
                {
                    do_beep();
                }
                else
                {
                    while (substr($buffer, $p, 1) eq ' ' && $p < length($buffer))
                    {
                        $p++;
                    }
                    if ($p == length($buffer) && substr($buffer, $p, 1) eq ' ')
                    {
                        do_beep();
                    }
                    else
                    {
                        $ptr = $p;
                        lock ($output_sem);
                        curs_to_ptr($input_window, $ptr);
                        refresh($input_window);
                    }
                }
            }
        }
        elsif (ord($key) == 8 || ord($key) == 127)						#bksp
        {
            if ($ptr > 0)
            {
                if ($ptr >= length($buffer))
                {
                    $buffer = substr($buffer, 0, -1);
                    $ptr = length($buffer);
                }
                else
                {
                    substr($buffer, $ptr-1, 1) = '';
                    $ptr--;
                }
                lock ($output_sem);
                erase($input_window);
                addstr($input_window, $buffer);
                refresh($input_window);
                curs_to_ptr($input_window, $ptr);
            }
            else
            {
                do_beep();
            }
        }
        elsif (ord($key) == 9) 						# tab
        {
            if ($buffer =~ /^$cmdchar(m)(sg)? (\S+)$/i && grep(/^$3/, keys(%tabhist)))
            {
                $buffer = sprintf ("%sm %s ", $cmdchar, (grep(/^$3/i, sort bytabtime keys(%tabhist)))[0]);
            }
            else
            {
                my $tabtemp;
                if (length($buffer))
                {
                    $buffer = $2 if ($buffer =~ /^$cmdchar(m)\s+\S+\s+(.*)/i);
                    $buffer = '' if ($buffer =~ /^$cmdchar(m)\s+$/i || $buffer =~ /^\/s+$/);
                    $tabtemp = $buffer if (length($buffer));
                    $buffer = '';
                }
                if ($intab)
                {
                    $buffer = sprintf ("%sm %s ", $cmdchar, $tabhist[$tabptr++]);
                    $tabptr = 0 if ($tabptr >= $tl);
                }
                elsif ($tl = @tabhist = grep (!/^\s*$/, sort bytabtime keys(%tabhist)))
                {
                    $buffer = sprintf ("%sm %s ", $cmdchar, $tabhist[0]);
                    $tabptr = ($tl == 1 ? 0 : 1);
                    $intab = 1;
                }
                else
                {
                    if ($tabtemp)
                    {
                        do_beep();
                    }
                    else
                    {
                        $buffer = sprintf("%sm ", $cmdchar);
                    }
                }
                $buffer .= $tabtemp if ($tabtemp);
            }
            $ptr = length($buffer);
            lock ($output_sem);
            erase($input_window);
            addstr($input_window, $buffer);
            refresh($input_window);
            curs_to_ptr($input_window, $ptr);
        }
        elsif (ord($key) == 11)						# del from eol
        {
            $buffer = substr($buffer, 0, $ptr);
            lock ($output_sem);
            erase($input_window);
            addstr($input_window, $buffer);
            move($input_window, 0, 0);
            refresh($input_window);
            curs_to_ptr($input_window, $ptr);
        }
        elsif (ord($key) == 21)						# del to bol
        {
            $buffer = substr($buffer, $ptr);
            $ptr = 0;
            lock ($output_sem);
            erase($input_window);
            addstr($input_window, $buffer);
            refresh($input_window);
            curs_to_ptr($input_window, $ptr);
        }
        elsif (ord($key) == 23)						# kill previous word
        {
            if ($ptr == 0)
            {
                do_beep();
            }
            else
            {
                my $p;
                if ($ptr >= length($buffer))
                {
                    $p = rindex($buffer, ' ');
                    $p = 0 if ($p < 0);
                    while ($p >0 && !(substr($buffer, $p) =~ /\S+/))
                    {
                        $p = rindex($buffer, ' ', $p-1);
                        if ($p <= 0)
                        {
                            $p = 0;
                            last;
                        }
                    }
                }
                else
                {
                    $p = rindex($buffer, ' ', $ptr);
                    $p = 0 if ($p < 0);
                    while ($p > 0 && !(substr($buffer, $p, $ptr - $p) =~ /\S+/))
                    {
                        $p = rindex($buffer, ' ', $p-1);
                        if ($p <= 0)
                        {
                            $p = 0;
                            last;
                        }
                    }
                }
                $p++ unless ($p == 0);

                if ($ptr >= length($buffer))
                {
                    $buffer = substr($buffer, 0, $p);
                    $ptr = length($buffer);
                }
                else
                {
                    substr($buffer, $p, $ptr - $p) = '';
                    $ptr = $p;
                }
                lock ($output_sem);
                erase($input_window);
                addstr($input_window, $buffer);
                refresh($input_window);
                curs_to_ptr($input_window, $ptr);
            }
        }
        elsif (ord($key) == 27)						# esc
        {
            ReadKey(0);							# throw away the [
            $key = ReadKey(0);
            if ($key eq 'A')						# up
            {
                $hl = @cmdbuffer;
                if ($histptr == $hl)
                {
                    do_beep();
                }
                elsif ($hl)
                {
                    $temp = $buffer if ($buffer && !$histptr);
                    $histptr++;
                    $buffer = $cmdbuffer[$hl - $histptr];
                    lock ($output_sem);
                    erase($input_window);
                    addstr($input_window, $buffer);
                    refresh($input_window);
                    $ptr = length($buffer);
                }
                else
                {
                    do_beep();
                }
            }
            elsif ($key eq 'B')						# down
            {
                $hl = @cmdbuffer;
                if ($histptr)
                {
                    $histptr--;
                    if ($histptr)
                    {
                        $buffer = $cmdbuffer[$hl - $histptr];
                    }
                    elsif ($temp)
                    {
                        $buffer = $temp;
                        $temp = '';
                    }
                    else
                    {
                        $buffer = '';
                    }
                    lock ($output_sem);
                    erase($input_window);
                    addstr($input_window, $buffer);
                    refresh($input_window);
                    $ptr = length($buffer);
                }
                else
                {
                    do_beep();
                }
            }
            elsif ($key eq 'C')						# right
            {
                $ptr += curs_r($input_window);
                lock ($output_sem);
                curs_to_ptr($input_window, $ptr);
                refresh($input_window);
            }
            elsif ($key eq 'D')						# left
            {
                $ptr -= curs_l($input_window);
                lock ($output_sem);
                curs_to_ptr($input_window, $ptr);
                refresh($input_window);
            }
            elsif ($key eq 'F' || $key eq '4')				# end
            {
                ReadKey(0) if ($key eq '4');				# discard ~ in screen
                $ptr = length($buffer);
                lock ($output_sem);
                curs_to_ptr($input_window, $ptr);
            }
            elsif ($key eq 'H' || $key eq '1')				# home
            {
                ReadKey(0) if ($key eq '1');				# discard ~ in screen
                $ptr = 0;
                lock ($output_sem);
                curs_to_ptr($input_window, $ptr);
            }
        }
        else
        {
            if ($ptr >= length($buffer))
            {
                $ptr = length($buffer);
                $buffer .= $key;
                lock ($output_sem);
                addch($input_window, $key);
            }
            else
            {
                substr($buffer, $ptr, 0) = $key;
                lock ($output_sem);
                erase($input_window);
                addstr($input_window, $buffer);
            }
            $ptr++;
            lock ($output_sem);
            refresh($input_window);
            curs_to_ptr($input_window, $ptr);
        }

        threads->yield unless ($ret);;
    }
    if ($use_color)
    {
        attroff ($input_window, COLOR_PAIR($colors{$color})) if ($colors{$color});
        attroff ($input_window, A_BOLD) if ($attr{$color} & 1);
        attroff ($input_window, A_REVERSE) if ($attr{$color} & 2);
    }
    lock ($output_sem);
    refresh($input_window);

    $input_thread_running = 0;

    ReadMode 0;
    shutdown(SOCKET, 1);
}


sub handle_input
{
    my ($input, $output_window, $input_window, $status_line, $connection) = @_;
    my $ret = 0;
    my (@tok, @tmpbuffer, @packet, @to,
        $line, $tok, $cmd, $msg, $to, $key, $sep, $time, $type,
        $reg, $flags, $orig, $sub, $out, $tolist, $i, $m, $ll);

    chomp ($input);
    $line = lc ($input);
    @tok = split (/\s+/, $line);


    # Any input beginning with the cmdchar (/ by default) is a command, except for
    # input beginning with a double cmdchar (// by default), which escapes the
    # leading cmdchar and the input is thus no longer a command.  We also later on
    # allow the use of \ to escape any specially-handled character if it is the first
    # character on a line.


    if (substr($tok[0],0,1) eq $cmdchar && !(substr($tok[0],1,1) eq $cmdchar))
    {
        $cmd = substr($tok[0],1);

        if ($cmd eq 'delcmd')			# check for delcmd and other essential commands
        {					# first, so that they cannot be overridden
            if (scalar @tok == 2)
            {
                delcmd($tok[1]);
            }
            else
            {
                do_beep();
            }
        }
        elsif ($cmd eq 'delhook')
        {
            if (scalar @tok == 3)
            {
                delhook($tok[1], (&strip_token((&strip_token($input))[1]))[1]);
            }
            else
            {
                do_beep();
            }
        }
        elsif ($cmd eq 'show')
        {
            if (scalar @tok != 2)
            {
                do_beep();
                icb_print ($output_window, 'error', "[=Error=] Wrong number of arguments to show command\n", '');
            }
            else
            {
                if ($tok[1] eq 'aliases')
                {
                    foreach $key (sort keys %aliases)
                    {
                        icb_print ($output_window, 'sbrkt', "%s", '[=');
                        icb_print ($output_window, 'status', "%s", 'Aliases');
                        icb_print ($output_window, 'sbrkt', "%s", '=]');
                        icb_print ($output_window, 'status', " /%s is aliased to /%s\n", $key, $aliases{$key});
                    }
                }
                elsif ($tok[1] eq 'corrections')
                {
                    foreach $key (sort keys %corrections)
                    {
                        icb_print ($output_window, 'sbrkt', "%s", '[=');
                        icb_print ($output_window, 'status', "%s", 'Correct');
                        icb_print ($output_window, 'sbrkt', "%s", '=]');
                        icb_print ($output_window, 'status', " '%s' will be corrected to '%s'\n", $key, $corrections{$key});
                    }
                }
                elsif ($tok[1] eq 'hilights')
                {
                    icb_print ($output_window, 'sbrkt', "%s", '[=');
                    icb_print ($output_window, 'status', "%s", 'Hilights');
                    icb_print ($output_window, 'sbrkt', "%s", '=]');
                    icb_print ($output_window, 'status', " The following nicks will be hilighted:\n");
                    foreach $line (split_output('', join(', ', @hilightnicks), 0))
                    {
                        icb_print ($output_window, 'status', "%s\n", $line);
                    }
                }
            }
        }
        elsif ($cmd eq 'setcolor')
        {
            set_color(split(/\s+/, (&strip_token($input))[1]));
        }
        elsif ($cmd eq 'set')
        {
            if (scalar @tok < 3)
            {
                do_beep();
                icb_print ($output_window, 'error', "[=Error=] Not enough arguments to set command\n", '');
            }
            elsif ($tok[1] eq 'timeformat' || $tok[1] eq 'logtimeformat' || $tok[1] eq 'timestampformat')
            {
                set($tok[1], (&strip_token((&strip_token($input))[1]))[1]);
            }
            elsif (scalar @tok != 3)
            {
                do_beep();
                icb_print ($output_window, 'error', "[=Error=] Wrong number of arguments to set command\n", '');
            }
            else
            {
                set($tok[1], (&strip_token((&strip_token($input))[1]))[1]);
            }
        }
        elsif ($cmd eq 'out')
        {
            if (scalar @tok > 1)
            {
                icb_print ($output_window, 'output', "%s\n", (&strip_token($input))[1]);
            }
            else
            {
                do_beep();
            }
        }
        elsif ($cmd eq 'log')
        {
            if (scalar @tok < 2 || scalar @tok > 3)
            {
                do_beep();
                icb_print ($output_window, 'error', "[=Error=] Wrong number of arguments to log command\n", '');
            }
            else
            {
                if ($tok[1] eq 'stop' || $tok[1] eq 'off')
                {
                    log_stop;
                }
                elsif ($tok[1] eq 'start' || $tok[1] eq 'on')
                {
                    scalar @tok == 3 ? log_start($tok[2]) : log_start();
                }
                else
                {
                    do_beep();
                    icb_print ($output_window, 'error', "[=Error=] Unrecognized parameter to log command\n", '');
                }
            }
        }
        elsif ($cmd eq 'timestamps')
        {
            if (scalar @tok == 2)
            {
                if ($tok[1] eq 'on')
                {
                    $timestamps_active = 1;
                    icb_print ($output_window, 'sbrkt',  "%s", '[=');
                    icb_print ($output_window, 'status', "%s", "Client");
                    icb_print ($output_window, 'sbrkt',  "%s", '=]');
                    icb_print ($output_window, 'status', " Timestamps now active.\n", '');
                }
                elsif ($tok[1] eq 'off')
                {
                    $timestamps_active = 0;
                    icb_print ($output_window, 'sbrkt',  "%s", '[=');
                    icb_print ($output_window, 'status', "%s", "Client");
                    icb_print ($output_window, 'sbrkt',  "%s", '=]');
                    icb_print ($output_window, 'status', " Timestamps now disabled.\n", '');
                }
                else
                {
                    do_beep();
                    icb_print ($output_window, 'sbrkt',  "%s", '[=');
                    icb_print ($output_window, 'error',  "%s", "Error");
                    icb_print ($output_window, 'sbrkt',  "%s", '=]');
                    icb_print ($output_window, 'status', " Unrecognized parameter to timestamps command\n", '');
                }
            }
        }
        elsif ($cmd eq 'encryption')
        {
            if (scalar @tok == 2)
            {
                if ($tok[1] eq 'on')
                {
                    if ($encryption_avail)
                    {
                        $encryption = 1;
                        icb_print ($output_window, 'sbrkt',  "%s", '[=');
                        icb_print ($output_window, 'status', "%s", "SECURE");
                        icb_print ($output_window, 'sbrkt',  "%s", '=]');
                        icb_print ($output_window, 'status', " Encryption now active.\n", '');
                    }
                    else
                    {
                        icb_print ($output_window, 'sbrkt',  "%s", '[=');
                        icb_print ($output_window, 'warning', "%s", "SECURE");
                        icb_print ($output_window, 'sbrkt',  "%s", '=]');
                        icb_print ($output_window, 'status', " Encryption is not available; cannot enable it.\n", '');
                    }
                }
                elsif ($tok[1] eq 'off')
                {
                    $encryption = 0;
                    icb_print ($output_window, 'sbrkt',  "%s", '[=');
                    icb_print ($output_window, 'status', "%s", "SECURE");
                    icb_print ($output_window, 'sbrkt',  "%s", '=]');
                    icb_print ($output_window, 'status', " Encryption now disabled.\n", '');
                }
                else
                {
                    do_beep();
                    icb_print ($output_window, 'sbrkt',  "%s", '[=');
                    icb_print ($output_window, 'error',  "%s", "Error");
                    icb_print ($output_window, 'sbrkt',  "%s", '=]');
                    icb_print ($output_window, 'status', " Unrecognized parameter to encryption command\n", '');
                }
            }
            else
            {
                do_beep();
            }
        }
        elsif ($cmd eq 'deltilde')
        {
            if (scalar @tok == 2)
            {
                if ($tok[1] eq 'on')
                {
                    $interpret_tilde_as_delete = 1;
                }
                elsif ($tok[1] eq 'off')
                {
                    $interpret_tilde_as_delete = 0;
                }
                else
                {
                    do_beep();
                    icb_print ($output_window, 'sbrkt',  "%s", '[=');
                    icb_print ($output_window, 'error',  "%s", "Error");
                    icb_print ($output_window, 'sbrkt',  "%s", '=]');
                    icb_print ($output_window, 'status', " Unrecognized parameter to deltilde command\n", '');
                }
            }
            else
            {
                do_beep();
            }
        }
        elsif ($cmd eq 'pagesize')
        {
            set_pagesize($output_window, @tok);
        }
        elsif ($cmd eq 'load')
        {
            if (scalar @tok == 2)
            {
                load($tok[1]);
            }
            else
            {
                do_beep();
            }
        }
        elsif ($cmd eq 'loadhook')
        {
            if (scalar @tok == 2)
            {
                loadhook($tok[1]);
            }
            else
            {
                do_beep();
            }
        }
        elsif ($cmd eq 'hilight')
        {
            if (scalar @tok < 2)
            {
                do_beep();
            }
            else
            {
                shift(@tok);
                foreach $tok (@tok)
                {
                    push (@hilightnicks, $tok) unless (grep(/^$tok$/i, @hilightnicks));
                }
            }
        }
        elsif ($cmd eq 'unlight')
        {
            if (scalar @tok < 2)
            {
                do_beep();
            }
            else
            {
                shift(@tok);
                foreach $tok (@tok)
                {
                    @hilightnicks = grep(!/^$tok$/i, @hilightnicks);
                }
            }
        }
        elsif ($cmd eq 'replay' || $cmd eq 'display')
        {
            replay($output_window, $input_window, $status_line, @tok);
        }
        elsif ($cmd eq 'grep')
        {
            if (scalar @tok == 1)
            {
                icb_print ($output_window, 'error', "[=Error=] %s\n", "Not enough arguments to /grep command");
            }
            else
            {
                @tok = split(/\s+/, $input);
                shift(@tok);
                $reg = join(' ', @tok);

                if ($reg =~ m,^[^/](.+)/i$,)		# attempt basic fixup on poorly formed regexps
                {
                    $reg = sprintf('/%s', $reg);
                }
                elsif ($reg =~ m,^[^/](.+)[^/]$,)
                {
                    $reg = sprintf('/%s/', $reg);
                }

                icb_print ($output_window, 'alert', "[=Regexp=] %s\n", $reg);

                if ($reg =~ m,^/([^/]+)/(i?)$,)
                {
                    ($reg, $flags) = ($1, $2);
                    @tmpbuffer = ($flags eq 'i') ? grep (/$reg/i, @logbuffer)
                                                 : grep (/$reg/, @logbuffer);
                    $sep = chr(255);

                    foreach my $m (@tmpbuffer)
                    {
                        my ($time, $type, @packet) = split(/$sep/, $m);
                        parse_packet($output_window, $input_window, $status_line, $time, $type, @packet);
                    }
                }
                else
                {
                    icb_print ($output_window, 'error', "[=Error=] %s\n", "Improperly formed regex for /grep command");
                }
            }
        }
        elsif ($cmd eq 'alias')
        {
            if (scalar @tok > 2)
            {
                if (grep(/^$tok[1]$/, @builtins))
                {
                    do_beep();
                    icb_print ($output_window, 'error', "[=Error=] Builtin command '%s' cannot be used as an alias\n", $tok[1]);
                }
                elsif ($tok[1] eq $tok[2])
                {
                    do_beep();
                    icb_print ($output_window, 'error', "[=Error=] Alias of '%s' to itself would cause recursion\n", $tok[1]);
                }
                else
                {
                    $aliases{$tok[1]} = (&strip_token((&strip_token($input))[1]))[1];
                }
            }
            else
            {
                icb_print ($output_window, 'error', "[=Error=] %s\n", "Empty alias");
                do_beep();
            }
        }
        elsif ($cmd eq 'unalias')
        {
            if (scalar @tok == 2)
            {
                delete ($aliases{$tok[1]}) if defined ($aliases{$tok[1]});
            }
            else
            {
                do_beep();
                icb_print ($output_window, 'error', "[=Error=] %s\n", "Not enough arguments to /unalias command");
            }
        }
        elsif ($cmd eq 'query' || $cmd eq 'personalto')
        {
            if (scalar @tok > 1)
            {
                icb_print ($output_window, 'warning', "[=Warning=] %s\n", "Excess arguments to /query ignored") if (scalar @tok > 2);
                $query = $tok[1];
                icb_print ($output_window, 'status', "[=Query=] Now talking to %s\n", $tok[1]);
            }
            else
            {
                icb_print ($output_window, 'status', "[=Query=] Unset\n") if (length($query));
                $query = '';
            }
        }
        elsif ($cmd eq 'urls')
        {
            urls((&strip_token($input))[1]);
        }
        elsif ($cmd eq 'correct')
        {
            $input = substr($input,index($input,' '));
            $input =~ s/\s+$//;
            $input =~ s/^\s+//;
            if ($input =~ /^'([^']+)'\s+'([^']+)'$/)
            {
                ($orig, $sub) = ($1, $2);
                correct($orig, $sub);
            }
            elsif ($input =~ /^\/(.+)\/(.+)\/$/)
            {
                ($orig, $sub) = ($1, $2);
                correct($orig, $sub);
            }
            else
            {
                icb_print ($output_window, 'error', "[=Error=] %s\n", "Invalid syntax for /correct command");
                do_beep();
            }
        }
        elsif ($cmd eq 'uncorrect')
        {
            $input = substr($input,index($input,' '));
            $input =~ s/\s+$//;
            $input =~ s/^\s+//;
            if ($input =~ /^'(.+)'$/)
            {
                my $orig = $1;
                delete ($corrections{$orig}) if defined ($corrections{$orig});
            }
            elsif ($input =~ /^\/(.+)\/$/)
            {
                $orig = $1;
                if (defined $corrections{$orig})
                {
                    icb_print ($output_window, 'sbrkt', "%s", '[=');
                    icb_print ($output_window, 'status', "%s", 'Correct');
                    icb_print ($output_window, 'sbrkt', "%s", '=]');
                    icb_print ($output_window, 'status', " Correction /%s/%s/ deleted.\n", $orig, $corrections{$orig});
                    delete ($corrections{$orig});
                }
            }
            else
            {
                do_beep();
                icb_print ($output_window, 'error', "[=Error=] %s\n", "Invalid syntax for /uncorrect command");
            }
        }
        elsif ($cmd eq 'version')
        {
            out(sprintf("ICBM version %s", $version));
            $connection->sendcmd('v');
        }
        elsif ($cmd eq 'redraw')
        {
            icb_print ($output_window, 'error', "[=Redraw=] COLS=%s LINES=%s\n", COLS(), LINES()) if ($options{debug});
            scr_dump ("scr_dump.1.out");
            resize    ($output_window, LINES() - $write_size - 1, COLS());
            resize    ($status_line, 1, COLS());
            resize    ($input_window, $write_size, COLS());
            redrawwin ($output_window);
            redrawwin ($status_line);
            redrawwin ($input_window);
            refresh   ($output_window);
            refresh   ($status_line);
            refresh   ($input_window);
            scr_dump  ("scr_dump.2.out");
        }
        elsif (defined ($aliases{$cmd}))	# alias expansion
        {
            substr($input, 1, length($cmd)) = $aliases{$cmd};
            handle_input ($input, $output_window, $input_window, $status_line, $connection);
        }
        elsif (defined ($usercmds{$cmd}))	# check for user commands next, so that
        {					# any other builtin can be overridden
            my ($usercmd, $args);
            if (scalar @tok > 1)
            {
                ($args = (&strip_token($input))[1]) =~ s/'/\\'/g;
                $usercmd = sprintf('&%s(\'%s\');', $cmd, $args);
            }
            else
            {
                $usercmd = sprintf('&%s();', $cmd);
            }
            eval $usercmd;
        }
        elsif ($cmd =~ /^(quit|x|exit)$/)	# QUIT ICB
        {
            if ($cur_mod && $modwarn && !$mod_warned)
            {
                $mod_warned = 1;
                do_hooks('modwarn');
                icb_print ($output_window, 'warning', "[=Quit=] Are you sure you want to quit?  You are still Moderator of %s.\n", $cur_group);
            }
            else
            {
                $input_thread_running = 0;
                $ret = 1;
            }
        }
        elsif ($cmd =~ /^(g|group)$/)
        {
            $connection->sendcmd('g', $tok[1] ? $tok[1] : '');
        }
        elsif ($cmd =~ /^(w|who)$/)
        {
            $connection->sendcmd('w', $tok[1] ? $tok[1] : '');
        }
        elsif ($cmd =~ /^(n|nick|name)$/)
        {
            $connection->sendcmd('name', (&strip_token($input))[1]);
        }
        elsif ($cmd =~ /^(m|msg)$/)		# private message
        {
            $tolist = $tok[1];
            @to = split(/,/, $tolist);
            $msg = do_correction((&strip_token((&strip_token($input))[1]))[1]);
            $msg = sprintf ('[CC: %s] %s', $tolist, $msg) if ($tolist =~ /,/ && $cc_msg_list);

            send_private($connection, $msg, @to);

            icb_print ($output_window, 'output', "%s\n", $input) if ($echo_outgoing);
            refresh($input_window);
        }
        elsif ($cmd eq 'write')
        {
            $to = $tok[1];
            $msg = do_correction((&strip_token((&strip_token($input))[1]))[1]);
            foreach $out (split_output("write $to", $msg, 0))
            {
                $connection->sendpriv ('server', join(' ', 'write', $to, $out));
            }
            refresh($input_window);
        }
        elsif ($cmd =~ /^(s|status)$/)
        {
            $connection->sendpriv ('server', $tok[1] ? join(' ', 'status', (&strip_token($input))[1])
                                                     : 'status');
            icb_print ($output_window, 'output', "%s\n", $input);
            refresh($input_window);
        }
        elsif ($cmd =~ /^(b|boot)$/)
        {
            if (scalar @tok > 1)
            {
                $connection->sendpriv ('server', join(' ', 'boot', (&strip_token($input))[1]));
            }
            refresh($input_window);
        }
        elsif ($cmd =~ /^(setup)$/)		# encryption setup
        {
            if ($encryption)
            {
                $tolist = $tok[1];
                @to = split(/,/, $tolist);
                $msg = sprintf('%s%s',
                               $EICB_CRYPT_PREFIX,
                               $EICB_DH_INIT);

                foreach my $to (@to)
                {
                    $connection->sendpriv ($to, $msg);
                }
            }
            elsif ($encryption_avail)
            {
                icb_print ($output_window, 'error', "[=Error=] Encryption is not enabled in the defaults file.\n");
                icb_print ($output_window, 'error', "[=Error=] Please enable it and restart ICBM.\n");
            }
            else
            {
                icb_print ($output_window, 'error', "[=Error=] Encryption cannot be enabled without installing more modules.\n");
                icb_print ($output_window, 'error', "[=Error=] Please see the documentation for required modules.\n");
            }
            refresh($input_window);
        }
        elsif ($cmd =~ /^(revoke)$/)		# end encrypted session
        {
            if ($encryption)
            {
                $tolist = $tok[1];
                @to = split(/,/, $tolist);
                $msg = sprintf('%s%s',
                               $EICB_CRYPT_PREFIX,
                               $EICB_SESSION_END);

                foreach my $to (@to)
                {
                    if (defined $session_keys{lc($to)})
                    {
                        $connection->sendpriv ($to, $msg);
                        undef($session_keys{lc($to)});
                        timestamp(time()) if ($timestamps_active);
                        icb_print ($output_window, 'sbrkt',  "%s", "[=");
                        icb_print ($output_window, 'status', "%s", "SECURE");
                        icb_print ($output_window, 'sbrkt',  "%s", "=]");
                        icb_print ($output_window, 'status', " Session key for user %s revoked\n", $to);
                    }
                }
            }
            elsif ($encryption_avail)
            {
                icb_print ($output_window, 'error', "[=Error=] Encryption is not enabled in the defaults file.\n");
                icb_print ($output_window, 'error', "[=Error=] Please enable it and restart ICBM.\n");
            }
            else
            {
                icb_print ($output_window, 'error', "[=Error=] Encryption cannot be enabled without installing more modules.\n");
                icb_print ($output_window, 'error', "[=Error=] Please see the documentation for required modules.\n");
            }
            refresh($input_window);
        }
        else				# make all other commands fall through for now
        {
            $tabhist{lc($tok[1])} = time() if ($cmd eq 'beep');
            $connection->sendpriv ('server', substr($input, 1));
            $messages = 0 if ($cmd eq 'read');
            icb_print ($output_window, 'output', "%s\n", $input) if ($cmd eq 'exclude' && $echo_outgoing);
            refresh($input_window);
        }
    }
    else				# anything else is open message
    {
        $input =~s/^$cmdchar{2}/$cmdchar/;

        if ($query)			# are we in query/personalto mode?
        {
            my @to = split(/,/, $query);
            $msg = do_correction($input);
            $msg = sprintf ('[CC: %s] %s', $query, $msg) if ($query =~ /,/ && $cc_msg_list);

            send_private($connection, $msg, @to);

            if ($echo_outgoing)
            {
                icb_print ($output_window, 'warning', "-> %s :", $query);
                icb_print ($output_window, 'output', " %s\n", $input);
            }
        }
        else
        {
					# allow any first character to be escaped as a literal
            $input = substr($input, 1) if (substr($input,0,1) eq $escchar);

            foreach my $out (split_output('', do_correction($input), 0))
            {
                $connection->sendopen($out);
            }
            icb_print ($output_window, 'output', "%s\n", $input) if ($echo_outgoing);
        }
        refresh($input_window);
    }

    return ($ret);
}


sub send_private
{
    my ($connection, $msg, @to) = @_;
    my $r;

    foreach my $to (@to)
    {
        $tabhist{lc($to)} = time();
        my $outgoing = $msg;
        presend(\$outgoing, $to);
        if ($encryption && defined($session_keys{lc($to)}))
        {
            encrypt(\$outgoing, $cipher, $session_keys{lc($to)});
            foreach my $out (split_output($to, $outgoing, length($cur_nick)))
            {
                $out = sprintf('%s %s',
                               $EICB_CRYPT_PREFIX,
                               $out);
                $connection->sendpriv ($to, $out);
            }
            $connection->sendpriv ($to, $EICB_CRYPT_PREFIX);
        }
        else
        {
            foreach my $out (split_output($to, $outgoing, 0))
            {
                $connection->sendpriv ($to, $out);
            }
        }
    }
}


sub presend
{
	# This function is a placeholder that exists solely
	# to allow users to overload it to perform pre-send
	# processing of message text.
}


sub encrypt
{
    my ($message, $cipher, $key) = @_;
    my ($plaintext, $comptext, $ciphertext, $armortext, $cbc);

    $cbc = Crypt::CBC->new( -key    => $key,
                            -cipher => $cipher);

    $plaintext = $$message;
    $comptext = Compress::Zlib::memGzip($plaintext) || out ("Compress failed!");
    $ciphertext = $cbc->encrypt($comptext) || out ("Encrypt failed!");
    $armortext = MIME::Base64::encode_base64($ciphertext) || out("Armor failed!");
    $$message = $armortext;

    return;
}


sub decrypt
{
    my ($message, $cipher, $key) = @_;
    my ($plaintext, $comptext, $ciphertext, $armortext, $cbc);

    $cbc = Crypt::CBC->new( -key    => $key,
                            -cipher => $cipher);
    $armortext = $$message;
    $ciphertext = MIME::Base64::decode_base64($armortext) || out ("Unarmor failed!");
    $comptext = $cbc->decrypt($ciphertext) || out ("Decrypt failed!");
    $plaintext = Compress::Zlib::memGunzip($comptext) || out ("Decompress failed!");
    $$message = $plaintext;

    return (1);
}


sub do_crypt_command
{
    my ($who, $args, $time) = @_;
    my @args = split(/\s+/, $args);
    my $command = shift (@args);

    if ($command == $EICB_DH_INIT)
    {
        create_public_key($who, $cur_nick, 0);
        $connection->sendpriv($who,
                              sprintf('%s%s %s',
                                      $EICB_CRYPT_PREFIX,
                                      $EICB_DH_REPLY,
                                      $DH_public));
    }
    elsif ($command == $EICB_DH_REPLY)
    {
        $DH_public2 = shift(@args);
        create_public_key($who, $cur_nick, 1);
        $connection->sendpriv($who,
                              sprintf('%s%s %s',
                                      $EICB_CRYPT_PREFIX,
                                      $EICB_DH_REPLY2,
                                      $DH_public));
        create_secret_key($who, $cur_nick, 1);
    }
    elsif ($command == $EICB_DH_REPLY2)
    {
        $DH_public2 = shift(@args);
        create_secret_key($who, $cur_nick, 0);

        my $sha1 = Digest::SHA->new;
        $sha1->add($logbuffer[rand(scalar @logbuffer)]);
        my $key = $sha1->b64digest;

        $session_keys{lc($who)} = $key;
        encrypt(\$key, $cipher, $DH_secret);
        $connection->sendpriv($who,
                                    sprintf('%s%s %s',
                                            $EICB_CRYPT_PREFIX,
                                            $EICB_SESSION_KEY,
                                            $key));

        timestamp($time) if ($timestamps_active);
        icb_print ($output_window, 'sbrkt',  "%s", "[=");
        icb_print ($output_window, 'status', "%s", "SECURE");
        icb_print ($output_window, 'sbrkt',  "%s", "=]");
        icb_print ($output_window, 'status', " Session key for user %s established\n", $who);
        $tabhist{lc($who)} = time();
    }
    elsif ($command == $EICB_SESSION_KEY)
    {
        my $key = shift(@args);
        decrypt(\$key, $cipher, $DH_secret);
        $session_keys{lc($who)} = $key;

        timestamp($time) if ($timestamps_active);
        icb_print ($output_window, 'sbrkt',  "%s", "[=");
        icb_print ($output_window, 'status', "%s", "SECURE");
        icb_print ($output_window, 'sbrkt',  "%s", "=]");
        icb_print ($output_window, 'status', " Session key for user %s established\n", $who);
        $tabhist{lc($who)} = time();
    }
    elsif ($command == $EICB_MISSING_KEY)
    {
        timestamp($time) if ($timestamps_active);
        icb_print ($output_window, 'sbrkt',  "%s", "[=");
        icb_print ($output_window, 'error', "%s", "SECURE");
        icb_print ($output_window, 'sbrkt',  "%s", "=]");
        icb_print ($output_window, 'normal', "%s", " ");
        icb_print ($output_window, 'warning', "Message failed (stale key): Set up new key for user %s\n", $who);
    }
    elsif ($command == $EICB_SESSION_END)
    {
        if (defined $session_keys{lc($who)})
        {
            undef($session_keys{lc($who)});
            timestamp($time) if ($timestamps_active);
            icb_print ($output_window, 'sbrkt',  "%s", "[=");
            icb_print ($output_window, 'status', "%s", "SECURE");
            icb_print ($output_window, 'sbrkt',  "%s", "=]");
            icb_print ($output_window, 'status', " Session key for user %s revoked by %s\n", $cur_nick, $who);
        }
    }
    elsif ($command == $EICB_CANNOT_ENCRYPT)
    {
        timestamp($time) if ($timestamps_active);
        icb_print ($output_window, 'sbrkt',  "%s", "[=");
        icb_print ($output_window, 'error', "%s", "SECURE");
        icb_print ($output_window, 'sbrkt',  "%s", "=]");

        if (defined $session_keys{lc($who)})
        {
            icb_print ($output_window, 'status', " User %s has turned off encryption\n", $who);
        }
        else
        {
            icb_print ($output_window, 'status', " User %s does not have encryption enabled\n", $who);
        }
    }
}


sub create_public_key
{
    my $to = lc($_[0]);
    my $from = lc($_[1]);
    my $player = $_[2];
    my ($DH, $p, $g, $i, $public, @chars);

    @chars = split(//,($player ? $to : $from));
    $g = ord(shift(@chars));
    while (scalar @chars)
    {
        $g += ord(shift(@chars));
    }

    @chars = split(//, ($player ? $from : $to));
    $i = ord(shift(@chars));
    while (scalar @chars)
    {
        $i += ord(shift(@chars));
    }

    $i %= scalar(@EICB_PRIMES);
    $p = $EICB_PRIMES[$i];

    $DH = Crypt::DH::GMP->new(p => $p,
                              g	=> $g);

    $DH->generate_keys;
    $DH_public = $DH->pub_key;
    $DH_private = $DH->priv_key;
}


sub create_secret_key
{
    my $to = lc($_[0]);
    my $from = lc($_[1]);
    my $player = $_[2];
    my ($DH, $p, $g, $i, $public, @chars);

    @chars = split(//,($player ? $to : $from));
    $g = ord(shift(@chars));
    while (scalar @chars)
    {
        $g += ord(shift(@chars));
    }

    @chars = split(//,($player ? $from : $to));
    $i = ord(shift(@chars));
    while (scalar @chars)
    {
        $i += ord(shift(@chars));
    }

    $i %= scalar(@EICB_PRIMES);
    $p = $EICB_PRIMES[$i];

    $DH = Crypt::DH::GMP->new(p        => $p,
                              g	       => $g,
                              priv_key => $DH_private);

    $DH->generate_keys;
    $DH_secret = $DH->compute_secret($DH_public2);
}

# %%% U2FsdG

sub replay
{
    my ($output_window, $input_window, $status_line, @tok) = @_;

    my (@tmpbuffer, @packet, @to, @replaynicks, @ignorenicks, %mask,
        $line, $tok, $sep, $code, $class, $time, $type,
        $first, $last, $start, $count, $err, $i, $ll,
        $starttime, $endtime);

    $ll = scalar @logbuffer;
    ($start, $count, $err) = (0,0,0);
 
    shift(@tok);
    while (@tok)
    {
        $tok = shift(@tok);

        if ($tok =~ /^\d+$/)
        {
            if ($start)
            {
                $err = 1;
                icb_print ($output_window, 'error', "[=Error=] %s\n", "Too many numeric arguments to /replay");
            }
            elsif ($count)
            {
                $start = ($tok > $ll ? $ll : $tok);
            }
            else
            {
                $count = ($tok > $ll ? $ll : $tok);
            }
        }
        elsif ($tok =~ /^\d{2}:\d{2}$/)
        {
            icb_print ($output_window, 'status', "[=REPLAY=] %s\n", "Time argument $tok detected.");
            if ($endtime)
            {
                $err = 1;
                icb_print ($output_window, 'error', "[=Error=] %s\n", "Too many time arguments to /replay");
            }
            elsif ($starttime)
            {
                $endtime = $tok;
            }
            else
            {
                $starttime = $tok;
            }
        }
        else
        {
            $tok =~ s/\\//;
            $code = substr($tok, 0, 1);

            if ($code eq '-')
            {
                push (@ignorenicks, substr($tok, 1));
            }
            elsif ($code eq '+')
            {
                $class = substr($tok, 1);

                if ($class eq 'open')
                {
                    $mask{$M_OPEN} = $class;
                }
                elsif ($class eq 'msg')
                {
                    $mask{$M_PERSONAL} = $class;
                }
                elsif ($class eq 'status')
                {
                    $mask{$M_STATUS} = $class;
                }
                elsif ($class eq 'error')
                {
                    $mask{$M_ERROR} = $class;
                }
                elsif ($class eq 'alert')
                {
                    $mask{$M_ALERT} = $class;
                }
                elsif ($class eq 'output')
                {
                    $mask{$M_CMDOUT} = $class;
                }
                elsif ($class =~ /^class=(\S+)/)
                {
                    foreach $class (split(/,/, lc($1)))
                    {
                        if ($class eq 'open')
                        {
                            $mask{$M_OPEN} = $class;
                        }
                        elsif ($class eq 'msg')
                        {
                            $mask{$M_PERSONAL} = $class;
                        }
                        elsif ($class eq 'status')
                        {
                            $mask{$M_STATUS} = $class;
                        }
                        elsif ($class eq 'error')
                        {
                            $mask{$M_ERROR} = $class;
                        }
                        elsif ($class eq 'alert')
                        {
                            $mask{$M_ALERT} = $class;
                        }
                        elsif ($class eq 'output')
                        {
                            $mask{$M_CMDOUT} = $class;
                        }
                    }
                }
            }
            else
            {
                push (@replaynicks, $tok);
            }
        }
    }

    unless ($err)
    {
        $count = $ll unless ($count);
        $start = 0 if ($start < $count);

        icb_print ($output_window, 'sbrkt', "%s", '[=');
        icb_print ($output_window, 'status', "%s", 'Replay');
        icb_print ($output_window, 'sbrkt', "%s", '=]');
        
        @tmpbuffer = @logbuffer;
        $sep = chr(255);
        @replayctr = (0,0);

        if ($start)
        {
            icb_print ($output_window,
                       'status',
                       " Beginning replay of%s %d packets starting %d packets back%s\n",
                       scalar @replaynicks ? sprintf(" messages to and from %s in", join (', ', @replaynicks)) : '',
                       $count,
                       $start,
                       scalar @ignorenicks ? sprintf(", excluding messages from %s", join (', ', @ignorenicks)) : '');
            $first = $ll - $start;
            $last = $first + $count;
        }
        elsif ($starttime)
        {
            my ($reftime, $hour, $min, @localtime, $starttime_posix, $endtime_posix);
            
            icb_print ($output_window,
                       'status',
                       " Beginning replay of%s packets between %s and %s%s\n",
                       scalar @replaynicks ? sprintf(" messages to and from %s in", join (', ', @replaynicks)) : '',
                       $starttime,
                       $endtime,
                       scalar @ignorenicks ? sprintf(", excluding messages from %s", join (', ', @ignorenicks)) : '');
            
            $first = 0;
            $last = $ll;
            
            $reftime = time();
            @localtime = localtime($reftime);
            (@localtime[2], @localtime[1]) = split(/:/, $starttime);
            $starttime_posix = timelocal_posix(@localtime);
            (@localtime[2], @localtime[1]) = split(/:/, $endtime);
            $endtime_posix = timelocal_posix(@localtime);
            $starttime_posix -= 86400 if ($starttime_posix >= $reftime || $starttime_posix >= $endtime_posix);
            
            for (my $i = 0; $i < $ll; $i++)
            {
                my ($time, undef, undef) = split(/$sep/, $tmpbuffer[$i]);
                
                if ($time < $starttime_posix)
                {
                    $first = $i;
                }
                elsif ($time > $endtime_posix)
                {
                    $last = $i - 1;
                    $first += 1;
                    last;
                }
            }
        }
        else
        {
            icb_print ($output_window,
                       'status',
                       " Beginning replay of%s last %d packets%s\n",
                       scalar @replaynicks ? sprintf(" messages to and from %s in", join (', ', @replaynicks)) : '',
                       $count,
                       scalar @ignorenicks ? sprintf(", excluding messages from %s", join (', ', @ignorenicks)) : '');
            $first = $ll - $count;
            $last = $ll;
        }

        if (scalar keys(%mask))
        {
            icb_print ($output_window, 'sbrkt', "%s", '[=');
            icb_print ($output_window, 'status', "%s", 'Replay');
            icb_print ($output_window, 'sbrkt', "%s", '=]');
            icb_print ($output_window,
                       'status',
                       sprintf(" Including message classes: %s\n",
                               join(', ', sort(values(%mask)))));
        }

        # check for encrypted message at start of replay, because we cannot play
        # back only PART of an encrypted message, we have to play back the entire
        # message

        my $p = $first;
        ($time, $type, @packet) = split(/$sep/, $tmpbuffer[$p]);
        if ($type eq $M_PERSONAL 
            && substr($packet[1],0,3) eq $EICB_CRYPT_PREFIX
            && (length($packet[1]) == 3 || substr($packet[1],4,3) ne 'U2F'))
        {
            # If we got here, replay started in the middle of an encrypted message.
            # We need to move the start point back and look for the beginning of
            # the message.

            while ($p > 0 && !(substr($packet[1],4,3) eq 'U2F'))
            {
                $p--;
                ($time, $type, @packet) = split(/$sep/, $tmpbuffer[$p]);
            }
            if ($p == 0 && !(substr($packet[1],4,3) eq 'U2F'))
            {
                # If we got here, we ran out of logbuffer going backward looking,
                # for the beginning of the encrypted message, and need to skip
                # forward to its end.

                while (substr($packet[1],0,3) eq $EICB_CRYPT_PREFIX)
                {
                    $p++;
                    ($time, $type, @packet) = split(/$sep/, $tmpbuffer[$p]);
                }
            }
            $first = $p;
        }

        # let's drop a timestamp of the start point
        ($time, undef, undef) = split(/$sep/, $tmpbuffer[$first]);
        icb_print ($output_window, 'sbrkt', "%s", '[=');
        icb_print ($output_window, 'status', "%s", 'Replay');
        icb_print ($output_window, 'sbrkt', "%s", '=]');
        icb_print ($output_window,
                   'status',
                   " Playback begins at %s\n",
                   strftime("%H:%M:%S %Z %b %d %Y", 
                            localtime($time)));
        
        $replaying = 1;
        for ($i = $first; $i < $last; $i++)
        {
            ($time, $type, @packet) = split(/$sep/, $tmpbuffer[$i]);
            my $cnick = lc($packet[0]);
            my $tnick = ($packet[1] =~ /^<\*to: (\S+)\*> (.*)/)
                      ? lc($1)
                      : '----NOUSER----';

	# if an ignore list was specified, skip nicks we were told to ignore

            if (scalar @ignorenicks)
            {
                next if (grep(/^$cnick$/, @ignorenicks));
            }

	# if a message class mask was defined, skip message types not listed in the mask

            if (scalar keys(%mask))
            {
                next unless (defined($mask{$type})				# message type matches
                             || ($type eq $M_CMDOUT				# OR, it's a msgto and
                                 && defined $mask{$M_PERSONAL}			# we want class msg
                                 && !($tnick eq '----NOUSER----')))
            }


	# if an explicit include list was specified, we now print ONLY messages to and
	# from nicks on the explicit include list.  Otherwise, we print everything left
	# after processing the type mask and ignore list.
            if (scalar @replaynicks)
            {
                if (grep(/^$cnick$/, @replaynicks) || grep(/^$tnick$/, @replaynicks))
                {
                    buffered_replay($output_window, $input_window, $status_line, $time, $type, @packet);
                }
                elsif ($type == $M_CMDOUT
                       && $cnick =~ /co/
                       && (scalar keys(%mask) == 0 || defined($mask{$M_CMDOUT})))
                {
                    (undef, $cnick, undef) = split(/\*/, $packet[1], 3);
                    ($cnick = lc($cnick)) =~ s/^to: //;
                    if (grep(/^$cnick$/, @replaynicks))
                    {
                        buffered_replay($output_window, $input_window, $status_line, $time, $type, @packet);
                    }
                }
            }
            else
            {
                buffered_replay($output_window, $input_window, $status_line, $time, $type, @packet);
            }
        }
        $replaying = 0;


        if ($replayctr[0] == $replayctr[1])
        {

        # ...and we'll timestamp the end point too

            ($time, undef, undef) = split(/$sep/, $tmpbuffer[$last-1]);
            icb_print ($output_window, 'sbrkt', "%s", '[=');
            icb_print ($output_window, 'status', "%s", 'Replay');
            icb_print ($output_window, 'sbrkt', "%s", '=]');
            icb_print ($output_window,
                       'status',
                       " Playback ends at %s\n",
                       strftime("%H:%M:%S %Z %b %d %Y", 
                                localtime($time)));

            icb_print ($output_window, 'sbrkt', "%s", '[=');
            icb_print ($output_window, 'status', "%s", 'Replay');
            icb_print ($output_window, 'sbrkt', "%s", '=]');
            icb_print ($output_window, 'status', " End of replay.\n");
            @replayctr = (0,0);
        }
    }
}


sub buffered_replay
{
    my ($output_window, $input_window, $status_line, $time, $type, @packet) = @_;

    $replayctr[0]++;

    if (scalar @pagebuffer || $page[2])
    {
        push (@pagebuffer, join(chr(255), $time, $type, @packet));
    }
    else
    {
        $replayctr[1]++;
        parse_packet ($output_window, $input_window, $status_line, $time, $type, @packet);
    }
}


sub set_pagesize
{
    my ($output_window, @tok) = @_;

    if (scalar @tok == 1)
    {
        icb_print ($output_window, 'sbrkt',  "%s", '[=');
        icb_print ($output_window, 'status', "%s", "Client");
        icb_print ($output_window, 'sbrkt',  "%s", '=]');
        if ($page[0])
        {
            icb_print ($output_window, 'status', " Pagesize currently set to %d lines.\n", $page[0]);
        }
        else
        {
            icb_print ($output_window, 'status', " Paging is currently disabled.\n", '');
        }
    }
    elsif (scalar @tok == 2)
    {
        unless ($tok[1] =~ /^\d+$/)
        {
            do_beep();
            icb_print ($output_window, 'sbrkt',  "%s", '[=');
            icb_print ($output_window, 'error',  "%s", "Error");
            icb_print ($output_window, 'sbrkt',  "%s", '=]');
            icb_print ($output_window, 'status', " Non-numeric pagesize.\n", '');
        }
        elsif ($tok[1] < 0)
        {
            do_beep();
            icb_print ($output_window, 'sbrkt',  "%s", '[=');
            icb_print ($output_window, 'error',  "%s", "Error");
            icb_print ($output_window, 'sbrkt',  "%s", '=]');
            icb_print ($output_window, 'status', " Pagesize cannot be negative.\n", '');
        }
        elsif ($tok[1] == 0)
        {
            $page[0] = 0;
            @replayctr = (0,0);
            icb_print ($output_window, 'sbrkt',  "%s", '[=');
            icb_print ($output_window, 'status', "%s", "Client");
            icb_print ($output_window, 'sbrkt',  "%s", '=]');
            icb_print ($output_window, 'status', " Paging now disabled.\n", '');
            unpause() if ($page[2]);
        }
        else
        {
            $page[0] = $tok[1];
            icb_print ($output_window, 'sbrkt',  "%s", '[=');
            icb_print ($output_window, 'status', "%s", "Client");
            icb_print ($output_window, 'sbrkt',  "%s", '=]');
            icb_print ($output_window, 'status', " Pagesize now set to %d lines.\n", $page[0]);
            unpause() if ($page[2] && $page[1] < $page[0]);
        }
    }
    else
    {
        do_beep();
    }
}


sub listen
{
    my ($output_window, $input_window, $status_line, $connection) = @_;
    my ($type, @packet);

    socket(SOCKET, PF_UNIX, SOCK_DGRAM, 0)   || die "Output thread cannot open socket";
    connect(SOCKET, sockaddr_un($logsocket)) || die "Output thread cannot connect to log socket";

    while ($input_thread_running > 0 && $exit_state == 0)
    {
        $output_window = resize_output_window($output_window) if ($sizechanged);
        
        my $rin = '';
        my $rout;
        vec($rin, fileno($connection->fd()), 1) = 1;

        until (scalar @packet || $exit_state > 0 || $input_thread_running < 1)
        {
            # do we need to reload any hook files?
            if ($reload_hookfile)
            {
                load($reload_hookfile);
                $reload_hookfile = '';
            }

            # do we need to delete any hooks?
            if (scalar @delhook_cmd)
            {
                delhook(@delhook_cmd);
                @delhook_cmd = ();
            }

            select($rout = $rin, undef, undef, 0.01);
            if (vec($rout, fileno($connection->fd()), 1))
            {
                my $time = time();
                ($type, @packet) = $connection->readmsg();
                if ($connection->error())
                {
                    $exit_state = lost_connection($output_window, $connection, $time) unless ($exit_state);
                }
                elsif ($type eq $M_EXIT)
                {
                    $exit_state = 1;
                }
                else
                {
                    if ($readhistsize && !($packet[1] =~ /$EICB_CRYPT_PREFIX\d/))
                    {
                        push (@logbuffer, join(chr(255), $time, $type, @packet));
                    }

                    shift(@logbuffer) while ((my $ll = @logbuffer) > $readhistsize);
                    if (scalar @pagebuffer || $page[2])
                    {
                        push (@pagebuffer, join(chr(255), $time, $type, @packet));
                    }
                    else
                    {
                        parse_packet ($output_window, $input_window, $status_line, $time, $type, @packet);
                        refresh($input_window);
                    }
                }
            }
        }
        @packet = ();
        threads->yield;
    }

    shutdown(SOCKET, 1);
}


sub lost_connection
{
    my ($output_window, $connection, $time) = @_;

    beep();
    timestamp($time) if ($timestamps_active);
    icb_print ($output_window,
                'error',
                "%s\n%s\n",
                '[=FAILURE=] ICB server connection terminated unexpectedly!',
                $connection->error());
    sleep (5);
    icb_print ($output_window,
                'warning',
                "%s\n",
                'Press Ctrl+C to exit.');
    $input_thread_running = -1;
    $quitmsg = 'Session terminated: Lost connection with ICB server';
    return (2);
}


sub parse_packet
{
    my ($output_window, $input_window, $status_line, $time, $type, @packet) = @_;

    do_hooks('rawmsg', @packet);
    do_hooks('trigger', @packet) if (&get_trig_status() == 1);

    if ($type eq $M_OPEN)
    {
        foreach my $line (split_line($packet[0], 2, $packet[1]))
        {
            timestamp ($time) if ($timestamps_active);
            icb_print ($output_window, 'abrkt', "%s", '<');
            icb_print ($output_window, 'nickname', "%s", $packet[0]);
            icb_print ($output_window, 'abrkt', "%s", '>');
            icb_print ($output_window, ($packet[0] eq $cur_nick ? 'ownmsg' : 'normal'), " %s\n", $line);
        }
        grab_URL(@packet);
        do_hooks('openmsg', @packet);
    }
    elsif ($type eq $M_PERSONAL)
    {
        my $decrypt = 0;
        my $show_msg = 1;

        if ($packet[1] =~ /^$EICB_CRYPT_PREFIX/)
        {
            $show_msg = 0;


            if ($packet[1] =~ /^$EICB_CRYPT_PREFIX (\S+)$/)
            {
                $ciphertext{lc($packet[0])} .= $1;
            }
            elsif ($encryption)
            {
                if ($packet[1] =~ /^$EICB_CRYPT_PREFIX(\d.*)$/)
                {
                    do_crypt_command($packet[0], $1, $time);
                }
                elsif ($packet[1] eq $EICB_CRYPT_PREFIX)
                {
                    if (defined($session_keys{lc($packet[0])}))
                    {
                        decrypt(\$ciphertext{lc($packet[0])},
                                $cipher,
                                $session_keys{lc($packet[0])});
                        $packet[1] = $ciphertext{lc($packet[0])};
                        $decrypt = 1;
                        $show_msg = 1;
                    }
                    else
                    {
                        timestamp($time) if ($timestamps_active);
                        icb_print ($output_window, 'sbrkt', "%s", '[=');
                        icb_print ($output_window, 'error', "%s", "SECURE");
                        icb_print ($output_window, 'sbrkt', "%s", '=]');
                        icb_print ($output_window, 'normal', "%s", ' ');
                        icb_print ($output_window,
                                   'warning',
                                   "Unable to decrypt encrypted packet from %s : No valid session key\n",
                                   $packet[0]);
                        $connection->sendpriv($packet[0],
                                              sprintf('%s%s',
                                                      $EICB_CRYPT_PREFIX,
                                                      $EICB_MISSING_KEY));
                        $show_msg = 0;
                    }
                    undef($ciphertext{lc($packet[0])});
                }
            }
            else
            {
                timestamp($time) if ($timestamps_active);
                icb_print ($output_window, 'sbrkt', "%s", '[=');
                icb_print ($output_window, 'alert', "%s", "SECURE");
                icb_print ($output_window, 'sbrkt', "%s", '=]');
                icb_print ($output_window, 'normal', "%s", ' ');

                if ($packet[1] eq $EICB_CRYPT_PREFIX)		# inappropriate encrypted packet
                {
                    undef($ciphertext{lc($packet[0])});
                    icb_print ($output_window,
                               'status',
                               "Received an encrypted packet from %s, but encryption is disabled.\n",
                               $packet[0]);
                }
                else						# inappropriate EICB_DH_INIT
                {
                    icb_print ($output_window,
                               'status',
                               "%s requested an encrypted session key, but encryption is not enabled.\n",
                               $packet[0]);
                }

                $connection->sendpriv(lc($packet[0]),
                                      sprintf('%s%s',
                                              $EICB_CRYPT_PREFIX,
                                              $EICB_CANNOT_ENCRYPT));
            }
        }

        if ($show_msg)
        {
            do_hooks('newpriv', @packet) unless (defined $tabhist{lc($packet[0])});
            $tabhist{lc($packet[0])} = time();
            foreach my $line (split_line($packet[0], 4, $packet[1]))
            {
                timestamp($time) if ($timestamps_active);
                my ($fromcolor, $bodycolor);
                my $sender = lc($packet[0]);
                if (grep(/^$sender$/, @hilightnicks))
                {
                    $fromcolor = 'hilightfrom';
                    $bodycolor = $decrypt ? 'encrypted' : 'hilight';
                }
                else
                {
                    $fromcolor = 'persfrom';
                    $bodycolor = $decrypt ? 'encrypted' : 'personal';
                }
                icb_print ($output_window, 'pbrkt', "%s", '<*');
                icb_print ($output_window, $fromcolor, "%s", $packet[0]);
                icb_print ($output_window, 'pbrkt', "%s", '*>');
                icb_print ($output_window, 'normal', "%s", ' ');
                icb_print ($output_window, $bodycolor, "%s\n", $line);
            }
            grab_URL(@packet);
            do_hooks('privmsg', @packet);
        }
    }
    elsif ($type eq $M_STATUS)
    {
        timestamp($time) if ($timestamps_active);
        icb_print ($output_window, 'sbrkt', "%s", '[=');
        icb_print ($output_window, 'status', "%s", $packet[0]);
        icb_print ($output_window, 'sbrkt', "%s", '=]');
        icb_print ($output_window, 'normal', "%s", ' ');
        icb_print ($output_window, 'status', "%s\n", $packet[1]);
        if ($packet[1] =~ /in group (\S+)/)
        {
            $cur_group = $1;
            $cur_mod = ($packet[1] =~ /as moderator/) ? 1 : 0;
            do_hooks('group', @packet);
            do_hooks('modgain', @packet) if ($cur_mod);
        }
        elsif ($packet[1] =~ /\b(.+?) has passed moderation/ && $1 eq $cur_nick)
        {
            $cur_mod = 0;
            do_hooks('modpass', @packet);
        }
        elsif ($packet[1] =~ /\b(.+?) just relinquished the mod/ && $1 eq $cur_nick)
        {
            $cur_mod = 0;
            do_hooks('modpass', @packet);
        }
        elsif ($packet[1] =~ /falls on you, dislodging moderat/)
        {
            $cur_mod = 0;
            do_hooks('modloss', @packet);
        }
	# Avoids "Nested quantifiers in regex" exception when group or user name is naughty, like "+++ATH0" (those wacky atbotters)
        elsif (($packet[1] =~ /just passed you moderation of group (.+?)\s*$/ && $1 eq $cur_group) ||
               ($packet[1] =~ /\b(.+?) is now mod/ && $1 eq $cur_nick) ||
               ($packet[1] =~ /\byou are (the )?moderator/i))
        {
            $cur_mod = 1;
            do_hooks('modgain', @packet);
        }
        elsif ($packet[1] =~ /renamed group to (\S+).$/)
        {
            $cur_group = $1;
            do_hooks('rename', @packet);
        }
        elsif ($packet[1] =~ /^\s*(.+?) changed nickname to (\S+)\b/ && $1 eq $cur_nick)
        {
            $cur_nick = $2;
        }
        elsif ($packet[1] =~ /\b(\S+) changed nickname to (\S+)\b/)
        {
            my ($old, $new) = ($1, $2);
            if ($encryption && defined($session_keys{lc($old)}))
            {
                $session_keys{lc($new)} = $session_keys{lc($old)};
                undef($session_keys{lc($old)});

                icb_print ($output_window, 'sbrkt', "%s", '[=');
                icb_print ($output_window, 'status', "%s", "Status");
                icb_print ($output_window, 'sbrkt', "%s", '=]');
                icb_print ($output_window, 'normal', "%s", ' ');
                icb_print ($output_window, 'status', "%s\n", "Session key for $old updated (now $new)");
            }
            do_hooks('nickchg', @packet);
        }
        elsif ($packet[1] =~ /booted you./)
        {
            do_hooks('boot', @packet);
        }
        elsif ($packet[0] eq 'Sign-on' || $packet[0] eq 'Arrive')
        {
            do_hooks('join', @packet);
        }
        elsif ($packet[0] eq 'Sign-off' || $packet[0] eq 'Depart')
        {
            do_hooks('leave', @packet);
        }
        elsif ($packet[0] eq 'Notify-On' || $packet[0] eq 'Notify-Off')
        {
            do_hooks('notify', @packet);
        }
        elsif ($packet[0] eq 'Idle-Boot')
        {
            do_hooks('idleboot', @packet);
        }
        elsif ($packet[0] eq 'Message' && ($packet[1] =~ /You have (\d+) message/))
        {
            $messages = $1;
            do_hooks('memo', @packet);
        }
        else
        {
            do_hooks('status', @packet);
        }
    }
    elsif ($type eq $M_ERROR)
    {
        timestamp($time) if ($timestamps_active);
        if ($packet[0] =~ /^(\S+) not signed on|User not found/)
        {
            icb_print ($output_window, 'warning', "[=AWOL=] %s\n", $packet[0]);
            delete ($tabhist{lc($1)}) if ($packet[0] =~ /^(\S+) not signed on/ && $tab_del_on_error);
            do_hooks('awol', @packet);
        }
        else
        {
            icb_print ($output_window, 'error', "[=ERROR=] %s\n", $packet[0]);
            do_hooks('error', @packet);
        }
    }
    elsif ($type eq $M_ALERT)
    {
        timestamp($time) if ($timestamps_active);
        icb_print ($output_window, 'sbrkt', "%s", '[=');
        icb_print ($output_window, 'alert', "%s", $packet[0]);
        icb_print ($output_window, 'sbrkt', "%s", '=]');
        icb_print ($output_window, 'normal', "%s", ' ');
        icb_print ($output_window, 'alert', "%s\n", $packet[1]);
        do_hooks('alert', @packet);
    }
    elsif ($type eq $M_CMDOUT)
    {
        my $subtype = shift(@packet);
        if ($subtype eq 'wl')
        {
                if ($packet[0] eq 'm')
                {
                    icb_print($output_window, 'normal', '%s', '  ');
                    icb_print($output_window, 'modstar', '%s', '*');
                }
                else
                {
                    icb_print($output_window, 'normal', '%s', '   ');
                }
                icb_print ($output_window, 'nickname', "%-12s", $packet[1]);
                icb_print ($output_window, 'normal', '%s', ' ');
                icb_print ($output_window, 'idletime', "%10s", calc_idletime($packet[2]));
                icb_print ($output_window, 'normal', '%s', ' ');
                icb_print ($output_window, 'logintime', "%8s", calc_logintime($packet[4]));
                icb_print ($output_window, 'normal', '%s', ' ');
                icb_print ($output_window, 'hostmask', "%s", $packet[5]);
                icb_print ($output_window, 'hostmask', "@%s", $packet[6]);
                icb_print ($output_window, 'normal', '%s', ' ');
                icb_print ($output_window, 'userflags', "%s", $packet[7]);
                icb_print ($output_window, 'normal', '%s', "\n");
        }
        elsif ($subtype eq 'wh')
        {
            icb_print ($output_window,
                        'who_hdr',
                        "   %-12s %10s %7s  %s\n",
                        'Nickname',
                        'Idle',
                        'Sign-On',
                        'Account');
        }
        elsif ($subtype eq 'co')			# subtype co can be three different things.
        {
            if ($packet[0] =~ /^<\*(to: \S+)\*> (.*)/)	# outgoing personal message, echoed back
            {
                my ($who, $what) = ($1, $2);
                my $crypt = 0;

                if ($encryption)
                {
                    my $realwho = lc($1) if ($who =~ /^to: (\S+)$/);
                    if ($what =~ /^$EICB_CRYPT_PREFIX (\S+)$/)
                    {
                        $ciphertext{$who} .= $1;
                        return;
                    }
                    elsif ($what eq $EICB_CRYPT_PREFIX)
                    {
                        if (defined($session_keys{$realwho}))
                        {
                            decrypt(\$ciphertext{$who},
                                    $cipher,
                                    $session_keys{$realwho});
                            $what = $ciphertext{$who};
                            $crypt = 1;
                        }
                        else
                        {
                            icb_print ($output_window,
                                       'error',
                                       "[=Crypto=] Unable to decrypt encrypted packet from %s : No session key\n",
                                       $realwho);
                        }
                        undef($ciphertext{$who});
                    }
                    elsif ($what =~ /^$EICB_CRYPT_PREFIX\d$/
                        || $what =~ /^$EICB_CRYPT_PREFIX\d\s\S+/)
                    {
                        return;
                    }
                }

                if ($packet[0] =~ /^<\*(to: \S+)\*> $EICB_CRYPT_PREFIX/ && !$encryption)
                {
                    unless ($packet[0] =~ /$EICB_CANNOT_ENCRYPT/)
                    {
                        # If we got here, then this is an echoback of an encrypted packet
                        # and we don't have encryption enabled, which is really weird.

                        timestamp($time) if ($timestamps_active);
                        icb_print ($output_window, 'sbrkt', "%s", '[=');
                        icb_print ($output_window, 'warning', "%s", 'SECURE');
                        icb_print ($output_window, 'sbrkt', "%s", '=]');
                        icb_print ($output_window, 'normal', "%s", ' ');
                        icb_print ($output_window, 'status', "Anomalous encrypted echoback packet received!\n", '');
                    }
                }
                else
                {
                    unless ($who =~ /^to: server$/i && $display_server_messages == 0)
                    {
                        foreach my $line (split_line($who, 7, $what))
                        {
                            timestamp($time) if ($timestamps_active);
                            icb_print ($output_window, 'pbrkt', "%s", '<*');
                            icb_print ($output_window, 'persfrom', "%s", $who);
                            icb_print ($output_window, 'pbrkt', "%s", '*>');
                            icb_print ($output_window, 'normal', "%s", ' ');
                            icb_print ($output_window,
                                       $crypt ? 'encrypted' : 'personal',
                                       "%s\n",
                                       $line);
                        }
                    }
                }

            }
            elsif ($packet[0] =~ /^Group: /)		# who display header line
            {
                icb_print ($output_window,
                            'who_hdr',
                            "%s\n",
                            $packet[0]);
            }
            elsif ($packet[0] =~ /^\w+(-\w+)?( \w+)?:/)	# whois or status output
            {
                # whois fields

                if ($packet[0] =~ /^(Nickname:\s+)(\S+)\s+(Address:\s+)(\S+)$/i)
                {
                    my @fields = ($1,$2,$3,$4);
                    icb_print ($output_window, 'status', "%-15s", $fields[0]);
                    icb_print ($output_window, 'normal', "%s\n", $fields[1]);
                    icb_print ($output_window, 'status', "%-15s", $fields[2]);
                    icb_print ($output_window, 'normal', "%s\n", $fields[3]);
                }
                elsif ($packet[0] =~ /^(Phone Number:\s+)(\S+.*\s+)(Real Name:\s+)(\S+.*)$/i
                    || $packet[0] =~ /^(Last signon:\s+)(\S+.*\s+)(Last signoff:\s+)(\S+.*)$/i)
                {
                    my @fields = ($1,$2,$3,$4);
                    icb_print ($output_window, 'status', "%-15s", $fields[0]);
                    icb_print ($output_window, 'normal', "%s\n", $fields[1]);
                    icb_print ($output_window, 'status', "%-15s", $fields[2]);
                    icb_print ($output_window, 'normal', "%s\n", $fields[3]);
                }
                elsif ($packet[0] =~ /^(\w+:\s+)(\S+.*)$/i
                    || $packet[0] =~ /^(.*:\s+)(\S+\@\S+)$/i)
                {
                    my @fields = ($1,$2);
                    icb_print ($output_window, 'status', "%-15s", $fields[0]);
                    icb_print ($output_window, 'normal', "%s\n", $fields[1]);
                    grab_URL(($packet[0],$fields[1]));
                }
                elsif ($packet[0] =~ /^Street Address:$/i)
                {
                    icb_print ($output_window, 'status', "%s\n", $packet[0]);
                }
                elsif ($packet[0] =~ /^(Proto Level: \d+) (Max Users: \d+) (Max Groups: \d+)$/i)	# protocol level packet
                {
                    icb_print ($output_window, 'status', "%-20s%-20s%-20s\n", $1, $2, $3);
                }

                # status fields

                elsif ($packet[0] =~ /^(Name:\s+)(\S+.*\s+)(Mod:\s+)(\S+\s+)(\(.*\))$/i)
                {
                    my @fields = ($1,$2,$3,$4,$5);
                    icb_print ($output_window, 'status', "%-15s", $fields[0]);
                    icb_print ($output_window, 'normal', "%s\n", $fields[1]);
                    icb_print ($output_window, 'status', "%-15s", $fields[2]);
                    icb_print ($output_window, 'normal', "%s\n", $fields[3]);
                    icb_print ($output_window, 'status', "%-15s", 'Flags:');
                    icb_print ($output_window, 'normal', "%s\n", $fields[4]);
                }
                elsif ($packet[0] =~ /^(Idle\S+:\s+)(\S+.*)$/i)
                {
                    my @fields = ($1,$2);
                    icb_print ($output_window, 'status', "%-15s", $fields[0]);
                    icb_print ($output_window, 'normal', "%s\n", $fields[1]);
                }

                else
                {
                    icb_print ($output_window,		# This should never be executed
                                'alert',
                                "You should never see this.  Please inform the author.  The packet that triggered this code contained: '%s'\n",
                                $packet[0]);
                }
            }
            else
            {
                icb_print ($output_window,		# normal server output text such as MOTD
                            'normal',
                            "%s\n",
                            $packet[0]);
            }
        }
    }
    elsif ($type eq $M_BEEP)
    {
        $tabhist{lc($packet[0])} = time();
        beep();
        timestamp($time) if ($timestamps_active);
        icb_print ($output_window, 'beep', "%s ", "[=Beep=]");
        icb_print ($output_window, 'beepfrom', "%s", "$packet[0]");
        icb_print ($output_window, 'beep', " %s\n", "sent you a beep!");
        do_hooks('beep', @packet);
    }
    elsif ($type eq $M_PING)
    {
        do_hooks('ping', @packet);
    }
    else
    {
        icb_print ($output_window, 'normal', "%s\n", join(' ', $type, @packet));
    }
}


sub status
{
    my ($output_window, $input_window, $status_line, $connection) = @_;
    my $time;

    while ($input_thread_running > 0)
    {
        $status_line = resize_status_line($status_line) if ($sizechanged);
        
        select(undef, undef, undef, .01);
        if (($time = time()) != $timeval)
        {
            $timeval = $time;
            set_status($output_window, $input_window, $status_line);
        }
        threads->yield;
    }
}


sub set_init_status
{
    my ($output_window, $input_window, $status_line, $server) = @_;
    my ($status, $statfmt);

    $statfmt = "ICBM >>>  Connecting to server %s ... %s ";
    $status = sprintf($statfmt,
                      $server,
                      ' ' x (COLS() - (length($statfmt) + length($server) - 4)));

    update_status_line($output_window, $input_window, $status_line, $status);
}


sub set_status
{
    my ($output_window, $input_window, $status_line) = @_;
    my ($statfmt, $fsize, $status, $mod, $group, $time, $server);

    ($fsize, $statfmt) = ($page[2] || scalar @pagebuffer)	# are we paused?
                       ? (43, "ICBM >>>  %s %sin group %s on server %s [PAUSED] %s %s ")
                       : (34, "ICBM >>>  %s %sin group %s on server %s %s %s ");

    $mod = $cur_mod ? 'is MOD ' : '';
    $group = $cur_group || '<none>';
    $time = strftime($timeformat, localtime(time()));

    $status = sprintf ($statfmt,
                       $cur_nick,
                       $mod,
                       $group,
                       $options{server},
                       embed_query($fsize, $cur_nick, $mod, $group, $options{server}, $time),
                       $time);

    update_status_line($output_window, $input_window, $status_line, $status);
}

    # We can't actually use icb_print here because we can't have nested semaphore uses,
    # and if set_status calls icb_print to set the status line, then icb_print()'s critical
    # section will fall inside set_status()'s critical section and will deadlock.  Also,
    # we don't want status line updates to ever be logged.


sub update_status_line
{
    my ($output_window, $input_window, $status_line, $status) = @_;

    lock ($output_sem);

# Since we can't seem to identify and kill the cross-window
# display corruption, we can at least make sure we clean up
# the status line each time we update it.

# We MIGHT not need this any more.  I'm going to disable it
# for testing, after release...

    erase ($status_line);

    if ($use_color)
    {
        attron ($status_line, COLOR_PAIR($colors{'statline'})) if ($colors{'statline'});
        attron ($status_line, A_BOLD) if ($attr{'statline'} & 1);
        attron ($status_line, A_REVERSE) if ($attr{'statline'} & 2);
    }

    addstr($status_line, $status);

    if ($use_color)
    {
        attroff ($status_line, COLOR_PAIR($colors{'statline'})) if ($colors{'statline'});
        attroff ($status_line, A_BOLD) if ($attr{'statline'} & 1);
        attroff ($status_line, A_REVERSE) if ($attr{'statline'} & 2);
    }

    refresh($status_line);
    refresh($input_window);
}




sub embed_query
{
    my ($fsize, $cur_nick, $mod, $group, $server, $time) = @_;
    my ($bar, $q, $l);

    $l = COLS() - ($fsize + length($cur_nick.$mod.$group.$server.$time));
    $bar = '-' x $l;

    if (length($query) > 0)
    {
        $q = sprintf ('[Q: %s]', $query);
        if ($l > length($query)+7)
        {
            $bar = sprintf ('%s%s-',
                            '-' x ($l - length($q) - 1),
                            $q);
        }
        elsif ($l >8)
        {
            $bar = sprintf ('%s%s-',
                            '-' x ($l - 8),
                            '[QUERY]');
        }
        elsif ($l >4)
        {
            $bar = sprintf ('%s%s-',
                            '-' x ($l - 4),
                            '[Q]');
        }
    }

    if ($messages)
    {
        my $ms = sprintf('-[%d]-', $messages);
        my $ml = length($ms);
        my $bl = length($bar);

        if (substr($bar, 0, $ml) eq '-' x $ml)
        {
            $bar = substr($bar, $ml).$ms;
        }
    }

    return ($bar);
}


#
#  strip_token is a command-parsing function which returns a list
#  containing two elements: the first token of the input, and the
#  rest of the input.
#

sub strip_token
{
    my ($in) = @_;
    my ($out, $tok);

    if ($in =~ / /)
    {
        $tok = substr($in, 0, index($in, ' '));
        $out = substr($in, index($in, ' ')+1);
        $out =~ s/^ +//;
    }
    else
    {
        $tok = $in;
        $out = '';
    }

    return(($tok, $out));
}


sub do_correction
{
    my ($string) = @_;

    if ($string =~ /^&&/)
    {
        $string =~ s/^&&\s*//;
    }
    else
    {
        $string =~ s/^\\// if ($string =~ /^\\&/);
        foreach my $orig (sort keys %corrections)
        {
            my $sub = $corrections{$orig};
            my $subcmd = sprintf('$string =~ s/%s/%s/', $orig, $sub);
            eval $subcmd;
        }
    }
    return ($string);
}


sub correct
{
    my $ok = 0;
    my ($orig, $sub);
    
    if ((@_ == 1) && ($_[0] =~ /^\/([^']+)\/([^']+)\/$/))
    {
        ($orig, $sub) = ($1, $2);
        $ok++;
    }
    elsif (@_ == 2)
    {
        $orig = $_[0];
        $sub = $_[1];
        $ok++;
    }
    else
    {
        icb_print ($output_window, 'error', "[=Error=] %s\n", "Invalid syntax for correct() function\n");
        do_beep();
    }

    if ($ok)
    {
        $corrections{$orig} = $sub;
        if ($report_correction_set)
        {
            icb_print ($output_window, 'sbrkt', "%s", '[=');
            icb_print ($output_window, 'status', "%s", 'Correct');
            icb_print ($output_window, 'sbrkt', "%s", '=]');
            icb_print ($output_window, 'status', " '%s' will be corrected to '%s'\n", $orig, $sub);
        }
    }
}


sub grab_URL
{
    my %urls;

    foreach my $url (@urllist)
    {
        $urls{$url} = 1;
    }

    foreach my $token (split(/[\s\[\]\{\}\<\>\'\"]/, $_[1]))  #' (syntax colorizer fix)
    {
        if ($token =~ /^\w+:\/\/\S+\/?$/i)
        {
            unless (defined $urls{$token})
            {
                $urls{$token} = 1;
                out ("Captured URL $token") if ($report_url_capture);
            }
            my @packet = ($_[0], $token);
            do_hooks('url', @packet);
        }   
        elsif ($token =~ /^www(\.\S+\/?)+$/i)
        {
            $token = 'http://'.$token;
            unless (defined $urls{$token})
            {
                $urls{$token} = 1;
                out ("Captured URL $token") if ($report_url_capture);
            }
            my @packet = ($_[0], $token);
            do_hooks('url', @packet);
        }
    }

    @urllist = sort(keys(%urls));
}


sub get_urls
{
    return (@urllist);
}


sub urls
{
    my ($cmd, @args) = split(/\s+/, $_[0]);

    if ($cmd eq 'list' || $cmd eq '')
    {
        my ($urls, @urls);

        if (@args)
        {
            my %urls;
            for (my $i = 0; $i < @args; $i++)
            {
                my $tok = $args[$i];
                my @matches = grep(/$tok/, @urllist);
                foreach my $url (@matches)
                {
                    $urls{$url} = 1;
                }
            }
            $urls = @urls = sort(keys(%urls));
            icb_print ($output_window, 'sbrkt', "%s", '[=');
            icb_print ($output_window, 'status', "%s", 'URLgrab');
            icb_print ($output_window, 'sbrkt', "%s", '=]');
            icb_print ($output_window, 'normal', "%s", ' ');
            icb_print ($output_window, 'status', "%d matching URLs in list\n", $urls);
        }
        else
        {
            $urls = @urls = @urllist;
            icb_print ($output_window, 'sbrkt', "%s", '[=');
            icb_print ($output_window, 'status', "%s", 'URLgrab');
            icb_print ($output_window, 'sbrkt', "%s", '=]');
            icb_print ($output_window, 'normal', "%s", ' ');
            icb_print ($output_window, 'status', "%d URLs in list\n", $urls);
        }

        if ($urls)
        {
            foreach my $url (@urls)
            {
                icb_print ($output_window, 'status', "    %s\n", $url);
            }
        }
    }
    elsif ($cmd eq 'purge' || $cmd eq 'clear')
    {
        @urllist = ();
        icb_print ($output_window, 'sbrkt', "%s", '[=');
        icb_print ($output_window, 'status', "%s", 'URLgrab');
        icb_print ($output_window, 'sbrkt', "%s", '=]');
        icb_print ($output_window, 'normal', "%s", ' ');
        icb_print ($output_window, 'status', "URL list purged\n");
    }
    elsif ($cmd eq 'drop')
    {
        my $oldurls = @urllist;
        foreach my $term (@args)
        {
            if (my $urls = @urllist)
            {
                @urllist = grep(!/$term/i, @urllist);
            }
        }
        $oldurls -= (my $urls = @urllist);
        icb_print ($output_window, 'sbrkt', "%s", '[=');
        icb_print ($output_window, 'status', "%s", 'URLgrab');
        icb_print ($output_window, 'sbrkt', "%s", '=]');
        icb_print ($output_window, 'normal', "%s", ' ');
        icb_print ($output_window, 'status', "%s URLs dropped\n", $oldurls ? $oldurls : 'No');
    }
}


sub curs_to_ptr
{
    my ($win, $ptr) = @_;
    my ($x, $y);

    $y = int ($ptr / COLS());
    $x = $ptr % COLS();
    my $ret = move ($win, $y, $x);
    refresh($win);
}


sub curs_l
{
    my ($win) = @_;
    my $ret = 0;
    my ($x, $y);

    getyx($win, $y, $x);
    if ($x == 0 && $y == 0)
    {
        do_beep();
    }
    else
    {
        if ($x == 0)
        {
            $x = COLS()-1;
            $y--;
        }
        else
        {
            $x--;
        }
        move ($win, $y, $x);
        refresh($win);
        $ret = 1;
    }

    return ($ret);
}


sub curs_r
{
    my ($win) = @_;
    my $ret = 0;
    my ($x, $y);

    getyx($win, $y, $x);

    if (!(inch($win, $y, $x) & A_CHARTEXT))
    {
        do_beep();
    }
    else
    {
        if ($x == COLS()-1)
        {
            $x = 0;
            $y++;
        }
        else
        {
            $x++;
        }
        move ($win, $y, $x);
        refresh($win);
        $ret = 1;
    }
    return ($ret);
}


sub bytabtime
{
    $tabhist{$b} <=> $tabhist{$a}
}


sub findfile
{
    my ($file) = @_;
    my $ret = '';

    # assume all relative paths are relative to $DATADIR unless present in current directory

    if (-f $file)
    {
        $ret = $file;
    }
    else
    {
        my $path = sprintf("%s/%s", $datadir, $file) unless ($file =~ /^\//);
        if (-f $path)
        {
            $ret = $path;
        }
    }

    if ($ret eq '' && $report_load_warnings)
    {
        icb_print ($output_window, 'warning', "[=Load=] File %s not found.\n", $file);
    }

    return ($ret);
}


sub load
{
    my ($file) = @_;
    my $path;

    if (length($path = findfile($file)))
    {
        load_file($path);
    }
}


sub loadhook
{
    my ($file) = @_;
    my $path;

    if ($report_load_warnings)
    {
        icb_print ($output_window, 'status', "[=Hook=] Loading hooks from %s\n", $file);
    }

    if (length($path = findfile($file)))
    {
        if ($tid[0])
        {
            my $tid = (threads->self)->tid();

            if ($tid == 3)
            {
                load_file($path);
                $reload_hookfile = '';
            }
            elsif ($tid == 2)
            {
                $reload_hookfile = $path;
            }
        }
        else
        {
            load_file($path);
        }
    }
}


sub load_file
{
    my ($file) = @_;

    do $file;

    if ((my $err = $@) ne '')
    {
        if ($report_load_errors)
        {
            chomp($err);
            icb_print ($output_window, 'error', "[=Load=] Error in $file : %s\n", $err);
        }
    }
    else
    {
        if ($report_load_success)
        {
            icb_print ($output_window, 'sbrkt',  "%s", "[=");
            icb_print ($output_window, 'status', "%s", "Load");
            icb_print ($output_window, 'sbrkt',  "%s", "=]");
            icb_print ($output_window, 'status', " File %s loaded.\n", $file);
        }
    }
}


sub preload
{
    my ($file) = @_;

    open (SETTINGS, $file);
    while (<SETTINGS>)
    {
        chomp;
        next if (/^\s*#/ || /^$/);

        if (/^writesize\s+(\d+)/i)
        {
            $write_size = $1 if ($1 > 0 and $1 < 11);
        }
        elsif (/^debugmask\s+(\S+)$/i)
        {
            my $mask = $1;
            $mask = oct($mask) if ($mask =~ /^0/);
            $options{debug} = $mask;
        }
        elsif (/^encryption\s+(\d)/i)
        {
            $encryption = 1 if ($1 && $encryption_avail);
        }
        elsif (/^logbuffer\s+(\d+)/i)
        {
            $readhistsize = $1 if ($1 > 100);
        }
        elsif (/^nick\s+(\S+)$/i)
        {
            push (@defnicks, $1);
        }
        elsif (/^(defgroup|defhost|defport|defserver|icbserverdb)\s+(\S+)$/i)
        {
            $options{$1} = $2;
        }
        elsif (/^sleeptime \d+$/i)
        {
            $sleeptime = $1;
        }
        elsif (/^timestamps\s+(on|off)$/i)
        {
            $timestamps_active = ($1 =~ /^on$/i ? 1 : 0);
        }
        elsif (/^timestampformat\s+(\S+)/i)
        {
            $timestampformat = $1 if ($1 =~ /^((\%\w:)+\%\w)$/);	# this is a pretty basic sanity check
        }
    }
    close (SETTINGS);
}


sub timestamp
{
    my ($time) = @_;

    icb_print ($output_window,
                'ownmsg',
                "%s ",
                strftime($timestampformat, localtime($time)));
}


sub split_line
{
    my ($from, $adj, $line) = @_;
    my ($p, @split, $l, $width);

    $adj += length(strftime($timestampformat, localtime(1300000000))) + 1 if ($timestamps_active);
    $width = COLS() - (length($from) + $adj + 2);

    while (length($line) > $width)
    {
        if (($p = rindex($line, ' ', $width)) <= 0)
        {
            ($l, $line) = (substr($line, 0, $width), substr($line, $width-1));
            substr($l, $width-1, 1) = '-';
        }
        else
        {
            ($l, $line) = (substr($line, 0, $p), substr($line, $p+1));
        }
        $l =~ s/\s+$//;
        push (@split, $l);
        $line =~ s/^\s+//;
    }
    push (@split, $line) if (length($line));

    return (@split);
}


sub split_output
{
    my ($to, $line, $adj) = @_;
    my ($p, @split, $l, $width);

    $width = 240 - length($to);
    $width -= $adj if ($adj);

    while (length($line) > $width)
    {
        if (($p = rindex($line, ' ', $width)) <= 0)
        {
            ($l, $line) = (substr($line, 0, $width-1) . '-', substr($line, $width-1));
        }
        else
        {
            ($l, $line) = (substr($line, 0, $p), substr($line, $p+1));
        }
        $l =~ s/\s+$//;
        push (@split, $l);
        $line =~ s/^\s+//;
    }

    push (@split, $line) if (length($line));

    return (@split);
}


sub set
{
    my ($var, $value) = @_;

    no strict 'refs';

    if ($var && length($value))
    {
        if ($var eq 'cmdchar')
        {
            if (length($value) > 1)
            {
                do_beep();
                icb_print ($output_window, 'error', "[=Error=] cmdchar may be set to a single character ONLY.\n");
            }
            elsif ($value eq '\\' || $value eq '!' || $value eq '^')
            {
                do_beep();
                icb_print ($output_window, 'error', "[=Error=] cmdchar may not be set to the \\, !, or ^ characters.\n");
            }
            else
            {
                $cmdchar = $value;
            }
        }
        elsif ($value != int($value))
        {
            do_beep();
            icb_print ($output_window, 'error', "[=Error=] %s may only be set to an integer value\n", $var);
        }
        elsif (eval "defined \$$var")
        {
            my $setcmd = sprintf($value =~ /\s+/ ? '$%s = "%s";'
                                                 : '$%s = %s;',
                                 $var,
                                 $value);
            eval $setcmd;
            if ($var eq 'readhistsize')
            {
                shift (@logbuffer) while ((my $l = @logbuffer) > $readhistsize);
            }
        }
        else
        {
            icb_print ($output_window, 'warning',  "[=Set=] Variable '%s' undefined\n", $var);
        }
    }
    else
    {
        do_beep();
        icb_print ($output_window, 'warning', "[=Set=] Insufficient arguments to set()\n");
    }

    use strict;
}


sub addcmd
{
    my ($newcmd) = @_;

    $usercmds{lc($newcmd)} = $newcmd;
}


sub delcmd
{
    my ($cmd) = @_;
    $cmd = lc($cmd);
    if (defined ($usercmds{$cmd}))
    {
        delete($usercmds{$cmd});
        icb_print ($output_window, 'sbrkt',  "%s", "[=");
        icb_print ($output_window, 'status', "%s", "Client");
        icb_print ($output_window, 'sbrkt',  "%s", "=]");
        icb_print ($output_window, 'status', " Command %s deleted.\n", $cmd);
    }
    else
    {
        icb_print ($output_window, 'warning', "[=Delcmd=] Command %s unknown.\n", $cmd);
    }
}


sub addhook
{
    my ($hooktype, $hookfunc) = @_;

    if ($hooktype && $hookfunc)
    {
        $hooktype = lc($hooktype);
        if (grep(/^$hooktype$/, @hooktypes))
        {
            my $usercmd = sprintf("\$hooks_%s{%s} = '%s';",
                                  $hooktype,
                                  $hookfunc,
                                  $hookfunc);
            eval ($usercmd);
            icb_print ($output_window, 'status', "[=Addhook=] Hook %s:%s installed.\n", $hooktype, $hookfunc);
        }
        else
        {
            icb_print ($output_window, 'warning', "[=Addhook=] Hook type %s unknown.\n", $hooktype);
        }
    }
    else
    {
        icb_print ($output_window, 'error',  "[=Addhook=] Insufficient arguments to addhook() function\n");
    }
}


sub delhook
{
    my ($hooktype, $hookfunc) = @_;
    my $tid = (threads->self)->tid();
    my $usercmd;

    if ($tid == 2)
    {
        @delhook_cmd = ($hooktype, $hookfunc);
    }
    elsif ($tid == 3)
    {
        @delhook_cmd = ();
    }

    if ($hooktype && $hookfunc)
    {
        $hooktype = lc($hooktype);
        if (grep(/^$hooktype$/, @hooktypes))
        {
            if ($tid == 2)
            {
                @delhook_cmd = ($hooktype, $hookfunc);
            }
            elsif ($tid == 3)
            {
                $usercmd = sprintf ("defined \$hooks_%s{%s}", $hooktype, $hookfunc);
                if (eval $usercmd)
                {
                    $usercmd = sprintf("delete (\$hooks_%s{%s});",
                                       $hooktype,
                                       $hookfunc);
                    eval ($usercmd);
                    icb_print ($output_window, 'sbrkt',  "%s", "[=");
                    icb_print ($output_window, 'status', "%s", "Client");
                    icb_print ($output_window, 'sbrkt',  "%s", "=]");
                    icb_print ($output_window, 'status', " Hook %s:%s deleted.\n", $hooktype, $hookfunc);
                }
                else
                {
                    icb_print ($output_window, 'warning', "[=Delhook=] Hook %s:%s unknown.\n", $hooktype, $hookfunc);
                }
            }
        }
        else
        {
            icb_print ($output_window, 'warning', "[=Delhook=] Hook type %s unknown.\n", $hooktype);
        }
    }
    else
    {
        icb_print ($output_window, 'error',  "[=Delhook=] Insufficient arguments to delhook() function\n");
    }
}


sub do_hooks
{
    my ($hooktype, @packet) = @_;
    my $r = 0;
    my %hooklist;

    no strict 'refs';
    eval sprintf ('%%hooklist = %%hooks_%s', $hooktype);
    if (keys %hooklist)
    {
        foreach my $hook (sort keys %hooklist)
        {
            $packet[0] = '*'.$packet[0] if ($hooktype eq 'privmsg');
            for (my $i = 0; $i < scalar @packet; $i++)
            {
                $packet[$i] =~ s/'/\\'/g;
            }
            my $usercmd = sprintf('&%s(@packet);', $hook);
            $r = eval ($usercmd);
#            icb_print ($output_window, 'status', "[=Hook=] Hook %s (command %s type %s) returned %d\n", $hook, $usercmd, $hooktype, $r);
            last if ($r == -1);
        }
    }

    use strict;

    return ($r);
}


# These functions and the variables they set and read
# are used for implementation of the generic trigger
# mechanism.

sub set_trig_mask
{
    $trig{mask} = $_[0];
}

sub set_trig_action
{
    $trig{action} = $_[0];
}

sub set_trig_status
{
    $trig{status} = $_[0];
}

sub set_trig_result
{
    $trig{result} = $_[0];
}

sub get_trig_mask
{
    return ($trig{mask});
}

sub get_trig_action
{
    return ($trig{action});
}

sub get_trig_status
{
    return ($trig{status});
}

sub get_trig_result
{
    return ($trig{result});
}


sub out
{
    my ($str) = @_;

    icb_print ($output_window, 'output', "%s\n", $str);
}


sub docommand
{
    handle_input(join(' ', @_), $output_window, $input_window, $status_line, $connection);
}


sub do_beep
{
    beep() if ($beep_on_error);
}


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

    $cmd = lc($cmd);
    $alias =~ s/^\s+//;
    $alias =~ s/\s+$//;
    if (grep(/^$cmd$/, @builtins))
    {
        do_beep();
        icb_print ($output_window, 'error', "[=Error=] Builtin command '%s' cannot be used as an alias\n", $cmd);
    }
    elsif ($cmd eq $alias || $cmd eq substr($alias, 0, index($alias, ' ')-1))
    {
        do_beep();
        icb_print ($output_window, 'error', "[=Error=] Alias of '%s' to itself would cause recursion\n", $cmd);
    }
    elsif (length($alias) == 0 || length($cmd) == 0)
    {
        do_beep();
        icb_print ($output_window, 'error', "[=Error=] %s\n", "Empty alias");
    }
    else
    {
        $aliases{$cmd} = $alias;
    }
}


sub unalias
{
    my ($cmd) = @_;

    $cmd = lc($cmd);
    if (length($cmd) == 0)
    {
        do_beep();
        icb_print ($output_window, 'error', "[=Error=] %s\n", "Nothing to unalias!");
    }
    else
    {
        delete ($aliases{$cmd}) if defined ($aliases{$cmd});
    }
}


sub hilight
{
    foreach my $tok (@_)
    {
        push (@hilightnicks, lc($tok)) unless (grep(/^$tok$/i, @hilightnicks));
    }
}


sub unlight
{
    foreach my $tok (@_)
    {
        @hilightnicks = grep(!/^$tok$/i, @hilightnicks);
    }
}


sub get_group
{
    return ($cur_group);
}


sub get_nick
{
    return ($cur_nick);
}


sub get_mod
{
    return ($cur_mod);
}


sub readservers
{
    my $serverdb = find_serverdb();

    if ($serverdb)
    {
        open (DB, $serverdb);
        while (<DB>)
        {
            if (/^\s*(\w+)\s+(\S+)\s+(\d+)\s*$/)
            {
                $servers{$1}->{host} = $2;
                $servers{$1}->{port} = $3;
            }
        }
        close (DB);
    }
    else
    {
        print "\tNo icbserverdb file found!  Using built-in defaults ONLY.\n";
    }

    foreach my $def ('chime','default')
    {
        unless (defined $servers{$def})
        {
            $servers{$def}->{host} = sprintf ('%s.icb.net', $def);
            $servers{$def}->{port} = 7326;
        }
    }
}


sub find_serverdb
{
    my $file = sprintf ('%s/.icbserverdb', $ENV{HOME});
    my $serverdb;

    if (defined $options{icbserverdb} && -f $options{icbserverdb} && -r _ && -T _)
    {
        $serverdb = $options{icbserverdb};
    }
    elsif (-f $file && -r _ && -T _)
    {
        $serverdb = $file;
    }
    else
    {
        foreach my $path ($datadir,
                          '/usr/share/icb',
                          '/usr/local/share/icb',
                          '/usr/local/lib/icb',
                          '/usr/local/lib',
                          '/usr/lib/icb',
                          '/usr/lib',
                          './share/icb')
        {
            $file = sprintf ('%s/icbserverdb', $path);
            if (-f $file && -r _ && -T _)
            {
                $serverdb = $file;
                last;
            }
        }
    }

    return ($serverdb);
}


sub listservers
{
    print "\n\tICBM:  Known ICB Servers\n";
    printf("\t%-20s %-20s %s\n",
           'Server name',
           'Host',
           'Port');
    foreach my $server (sort keys %servers)
    {
        printf("\t%-20s %-20s %d\n",
               $server,
               $servers{$server}->{host},
               $servers{$server}->{port});
    }
    print "\n";
}


sub find_primes_file
{
    my ($primefile, $file);

    foreach my $path ($datadir,
                      '/usr/share/icb',
                      '/usr/local/share/icb',
                      '/usr/local/lib/icb',
                      '/usr/local/lib',
                      '/usr/lib/icb',
                      '/usr/lib',
                      './share/icb')
    {
        $file = sprintf ('%s/primes', $path);
        if (-f $file && -r _ && -T _)
        {
            $primefile = $file;
            last;
        }
    }

    return ($primefile);
}


sub calc_idletime
{
    my ($s) = @_;
    my $h = int($s/3600);
    $s -= $h*3600;
    my $m = int($s/60);
    $s -= $m*60;
    my $time = $h ? sprintf ("%2dh%02dm%02ds", $h, $m, $s)
                  : $m ? sprintf ("%2dm%02ds", $m, $s)
                       : $s ? sprintf ("%2ds", $s)
                            : '-';
    return ($time);
}


sub calc_logintime
{
    my ($t) = @_;
    my ($s,$m,$h,undef) = localtime($t);
    my $time = sprintf ("%02d:%02d:%02d", $h, $m, $s);
    return ($time);
}


sub printfcolor
{
    my ($buf, $color, $fmt);

    $color = shift(@_);
    $fmt = shift (@_);

    $buf = length($ansi{$color})
         ? colored (sprintf ($fmt, @_), $ansi{$color})
         : sprintf ($fmt, @_);

    print ($buf);
}


sub icb_print
{
    my ($window, $color, $fmt, @args) = @_;
    my $buf = sprintf ($fmt, @args);

    lock ($output_sem);

    if ($use_color)
    {
        attron ($window, COLOR_PAIR($colors{$color})) if ($colors{$color});
        attron ($window, A_BOLD) if ($attr{$color} & 1);
        attron ($window, A_REVERSE) if ($attr{$color} & 2);
    }

    addstr($window, $buf);

    if ($use_color)
    {
        attroff ($window, COLOR_PAIR($colors{$color})) if ($colors{$color});
        attroff ($window, A_BOLD) if ($attr{$color} & 1);
        attroff ($window, A_REVERSE) if ($attr{$color} & 2);
    }

    log_send($buf) if ($log_sem);

    if (chomp($buf))
    {
        if ($page[0])
        {
            $page[1]++ unless ($page[2]);		# count newlines
            if ($page[1] > $page[0])			# maintain paused flag
            {
                unless ($page[2])
                {
                    $page[2] = 1;

                    if ($use_color)
                    {
                        attron ($window, COLOR_PAIR($colors{more})) if ($colors{$color});
                        attron ($window, A_BOLD) if ($attr{more} & 1);
                        attron ($window, A_REVERSE) if ($attr{more} & 2);
                    }

                    addstr($window, " ---- PAUSED: Press Enter to continue ---- \n");

                    if ($use_color)
                    {
                        attroff ($window, COLOR_PAIR($colors{more})) if ($colors{$color});
                        attroff ($window, A_BOLD) if ($attr{more} & 1);
                        attroff ($window, A_REVERSE) if ($attr{more} & 2);
                    }
                }
            }
        }
    }

    refresh($window);
}


sub icb_debug
{
    my ($window, $flag, $fmt, @args) = @_;

    if ($options{debug} & $flag)
    {
        icb_print($window, 'debug', " <[DEBUG]>  $fmt", @args);
    }
}


sub unpause
{
    my $sep = chr(255);
    $page[1] = $page[2] = 0;
    while (scalar @pagebuffer && !($page[2]))
    {
        my ($time, $type, @packet) = split(/$sep/, shift(@pagebuffer));
        $replayctr[1]++ if ($replayctr[0]);
        parse_packet($output_window, $input_window, $status_line, $time, $type, @packet);
        select(undef, undef, undef, 0.01);
    }

    if ($replayctr[0] && ($replayctr[0] == $replayctr[1]))
    {
        icb_print ($output_window, 'sbrkt', "%s", '[=');
        icb_print ($output_window, 'status', "%s", 'Replay');
        icb_print ($output_window, 'sbrkt', "%s", '=]');
        icb_print ($output_window, 'status', " End of replay.\n");
        @replayctr = (0,0);
    }
}


sub icb_perror
{
    my ($str, $window) = @_;

#    if ($window)
#    {
#        icb_print($window, 'error', "[=Setcolor=] %s\n", $str);
#    }
#    else
    {
        printf STDERR ("ERROR: %s\n", $str);
    }
#    return (0);
}


sub set_colors
{
    my (@colors, $line, $canon);

    open (COLORFILE, $colorfile);
    @colors = <COLORFILE>;
    close (COLORFILE);

    foreach my $line (@colors)
    {
        next if ($line =~ /^#/);
        next unless ($line =~ /\w+/);
        chomp($line);
        $line =~ s/\s+/ /g;
        $line =~ s/^ //;
        $line =~ s/ $//;
        my $canon = $line = lc($line);
        $canon =~ tr/A-Za-z //cs;
        if ($canon eq $line)
        {
            my @settings = split(/ /, $canon);
            if ($colors{$settings[0]})
            {
                my $buf = sprintf ("Color %s previously set!", $settings[0]);
                icb_perror($buf, $output_window || 0);
            }
            else
            {
                set_color(@settings);
            }
        }
        else
        {
            icb_perror ("Unable to parse line '$canon' in $colorfile\n", $output_window);
        }
    }
}


sub set_default_colors
{
    set_color('normal',      'green',  'black');
    set_color('output',      'white',  'black');
    set_color('ownmsg',      'cyan',   'black');
    set_color('personal',    'cyan',   'black');
    set_color('persfrom',    'blue',   'yellow',  'bold');
    set_color('hilight',     'green',  'black',   'bold');
    set_color('hilightfrom', 'green',  'blue',    'bold');
    set_color('who_hdr',     'white',  'black',   'bold');
    set_color('nickname',    'yellow', 'black',   'bold');
    set_color('hostmask',    'green',  'black',   'bold');
    set_color('modstar',     'red',    'black',   'bold');
    set_color('userflags',   'red',    'black',   'bold');
    set_color('idletime',    'cyan',   'black',   'bold');
    set_color('logintime',   'cyan',   'black',   'bold');
    set_color('beep',        'green',  'magenta', 'bold');
    set_color('beepfrom',    'yellow', 'magenta', 'bold');
    set_color('status',      'cyan',   'black',   'bold');
    set_color('alert',       'cyan',   'black',   'bold');
    set_color('warning',     'yellow', 'black',   'bold', 'reverse');
    set_color('error',       'red',    'black',   'bold', 'reverse');
    set_color('more',        'yellow', 'black',   'bold');
    set_color('sbrkt',       'green',  'black',   'bold');
    set_color('abrkt',       'green',  'black',   'bold');
    set_color('pbrkt',       'red',    'yellow',  'bold');
    set_color('statline',    'green',  'black',   'bold', 'reverse');
    set_color('encrypted',   'blue',   'white',   'bold', 'reverse');
    set_color('debug',       'blue',   'white',);
}

    
sub set_color
{
    my $args = @_;
    my $ret = 1;
    my ($arg, $fg, $bg);

    if ($args < 2)
    {
        $ret = icb_perror ("Not enough arguments to set_color", $output_window || 0);
    }
    elsif ($args > 5)
    {
        $ret = icb_perror ("Too many arguments to set_color", $output_window || 0);
    }
    else
    {
        my $name = shift(@_);
        $args--;
        if (grep /^$name$/, @color_set)
        {
            delete($attr{$name});
        }
        else
        {
            $ret = icb_perror ("Color setting name $name is unknown", $output_window || 0);
        }

        if ($ret)
        {
            $arg = shift (@_);
            $args--;

            if (grep (/^$arg$/, keys %color_names))
            {
                $ansi{$name} = $arg;
                $fg = $arg;
            }
            else
            {
                $ret = icb_perror ("Color attribute $arg is unknown", $output_window || 0);
            }
        }

        if ($ret)
        {
            $arg = shift (@_) || 'black';
            $args--;

            if (grep (/^$arg$/, keys %color_names))
            {
                $ansi{$name} .= sprintf (" on_%s", $arg);
                $bg = $arg;
            }
            else
            {
                $ret = icb_perror ("Color attribute $arg is unknown", $output_window || 0);
            }
        }

        if ($ret && $args > 0)
        {
            $arg = shift (@_);
            $args--;

            if ($arg eq 'bold')
            {
                $ansi{$name} = $arg . ' ' . $ansi{$name};
                $attr{$name} |= 1;
            }
            elsif ($arg eq 'reverse')
            {
                $ansi{$name} = $arg . ' ' . $ansi{$name};
                $attr{$name} |= 2;
            }
            else
            {
                $ret = icb_perror ("Unexpected argument $arg to set_color", $output_window || 0);
            }
        }

        if ($ret && $args > 0)
        {
            $arg = shift (@_);
            $args--;

            if ($arg eq 'bold')
            {
                $ansi{$name} = $arg . ' ' . $ansi{$name};
                $attr{$name} |= 1;
            }
            elsif ($arg eq 'reverse')
            {
                $ansi{$name} = $arg . ' ' . $ansi{$name};
                $attr{$name} |= 2;
            }
            else
            {
                $ret = icb_perror ("Unexpected argument $arg to set_color", $output_window || 0);
            }
        }
        
        if ($ret)
        {
            my $pair = $colors{$name} || ++$last_color_set;
            init_pair($pair, $color_names{$fg}, $color_names{$bg});
            $colors{$name} = $pair;
        }
    }

    return ($ret);
}


__END__

=head1 NAME

B<ICBM> - An extensible threaded ICB client in Perl

=head1 VERSION

Version 1.6.0 ("Time after time")

=head1 SYNOPSIS

icbm [options]

  Options:
    -server <name>		(default: chime)
    -group <name>		(default: 1)
    -nick <name>		(default: username)
    -host <hostname>		(default: chime.icb.net)
    -port <port>		(default: 7326)
    -icbserverdb, db <path>
    -color, -turner
    -list
    -who
    -alacritty, -a
    -help, -usage, -?
    -man
    -version

=head1 OPTIONS

=over 4

=item B<-server <name>>

Specify the server to use, by name (defaults to chime)

=item B<-group <name>>

Specify the initial group to join (defaults to group 1)

=item B<-nick <name>>

Specify the nickname to use in ICB (defaults to your username)

B<-nick> can be specified more than once.

=item B<-host <hostname>>

Specify the server to use, by hostname (defaults to chime.icb.net)

=item B<-port <number>>

Connect to the specified port (defaults to port 7236)

=item B<-icbserverdb, -db <path>>

Specify the fully qualified pathname of the icbserverdb file to use

=item B<-color, -turner>

Colorize messages in ICB

=item B<-list>

List the known ICB servers, do not connect

=item B<-who>

List users on the server, do not join server

=item B<-alacritty, -a>

Activate features for Alacritty compatibility to enable mousewheel scrolling
in alacritty terminals.  B<THIS DOES NOT WORK INSIDE SCREEN OR TMUX.>

=item B<-help, -usage, -?>

List command-line options and usage, then exit

=item B<-man>

Display full documentation and exit

=item B<-version>

Display version string and exit

=back

=head1 DESCRIPTION

B<ICBM> is a threaded B<ICB> client which supports a superset of the feature set
of B<CICB>, but scripted in Perl instead of TCL.  The primary goal of B<ICBM>
is to be a lightweight ICB client that is not dependent upon TCL.  TCL may
well be traditional for ICB/ForumNet clients to embed, but come on, people, it's
a ghastly language for most purposes.

B<ICBM>'s initial behavior is controlled by the command-line options listed above.
Most options can be abbreviated to their first letter.  All command-line options
may be shortened to any unique form -- for instance, -n for -nick and -s for server.
The presence of -turner as a synonym for -color is for historical reasons from the
development of B<CICB>'s color code.

=head1 INITIALIZATION

On startup, B<ICBM> will attempt to read the list of known ICB servers from the file
icbserverdb, if it exists.  B<ICBM> will look for this file by default in the
following paths:

    $HOME/.icbserverdb
    <data directory>/icbserverdb (see below)
    /usr/share/icb/icbserverdb
    /usr/local/share/icb/icbserverdb
    /usr/local/lib/icb/icbserverdb
    /usr/local/lib/icbserverdb
    /usr/lib/icb/icbserverdb
    /usr/lib/icbserverdb
    ./share/icb/icbserverdb

B<ICBM> can also be instructed to look for the file in a specified location, either
by including an B<icbserverdb> directive in the defaults file, or by using the B<-db>
option.  If a location is specified by either means, and an icbserverdb file is found
there, the default list of locations will not be searched; if the specified location
is not found, though, the search will continue.

This file is intentionally the same server list file used by B<CICB>.  B<ICBM> will
use the first of the listed files that it finds which is readable by the user and
appears to be a text file.

The format of the icbserverdb file is as follows:

    server1	hostname	port
    server2	hostname	port
    server3	hostname	port
    ...

B<ICBM> will use the contents of this file, if found, to resolve ICB server names
to hostnames.  If the icbserverdb file is not found, B<ICBM> knows only about the
current (as of the time of writing) default server, chime.icb.net, aka default.icb.net.

B<ICBM> will next look for its startup files.  By default, these are located in your $HOME/.icbm
directory.  This location can be overridden by setting the environment variable ICBM_DATA.

If the data directory does not exist the first time you run B<ICBM>, B<ICBM> will attempt to
create it.  B<ICBM> will die with an error message if it cannot create the $HOME/.icbm directory
(or $ICBM_DATA if ICBM_DATA is set), or if $HOME/.icbm (or $ICBM_DATA) already exists and is not
a directory.  (From here on, this directory will be referred to as $DATADIR.)

The very first data file B<ICBM> will try to read is $DATADIR/defaults.  This file should contain
ONLY variable settings to be changed before ICBM sets up its windows, plus an optional list
of default nicks.  At present, this file supports setting the following defaults:

=over 8

=item writesize <number of lines>

=item logbuffer <number of lines>

=item defserver <default server name>

=item defhost <default server hostname>

=item defport <default server port>

=item defgroup <default initial group>

=item encryption <0|1>

=item nick <nick> (may appear multiple times)

=item icbserverdb <path to icbserverdb>

=item debugmask <hexadecimal mask>

=item sleeptime <seconds>

=item timestamps <on|off>

=item timestampformat <format>

=back

The writesize is the number of lines B<ICBM> sets aside for the writing pane in the terminal
window.  Its value defaults to 5, and may be set anywhere from 1 to 10.  If a nick is specified
in the defaults file, it will be used to seed the first value in the list of nicks (see below).
If no server or host and port are specified on the command line, and no defaults are specified
in the defaults file, then B<ICBM> will fall back to the defaults 'chime', chime.icb.net, and
port 7326.  Likewise, if no group is specified in either place, the fallback group is group 1.
The logbuffer default sets the number of incoming messages B<ICBM> will cache in its FIFO buffer
for replays; if not specified, it defaults to 500, and may be set to any integer value greater
than 100.

Sleep time is the time ICBM will pause after initialization if debugging is enabled, or if
warnings occur while initializing encryption modules.  It defaults to 10 seconds.

The timestamp format can be any valid strftime format string.  It is checked for general format
validity, but not for sanity.  It defaults to B<%H:%M:%S>.

If you started B<ICBM> with the B<-color> option, it will next look for $DATADIR/colors
to find color definitions.  If B<-color> was used and no $DATADIR/colors file exists,
B<ICBM> will use its default colors.  Be aware that these default colors are tuned for a
terminal window or Xterm with a black background.  (See B<COLORS>, below.)

=head1 STARTUP MODES

What happens next depends on whether you started B<ICBM> in non-interactive mode (icbm -who) or
in interactive mode.

In a non-interactive (query-only) startup, B<ICBM> will at this point simply attempt to connect
to the ICB server, obtain a list of users on the server in visible groups, display the list on
your screen, and exit.

In an interactive (log-in) startup, after doing initial color setup, B<ICBM> divides your
terminal window into three regions.  The bottom five lines of the terminal are used for an input
typing area (which scrolls as needed).  The next line up is B<ICBM>'s status line, which displays
various information about your connection (see B<THE STATUS LINE>).  The remainder of the terminal
above the status line is used to display output from the server, which also scrolls as needed.  The
input window supports a readline-like history buffer with command-line editing (see B<EDITING>), as
well as a tab-history buffer of people whom you have messaged or who have messaged you (see B<TAB
HISTORY>).

After creating the terminal windows, B<ICBM> will attempt to connect to the ICB server.  If
connection succeeds, B<ICBM> will automatically load the default script file, $DATADIR/commands
(assuming it exists).  No other files will be autoloaded, but you can make ICBM load additional
script files from your commands file if you wish (see B<SCRIPTING>) or load them manually yourself
as needed using the B</load> and B</loadhook> commands (see B<COMMANDS>).

=over 4

B<IMPORTANT NOTE:> Hook functions can be contained in the same file as regular scripting functions.
However, at this time, it is necessary to use the B<loadhook> command/function to load hook
functions, as they must be loaded in the context of the loader thread, while regular scripting
functions must be loaded using B<load>.  Therefore, if you mix hooks and regular functions in the
same file, you must load that file twice using both B<load> and B<loadhook>.  It is recommended
that you keep your hooks and all other scripting functions in separate files.

This limitation may be removed in a future release.

You should also be aware that there is at this time no way for hooks and regular scripting
functions to share any data not contained in existing B<ICBM> shared variables, because they are
executed in different threads.

=back

After loading your command file(s), ICB will log you in to the server, and you're up and running,
unless the nick you chose is already is use by another user on the server.  In this case, the
server will rather unceremoniously drop you, and you'll have to try again using a different nick.


B<ICBM> allows you to specify more than one nick on the command line, as in the example below:

    icbm -n Larry -n Moe -n Curly

In this case, B<ICBM> will first try to connect you to the server as Larry.  If someone else
is already logged on as Larry, B<ICBM> will automatically try the connection again as Moe.
If all supplied nicks fail, B<ICBM> will make one more last-ditch attempt using what it has
determined to be your login username, before giving up.

By default, you will join the server in group 1.  Message traffic from the server appears in the
top window (the read window), while you type your commands and messages in the bottom window.
By default, your own outgoing messages are echoed to the read window, so that you can see what
you said and when in the conversation you said it.  (This feature can be disabled by setting
the B<echo_outgoing> variable to 0, as explained below in B<ICBM VARIABLES>.)

B<ICBM> maintains a log of up to the last 500 (by default) message packets received from the
server, which can be "replayed" in the read window at any time using the B</replay> command
(see B<ICBM COMMANDS>).  This does not include automatically-echoed outgoing messages.

B<ICBM> allows you to edit the commands and messages you type in a similar manner to readline
or emacs.  It also has a capability for automatic correction of your own common typos using
your own personal list of corrections (see the B</correct> command).  This automatic
correction can be disabled on a line-by-line basis.

=head1 THE STATUS LINE

The status line on your ICBM display contains several pieces of information.  On the left end
is an ICBM bannerlet or "text icon".  To the right of this are located the following:

=over 4

=item Your current nickname;

=item A MOD indicator, if you are moderator of the group you're talking in;

=item The group you're currently in;

=item The name of the server you're connected to;

=item A query-mode indicator, if you're currently in query mode (see B</query>) and space permits;

=item A count of unread 'write' messages for you on the server, if any and if space permits;

=item A 24-hour digital clock displaying local time.

=back

If in query mode, the contents of the query indicator will vary depending on the available
space on your status line.  If there is sufficient space, it will display as B<[Q: nickname]>,
showing who you are currently in query mode with.  If there is not room for the nickname, it
will display just as B<[QUERY]>, and if there is still insufficient space for that, only B<[Q]>
will be displayed.  In the unlikely event that there is no room even for that, the query mode
indicator will be omitted altogether.  If you have unread messages on the server, the number
of messages will be displayed in a counter


=head1 EDITING

The following editing keys can be used in the input window:

=over 4

=item B<Up arrow>

Recall previous line from history buffer

=item B<Down arrow>

Recall next line from history buffer

=item B<Left arrow>

Move cursor one character left

=item B<Right arrow>

Move cursor one character right

=item B<Home / Ctrl-A>

Move cursor to beginning of line

=item B<End / Ctrl-E>

Move cursor to end of line

=item B<Backspace>

Delete previous character

=item B<Del key>

Delete next character

=item B<Ctrl-B>

Move cursor back one word

=item B<Ctrl-F>

Move cursor forward one word

=item B<Ctrl-W>

Delete previous word

=item B<Ctrl-U>

Delete from cursor position to beginning of line

=item B<Ctrl-K>

Delete from cursor position to end of line

=item B<Tab key>

Recall previous entry from /msg history buffer

=back

B<ICBM> also has several ways of recalling previously-typed messages or commands.  If you
type '!foo' and hit enter, the last line that you typed which began with 'foo' (if there
is one) will be recalled from your history buffer and placed in the write buffer for you
to resend as-is or edit further.  If there is no prior matching line, nothing will happen.

If you type '^foo^bar', the last line you typed (if there is one) will be recalled to the
write buffer with all instances of the first term ('foo' in this example) replaced with the
second term ('bar').  If the first term is not found in the last line typed or the history
buffer is empty, nothing will happen.

Both of these operators are case-sensitive.

It is possible to combine both operators in a single command with the syntax !foo^bar^baz.
In this form, first the last line beginning with foo is retrieved from history, then
substitution is performed on the retrieved line.  If any step fails, the entire command does
nothing.

For example, if a few minutes ago you had previously typed:

    The dog is stalking the cat

And then you realized that it was actually the tiger that the dog was stalking, you could type:

    !The dog^cat^tiger

After you hit enter, your input buffer would now contain:

    The dog is stalking the tiger

(The tiger would probably very shortly contain the dog, but that problem is outside the scope
of this document.)

=head1 TAB HISTORY

The tab-history buffer keeps a list of the ICB users you have sent personal messages to
or received personal messages from, maintained in order of most recently used.  Nicks which
generate a "<nick> is not signed on" error are automatically deleted from the tab history
buffer.  (This can be overridden by setting the variable tab_del_on_error to 0; see
B<client variables>.)  Messages received while you are already in tab history composing a
message will not affect the order of nicks as used by tab history.

If the tab history buffer is empty and you hit TAB on an empty input buffer, "/m " will
be preloaded into your input buffer.  If there are nicks saved in the tab history buffer,
"/m <nick> " will be loaded into your input buffer, where <nick> is the most recently
updated nick in the tab history buffer.  Continuing to hit TAB will step through the tab
history buffer in order of increasingly older nicks.

If there is a message in your input buffer when you hit tab, and there are nicks saved in
your tab history buffer, the message content will be preserved, while nick insertion or
substitution is performed as above.  If the tab history buffer is empty and you hit TAB
on an existing message, nothing will happen.

The tab history also supports nick autocompletion for nicks in the tab history buffer.
If you type "/m al" and hit TAB, the nick will be completed with the most recently
accessed nick beginning with 'al' in the tab history buffer, if present.  If no matching
nick is found, no replacement will be performed.

=head1 ICB COMMANDS

All ICB commands are prefixed by a / character.  (This can be changed by setting a new
value for the B<cmdchar> variable (see B<ICBM VARIABLES>)).  Anything you type that is not
prefixed by the current command prefix character will be sent directly to whatever group
you happen to be in at the time, and will be visible to everyone in the group.  If there
is no-one but yourself in the group, the server will send you a warning to that effect.

Certain other characters are handled specially by ICBM.  The ! and ^ characters are used
for input history recall and editing.  If you wish to send an open message beginning with
the ! or ^ characters, you must actually begin your line with \ to escape that character.
For example, if you wish to send the message:

!hola!

you must type:

\!hola!

These three reserved characters (!, ^, \) may not be used as values for cmdchar.

The following is a brief summary of a few of the most frequently used ICB commands:

=over 4

=item B</group> newgroup

Leave the group you are in and join "newgroup".  If it does not exist, it will be created
and you will become its moderator.  You can abbrevtate group as B</g>.

=item B</nick> newname

Change your nickname to "newname".  'name' is a synonym for 'nick'.  The command can be
abbreviated to B</n>.

=item B</msg> person message......

Sends a private message to "person".  The message cannot be read by any other ICB user.
This command can be abbreviated to B</m>.  Note that it is normally considered bad ICB etiquette
to paste private messages to an open group, although there are exceptions (for instance,
if you are being sent offensive private messages by a user in that group).

B<ICBM> extends the B</msg> command to allow you to send the same private message to multiple
users simultaneously by separating their nicks by commas:

B</msg> person1,person2,person3 message .....

=item B</who> group

Lists the users currently in "group".  If the group is omitted, lists all users in visible
groups on the current ICB server.  Use B</who .> to see who is in the current group.  Can be
abbreviated to B</w>.

=item B</echoback> off | on | verbose

This command (which defaults to off) tells the ICB server whether to send you back copies
of your own public and private messages.  It is similar to B<ICBM>'s echo_outgoing feature
(see B<ICBM VARIABLES>), except that echo_outgoing displays what you actually typed, while
echoback displays your message as the server formatted it (and hence as the recipients will
see it).  You will probably not want to use both this and echo_outgoing at the same time.

The settings for this command are as follows:

  off      Server sends none of your messages back to you
  on       Server sends you your own public messages
  verbose  Server sends you your public and private messages

Note that if you use echoback, your own messages echoed back to you from the server will go
into the incoming message log, and will be redisplayed when you use the B</replay> command.
Outgoing messages displayed by B<ICBM>'s echo_outgoing feature are not saved in the message
log, and will not be replayed.

=item B</quit>

Disconnects from the server and quits your ICB session.  Unlike CICB, B<ICBM> does not allow
B</quit> to be abbreviated to B</q>, because it's too easy to accidentally hit B</q> when you
meant to hit B</w>.  If you feel confident, feel free to B<alias> it (see B</alias>, below).

If you attempt to quit while still mod of the group you are in, ICBM will issue a one-time
warning.  This warning can be disabled by setting the B<modwarn> variable to 0.

=back

There are many more ICB commands.  The exact list of commands supported by a given server
varies from one server to the next.  To get a full list of commands supported by a given
server, as well as their possible options (many commands have numerous possible options),
try one of the following commands:

=over 4

=item B</help>

Gets a brief summary of the major commands supported by the ICB server

=item B</s_help>

Gets a detailed listing of ICB server commands and options

=back

=head1 ICBM COMMANDS

B<ICBM> supports the following builtin commands in addition to the regular ICB command set:

=over 4

=item B</query> person

This is similar to cicb's '/oset personalto person' function, but simpler and more flexible.  It
causes all text you paste or type, unless explicitly messaged to someone else with B</msg>, to
be sent as a private message to the person specified in your most recent B</query> command.

As with the B</msg> command, B<ICBM> allows you to specify a comma-separated list of nicks in a
B</query> command:

B</query> person1,person2,person3

To unset query mode, just type B</query> with no arguments.  (Most IRC users should be immediately
familiar with this command's operation.)

=item B</alias> command othercommands

Registers 'command' as an alias that will be expanded to 'othercommands' when used.  For example:

B</alias> pw msg server mypassword

The B</alias> command will not allow you to use key builtin commands (for instance, B<alias>, B<delcmd>,
B<set> and B<exit>, among others) as aliases.  It will also not allow you to create an alias which
begins with itself, such as:

B</alias> bark bark Ploogie!

as this would lead to infinite recursion during alias expansion.  It is, of course, possible
to set up alias loops which would lead to infinite recursion, as in the following example:

B</alias> bark howl

B</alias> howl bark

However, you're pretty unlikely to do this by accident, and if you intentionally B<choose> to go
shoot yourself in the foot, well ....  be my guest, it's your privilege.

=item B</unalias> alias

Unregisters an alias created with B</alias>.

=item B</correct> 'pattern' 'replacement'

=item B</correct> /pattern/replacement/

Registers B<replacement> as a send-time typing correction for B<pattern> in open messages, private
messages and server /writes.  This is used to automatically correct your own most common typos.
Both the pattern and the replacement string may contain whitespace.  The pattern and replacement
string must either both be delimited by single quotes, or by slashes as shown above.  All instances
of the pattern in the input will be replaced.

B</correct> ' teh ' ' the '

B</correct> / teh / the /

The B</correct> command will not allow you to correct a pattern to something which contains the
original pattern, such as:

B</correct> 'bark' 'we bark'

as this could lead to infinite recursion during correction.  However, it is not possible for
B<ICBM> to predict all possible corrections which could cause recursion.

Both the pattern and the replacement may also contain perl regular expression terms, as in the
following examples:

To correct 'teh' to 'the', only as a complete word:

B</correct> '\bteh\b' 'the'

or

B</correct> /\bteh\b/the/

To correct 'teh' to 'the' and 'Teh' to 'The', as a whole word only:

B</correct> '\b([Tt])eh\b' '$1he'

or

B</correct> /\b([Tt])eh\b/$1he/

You can use any Perl regular expression construct in your corrections.  If you aren't familiar
with Perl regular expressions and you're on a Unix or Unix-like system, try reading the following
man pages:

=over 4

=item B<man perlrequick>

Perl regular expressions quick start

=item B<man perlretut>

Perl regular expressions tutorial

=item B<man perlre>

Perl regular expressions, "the rest of the story"

=back

Be careful when adding send-time typo corrections.  B<ICBM> will not prevent you from shooting
yourself in the foot with this feature.

Autocorrection can be disabled on a line-by-line basis by beginning the line with the characters
&&.  These characters, and any following whitespace, will be stripped from that is actually sent.

B</m alaric teh foo> sends the corrected "the foo", given the corrections above.
B</m alaric && teh foo> actually sends "teh foo" exactly as typed.

If you actually WANT to send text beginning with &&, use a \ to escape the leading &:

B</m alaric \&& teh foo> sends "&& the foo"

=item B</uncorrect> 'pattern'

=item B</uncorrect> /pattern/

Unregisters a correction defined with B</correct>.

=item B</delcmd> command

Deletes a user-defined command from B<ICBM>'s command space.  This is useful if you've
loaded a script that turns out to contain a broken command, or if you unintentionally
defined a scripted command that replaces a standard ICB command that you just realized
you need.

=item B</delhook> type hook

Deletes a user-defined hook from B<ICBM> (see B<HOOKS>).  Useful to disable a hook that's not
working as anticipated.

=item B</deltilde> on|off

Enables or disables interpret_tilde_as_delete mode.  If this mode is enabled, the character
126 (0xFE) will be interpreted as a delete rather than as a tilde, for keyboards which send a
tilde as the DEL character.

=item B</encryption> on|off

The B<encryption> command interactively enables or disables encryption (if encryption is available).

See B<ENCRYPTION> for further details.

=item B</exit>

B<ICBM> provides B</exit> (which can be abbreviated to B</x>) as a synonym for B</quit>.
B</exit> can be abbreviated to B</x>.

=item B</grep> /pattern/

Replays messages in the history buffer which match the regular expression B</pattern/>.  Matching
is case sensitive by default; use B</pattern/i> to obtain a case-insensitive match.  While replaying
messages, the global variable B<$replaying> is set to 1; this enables hook functions to detect when
replay is active and possibly choose not to trigger.

=item B</hilight> nick [nick ...]

Adds nicks to the list of nicks from which B<ICBM> will highlight personal messages.  Use
this to emphasize personal messages from people whose messages you particularly don't want
to miss.

=item B</unlight> nick [nick ...]

Removes nicks from the list of nicks to be highlighted.

=item B</load> scriptfile

Instructs B<ICBM> to load a file containing user-defined scripting functions and data in Perl
(see B<SCRIPTING>).  Relative pathnames passed to B</load> (pathnames that do not begin with /)
are assumed to be relative to $DATADIR unless present in the current directory.

=item B</loadhook> hookfile

Instructs B<ICBM> to load a file containing user-defined hook functions in Perl (see B<SCRIPTING>).
Relative pathnames passed to B</loadhook> (pathnames that do not begin with /) are assumed to be
relative to $DATADIR unless present in the current directory.

=item B</out> message

Prints the specified message to the output window.  This command is mainly useful in
scripts.

=item B</replay> [[count [start]] [nick1 [nick2 nick3...]]
=item B</replay> [[starttime [endtime]] [nick1 [nick2 nick3...]]

Replay the last <count> incoming messages.  If <count> is not specified or exceeds the
number of lines in the history buffer, the entire buffer will be replayed.  If both <count>
and <start> are specified, then a block of <count> messages starting <start> messages ago will
be replayed.  If <count> is greater than <start>, <start> will be ignored.  For example:

/replay 50 100

would replay 50 messages starting 100 messages ago.

In the second form, instead of message counts and offsets (which can be confusing), B<icbm>
replays messages received between a specified start and end time (hour:minute) within the
preceding 24 hours.  For example:

/replay 13:00 14:00

would replay messages received between 1PM and 2PM local time.  B<endtime> is allowed to be
in the future; B<starttime> must be in the past.  If endtime is greater than or equal to
either starttime or "now", it is presumed to refer to the previous day.  Thus:

/replay 18:00 18:00

would replay messages from a 24-hour period.

NOTE:  B<ICB> allows user nicks to be composed entirely of digits.  If you attempt to replay
messages from a user whose nick is numeric, B<ICBM> would naturally be unable to tell the nick
from a message count.  Since this is likely to be a rather rare case, this problem is solved
by escaping numeric nicks.  Thus:

/replay 50 \666

would replay messages from user 666 in the last 50 messages.

The B</replay> command allows extensive filtering of which messages are replayed.  There are
three filtering methods, any or all of which may be combined in a single command.

=over 4

=item B<Explicit include lists>

If one or more nicks are listed following the B</replay> command, only messages from those
nicks will be included in the replay output.

Example: /replay tom dick harry

=item B<Exclude lists>

B<ICBM> can be told not to display messages from certain nicks by preceding those nicks with
a minus character.

Example: /replay -dick -jane

=item B<Message class filtering>

B<ICBM> can filter replayed messages by packet class.  There are two possible syntactical ways
to specify message class filtering.  The first is to simply list the desired message classes,
preceded by a + symbol:

/replay +open +msg

The second form combined desired classes into a comma-separated class list:

/replay +class=msg,open,status

Supported message classes for class filtering are:  open, msg, status, alert, error, output.

=back

For compatibility with older ICB clients, this command may also be invoked as B</display>.

=item B</set> variable value

Use B<set> to change the value of client variables such as cmdchar (see below).

=item B</setcolor> name fg [bg [bold] [reverse]]

Use B<setcolor> to dynamically change color settings.  This is useful when you're trying to
fine-tune your color settings.  Color setting name (from the list above) and foreground color
are optional; you may also specify background color, and bold and/or reverse.  You must
specify a background color if you wish to specify bold and/or reverse.  Color changes made
with B<setcolor> take effect immediately; all affected text onscreen will immediately update
to the new setting, although if you change bold or reverse attributes assocated with a color
setting, the attributes of accected onscreen text will not change dynamically along with the
color.  This can result in an unexpected text appearance temporarily.

All parameters are case-insensitive.

=item B</setup> nick

The B<setup> command initiates a handshake with B<nick>'s client to establish a shared session
key for client-to-client encryption of personal messages.  In order to use this command you must
have B<encryption> set to 1 in your defaults file, you must have all the required Perl modules
installed, and the other user must be using an ICB client that supports B<ICBM>-style
client-to-client encryption.

=item B</revoke> nick

The B<revoke> command is the reverse of B<setup>.  It revokes your session key for B<nick> and
sends B<nick>'s client a message to revoke his session key for you.

See B<ENCRYPTION> for further details.

=item B</show> aliases|corrections|hilights

This command allows you to view the current contents of client data lists in B<ICBM>.  You
can currently use it to list defined aliases (see B</alias>, defined corrections (see B</correct>),
and the list of nicks to be hilighted (see B</hilight>).

B</show aliases> replaces the former B</aliases> command.

=item B</timestamps> on|off

This command turns timestamping on or off.  When timestamping is on, incoming message packets
will be displayed prefixed with the time the packet was received, in 24-hour HH:MM:SS format.
(This format may be changed if desired by changing the B<timestampformat> variable.)  Output of
/who commands and client-side command output is not timestamped.  Replayed packets are timestamped
correctly with the time they were originally received, even if timestamping was not active when
the packet was received.

=item B</urls> list|drop|purge

This command manipulates the URL list maintained by B<ICBM>'s internal URL grabber.  (This grabber
used to be an external sample script that illustrated the use of hook_openmsg and hook_privmsg to
implement a grabber, but was moved into the core due to problems with cross-thread variable scoping
encountered with the external implementation.)

Used without arguments, B</urls> simply lists the current contents of the list.  B</urls list> with
no other arguments also has this effect.  B</urls list foo bar baz> lists all URLS containing one or
more of the strings foo, bar, baz.  B</urls drop foo bar baz> deletes all URLs containing one or more
of the strings foo, bar, baz from the list.  B</urls purge> completely clears the list.  B<clear> is
a synonym for B<purge>.

You can add any number of extension commands to these via scripting (below).  You can also use
scripting to redefine the behavior of any existing command, with the exception of defined B<ICBM>
builtins.  Replacing builtins could potentially leave you with an unusable client.

=back

=head1 LOGGING

B<ICBM> has the capability to log your sessions.  This is accomplished using the B</log> command.
By default, B<ICBM> will log (when logging is active) to $HOME/ICBM.log, but any alternate logfile
can be specified, and the default location can be changed by reassigning the B<$deflogfile> variable.

The B</log> command has three forms:

=over 4

=item B</log start>

Starts logging to the default logfile specified by B<$deflogfile>.

=item B</log start logfile>

Starts logging to the specified by log file.

=item B</log stop>

Closes the log file and stops logging.

=back

If you issue a B</log start> while logging is already active, the currently-open log file will
be closed and the new log file (which may be the same file) will be opened.

Any file in any directory may be used for logging, provided the directory is writeable by your
real or effective UID, and the file either does not exist or is a normal file writeable by your
real or effective UID.  If the file does not exist, it will be created.  If no path for the
logfile is specified, B<ICBM> will assume the file is to be opened (or created) in your home
directory.  All log files are opened in append mode.

B<ICBM makes no attempt to verify that the specified file is a plain ASCII text file; be careful.>

In B<ICBM> version 1.4.5 and later, the log_start() function can be invoked in the B<ICBM> startup
commands file, and B<ICBM> will wait until logging is actually running to open the log.

=head1 REMOTE CONTROL

B<ICBM> has a capability to accept input via a named pipe, or FIFO.  All text sent to this FIFO
will be handled by B<ICBM> exactly as though it had been typed in the input window, with the
exception of the B</exit> command (or the synonyms B</quit> or B</x>), which will be ignored if
sent to the FIFO.  You can doubtless easily find a way to circumvent this simple filter, but
doing so may cause your ICBM session to hang or otherwise behave in an unstable fashion.  No
bragging points are awarded for shooting yourself in the foot, so let's just not do it, OK?

The default location of the FIFO is B<$datadir/ICBM.nnn>, where B<nnn> is the PID of the B<ICBM>
process.

B<WARNING!!!>  Use this feature with caution.  Abuse of this feature to dump vast quantities of
program output into an ICBM session is likely to get you at the very least booted from the group
you're in, and may possibly get you -- B<or your entire site> -- banned from that ICB server.

=head1 PAGING

B<ICBM> can either display output continuously, or keep a count of lines and pause after each
'page' of a specified size.  When the set pagesize is reached, output will stop, incoming packets
will be queued, and the user will be prompted to press Enter to continue.  Normal commands may
still be entered at this prompt, and local command output will be displayed, though any remote
responses will be queued in the buffer along with all other incoming traffic.  The pagesize may
be changed, or paging disabled, while paused.  Timestamps of queued packets are preserved.

Paging is managed using the B/pagesize> command.  Setting pagesize to 0 (the default) disables
paging.  Pagesize may be set to any non-negative integer value.  (Reals will be rejected as
non-numeric.)  The following examples are valid:

=over 4

=item B</pagesize 50>

Sets page size to 50 lines.

=item B</pagesize 0>

Turns off paging.

=item B</pagesize>

Displays the currently-set pagesize.

=back

=head1 ENCRYPTION

B<ICBM> v1.4.0 and later supports encryption of personal messages between B<ICBM> and other ICB
clients which implement B<ICBM>'s client-to-client encryption scheme.  This encryption uses the
Blowfish cipher with a per-user, per-session shared key generated by taking a Base64-encoded
SHA1 hash of a randomly-chosen line from message history.  Session keys are exchanged encrypted
with a one-time key generated using Diffie-Hellman key exchange.  Interoperability of this key
exchange relies upon all clients using the same initialization table of 48-bit primes.

To initiate encryption to another user, enter the command B</setup USER>.  This will begin the
key negotiation handshake with the remote user.  If the handshake succeeds, B<ICBM> will display
the following message within a few seconds:

B<[=SECURE=] Session key for user %s established>

This indicates that both you and the remote user now have a copy of the valid session key.  This
message will also be displayed when a remote user successfully establishes a session key with
your client.  This key is valid between your two clients as long as both clients maintain the
same session and the same nick.

If you receive the following warning message:

B<[=Warning=] Cannot setup: user USER has encryption disabled>

then your remote user's client supports encryption, but the user has it disabled.

If there is no result within a few seconds, or if you receive a puzzled response from the user,
the user's client does not support encryption.

Once a session has been set up, all private messages between the two clients will automatically
be encrypted with the last known good key.  If one client changes nick, while both clients are
in the same group, B<ICBM> will react to the nick-change message and update its session key
table.  If one client makes a nick change that is not visible to the other, or signs off and
logs back on, the shared key will become invalid.  Sending a message encrypted using an invalid
key will result in the following message on the sender's console:

B<[=Warning=] Message failed (stale key): Set up new key for user USER>

And on the recipient's console:

B<[=Warning=] Unable to decrypt encrypted packet from USER : No session key>

If this occurs, a new session key must be negotiated using the B</setup> command.  Alternately
you can choose to end the encrypted session using B</revoke nick>.  This command will cause both
your and the other user's keys for the session to be cancelled.

Encryption can be turned on or off during a session, if available, using the B</encryption on|off>
command.  This is equivalent to B</set encryption 1|0>.

See the file README.ENCRYPTION for additional information.

=head1 ICBM VARIABLES

Many aspects of B<ICBM>'s internal behavior are controlled by variables which can be set either
in the startup file using the B<set()> function, or interactively using the B</set> command.  The
variables available, their defaults, and their effects, are as follows:

=over 4

=item B<beep_on_error> (default: 1)

Controls whether B<ICBM> emits an audible beep on certain types of input errors.  Set to 0
to disable.

=item B<beeps> (default: 1)

Controls whether B<ICBM> emits an audible beep when you receive a beep message from the server.
Set to 0 to disable.

=item B<cc_msg_lists> (default: 0)

Controls whether all recipients of a message with private multiple recipients receive a list
of the persons to whom the message was sent.  Set to 1 to enable.

=item B<cmdchar> (default: /)

The character used to prefix ICB/B<ICBM> commands.  May be set only to a single character, which
may not be one of the following reserved characters:  \ ^ !

=item B<deflogfile> (default: $HOME/ICBM.log)

Controls the default location of the B<ICBM> log file.

=item B<echo_outgoing> (default: 1)

Controls whether outgoing open and private messages that you type are echoed to the output
window.  (You will probably not want to use both this and echoback.)  Set to 0 to disable.

=item B<logtimeformat> (default: "%H:%M:%S %Z %b %d %Y")

This is the format string used to timestamp log open and close events.  It may be set to any
format string valid for strftime().  Changes to B<logtimeformat> take effect immediately.

=item B<modwarn> (default: 1)

Controls whether B<ICBM> issues a one-time warning if you attempt to quit while still
moderator of the group you are in.  Set to 0 to disable.

=item B<report_correction_set> (default: 1)

Controls whether B<ICBM> displays a confirmation message on loading corrections.  Set to
0 to disable.

=item B<report_load_success> (default: 1)

Controls whether B<ICBM> displays a status message to report successful loading of scriptfiles
loaded with the load command.  Set to 0 to disable.

=item B<report_load_warnings> (default: 1)

Controls whether B<ICBM> displays a warning message if it is unable to locate a file in a
load command.  Set to 0 to disable.

=item B<report_load_errors> (default: 1)

Controls whether B<ICBM> displays an error message if an error occurs while loading a file.
Set to 0 to disable.

=item B<report_url_capture> (default: 1)

Controls whether B<ICBM> displays a message when it detects and stores a URL.  Set to 0 to
disable.

=item B<readhistsize> (default: 500)

Controls the maximum size of B<ICBM>'s history buffer for incoming message packets.  Set to
0 to disable message history.

=item B<tab_del_on_error> (default: 1)

Controls whether B<ICBM> deletes nicks from tab history if they generate a 'not signed on'
error.  Set to 0 to disable.

=item B<timeformat> (default: "%H:%M:%S")

This is the format string used to display the time in the status bar clock.  It may be set
to any format string valid for strftime().  Changes to B<logtimeformat> take effect immediately.

=item B<timestampformat> (default: "%H:%M:%S")

This is the format string used to timestamp message packets.  It may be set to any format
string valid for strftime(); however, it is recommended that you B<keep it short>.  Changes
to B<timestampformat> take effect immediately.

=item B<writehistsize> (default: 500)

Controls the maximum size of B<ICBM>'s history buffer for typed commands and messages.  Set
to 0 to disable typing history.

=item B<writesize> (default: 5)

This variable controls the number of lines set aside in your terminal for the input window.
It B<CAN ONLY BE SET IN THE datadir/settings FILE>, and cannot be changed while B<ICBM> is
running (or, more accurately, changing it while B<ICBM> is running has no effect).

=back

More variables may be added in future as necessary.

=head1 SCRIPTING

B<ICBM> can be extended by user-side scripting in Perl.  The script file does not need to
begin with a #!/usr/bin/perl, but it does no harm if it does.

Several commands exist in ICBM to facilitate scripting.  The set is small, but versatile.
These commands are explained below.

=over 4

=item B<addcmd()>

addcmd("news");

Used to register a user-defined command in B<ICBM>'s command space.

=item B<alias()>

alias(".", "who .");

Creates an alias, just like the B</alias> command.

=item B<unalias()>

unalias(".");

Unregisters an alias, just like the B</unalias> command.

=item B<delcmd()>

delcmd("news");

This, exactly like the B</delcmd> command, unregisters a user function from the command
space.

=item B<get_group()>

my $group = get_group();

Returns the name of the current group to a script.

=item B<get_mod()>

if (get_mod()) { ..... }

Returns a binary flag indicating whether you are mod of the current group or not.

=item B<get_nick()>

my $nick = get_nick();

Returns your current active nick to a script.

=item B<get_urls()>

my @urls = get_urls();

Returns the list of URLs captured by the internal URL grabber for use by userside scripts.

=item B<hilight()>

hilight('larry', 'moe', 'curly');

Adds nicks to the hilight list, exactly like B</hilight>.

=item B<unlight()>

unlight('larry', 'moe', 'curly');

Removes nicks from the hilight list, exactly like B</unlight>.

=item B<docommand()>

docommand("/m server news 8");

This function allows any command which can be typed at the keyboard to be embedded in
a script.  This command enables the user to write scripts that interact with the server
and client using any defined B<ICBM> command or alias, without having to understand
the internal workings of B<ICBM>, the server, or the ICB protocol.

=item B<load()>

load('more_commands');

Just like the B</load> command, this causes B<ICBM> to load a specified script file.

=item B<loadhook()>

loadhook('my_hooks');

This causes B<ICBM> to load a file containing hook functions.  B<load()> and
<loadhook()> are not interchangeable, as they load the files into the context of
different threads.

=item B<out()>

out ("You can't get the wood, you know.");

Displays a message in the read window, just like the B</out> command.

=item B<set()>

set ('modwarn', 0);

Used to change client variables from a script.

=item B<set_color()>

set_color ('output', 'green');

set_color ('output', 'green', 'magenta', 'bold', 'reverse');

Changes a color setting, like the B<setcolor> command.

=back

=head1 HOOKS

In addition to the above scripting functions, B<ICBM> provides a range of different hooks
which allow scripted actions to be triggered by a variety of ICB events.  Two functions
exist for managing hooks from a script:

=over 4

=item B<addhook()>

addhook ('connect', 'register');

The B<addhook()> function takes two operands, an event class and a function name, and adds
the specified function to the list of functions called by B<ICBM> when an event of that class
occures.  The function will be passed a single string containing the entire contents of the
ICB message packet which triggered the event.  It is your responsibility to decide what to do
based on the contents of the packet.

A warning will be issued if the event class is unknown.

=item B<delhook()>

delhook ('connect', 'register');

The B<delhook()> function unregisters a specified hook function.  B<delhook()> is to B<addhook()>
exactly as B<delcmd()> is to B<addcmd()>.

A warning will be issued if the event class is unknown or if the hook function is not found.

=back

Script functions are passed an array containing the data fields of the ICBM message packet.
Depending on the packet type, there may be from 0 to 8 fields.  It is the responsibility
of the hook function to parse these fields and determine what to do with them.  B<MOST> 
packets will contain only two fields; in this case, argument 0 (accessed as $_[0]) will
contain either the name of the sender of an open or private message, or a specific message
type string such as 'Notify-On' in the case of a server message, and argument 1 (accessed
as @_[1]) will contain the actual text of the message.  See the provided sample_hooks file
for example hook functions, including a show_packet function that could be used to display
all the fields of the function that triggered the packet.  A hook function could even call
this function internally by including the following line:

=over 4

show_packet(@_);

=back

The event classes currently defined are as follows (not in any particular logical order):

=over 4

=item B<connect>

Called exactly once when your client initially connects to the server.  This is one of only
two hook types that does B<NOT> pass a message packet (the other is B<modwarn>).

=item B<openmsg>

Called whenever your client receives an open message.

=item B<privmsg>

Called whenever your client receives a private message.

B<NOTE:> The sender nick in private messages will be prefixed with an asterisk (e.g,
*fred instead of fred).  This enables a single hook function to be used for both open
and private messages and to be able to tell which of the two types of event triggered it.
If you're writing a hook function that will automatically reply to private messages,
B<remember to strip the asterisk off>.

=item B<newpriv>

Called whenever your client receives a private message that begins a conversation
(i.e, from a user not already in the tab history list).

=item B<memo>

Called whenever someone sends you a /write memo while you are logged in, or whenever
there is one or more /write memos already waiting for you when you log in.

=item B<beep>

Called whenever another ICB user beeps you.

=item B<ping>

Called whenever another ICB user pings your client to see how much lag time exists
between your client and theirs.

=item B<group>

Called whenever you enter a group.

=item B<join>

Called whenever someone else enters the group you're in.

=item B<notify>

Called whenever someone you have in your notify list signs onto or off of the server
(see the ICB server help, B</s_help>, for documentation of the B<notify> command).
It is your responsiblity to distinguish Notify-On and Notify-Off events.

=item B<modgain>

Called whenever you become the moderator of a group, including by creating a new group.

=item B<modloss>

Called whenever you lose moderatorship of a group (usually via the server's idle-mod feature).

=item B<modpass>

Called whenever you pass moderatorship of a group, or drop moderation on the group altogether.

=item B<modwarn>

Called when you attempt to exit B<ICBM> while still moderator of a group.  This is one of only
two hook types that does B<NOT> pass a message packet (the other is B<connect>).

=item B<boot>

Called whenever someone boots you from a group.

=item B<idleboot>

Called whenever you are idlebooted from a group by the server.

=item B<error>

Called whenever the server sends you an error message packet.

=item B<alert>

Called whenever the server sends you an alert message packet.

=item B<awol>

Called when the server reports that a user you just tried to message (etc.) is no longer logged on.

=item B<nickchg>

Called when a user in the group you are in changes their nick.

=item B<rename>

Called whenever the group you are in is renamed.

=item B<status>

Called whenever the server sends you any other status packet.

=item B<trigger>

Called on ALL packets when a generic trigger event has been set (see TRIGGERS).

=item B<url>

Called whenever the URL grabber captures a URL token from a message packet.  The hook function
is sent an abbreviated packet containing only the sender name and the URL token.  The hook
function may be called multiple times on a given message if the message packet contains multiple
detected URL tokens.

=back

=head1 TRIGGERS

In addition to all the above, B<ICBM> contains a mechanism for implementing generic
trigger events.  At present, only one trigger should be defined at a time.  This mechanism
allows the user to define a function which will send a command to the ICB server, wait
for the server's response to that command, and then finish executing.  There are a set of
eight functions for using this functionality with the 'trigger' hook type referenced above:

=over 4

=item B<get_trig_mask()>, B<set_trig_mask()>

These functions set and read, respectively, the mask against which server responses should
be checked by the hook function which will "catch" the trigger.

=item B<get_trig_result()>, B<set_trig_result()>

These functions set and read, respectively, a result field which the trigger hook function
can use to pass the trigger result back to the function which set the trigger.

=item B<get_trig_status()>, B<set_trig_status()>

These functions set and read a flag used to determine whether a trigger is active and,
accordingly, whether the trigger function list should be called.  Functions using triggers
should always clear the trigger by calling set_trig_status(0) after the desired trigger
has been caught.

=item B<get_trig_action()>, B<set_trig_action()>

These functions set and read a variable which can be used to pass an action to the trigger
hook function.

=back

The basic method which should be used to utilize generic triggers is as follows:

=over 4

Set up the trigger mask, ensure the result is set to 0, and set status to 1 to
enable the trigger.

Send the server command

Sleep, polling the trigger result, until the trigger is caught

Clear the trigger and resume processing.

=back

=head1 SAMPLE SCRIPT

The following simple scripting example illustrates how to put the above functions together
with ICB commands and B<ICBM> commands to achieve a variety of results:

sub ploogie
{
    my ($input) = $_[0];

    if ($input)
    {
        docommand("/msg icbm Ploogie!  $input");
        docommand($input);
        out ("We can TOO get the wood!");
    }
    else
    {
        docommand('/msg icbm Ploogie!');
        docommand('Ploogie!');
        out ("You can't get the wood, you know.");
    }
    chomp (my $date = `date`);
    out ($date);
}
addcmd ("ploogie");

setcolor ('output', 'green', 'magenta', 'bold', 'reverse');

out ("Today's date is $date, ICBM time.");

sleep (2);

setcolor ('output', 'green');

out ("My god, those colors were hideous beyond mortal imagination.");
out ("Please kill yourself now as a service to humanity.");

set ('readhistsize', 750);

load ('more_commands');

If you should happen to accidentally load a function that overwrites a key built-in
command that you need, you can use delcmd to delete the user-defined function and
return to the built-in function.  In the worst case, you can always kill B<ICBM>,
remove the offending code from your script files, and restart B<ICBM>.

=head1 SAMPLE HOOKS

The following illustrates some basic example hook functions:

sub who
{
    docommand ('/who .');
}

addhook ('group', 'who');

sub register
{
    docommand ('/msg server p MYPASSWORD');
}

addhook ('connect', 'register');

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

    if ($msg =~ /^bah$/i)
    {
        docommand ('Humbug');
    }
    elsif ($msg =~ /^bah!$/i)
    {
        docommand ('Humbug!');
    }
}
addhook ('openmsg', 'humbug');

sub sample_url_hook
{
    my ($who, $msg) = @_;
    show_packet(@_);
    docommand ("/msg $who You sent the URL $msg");
    docommand ("Captured URL $msg");
}
addhook('url','sample_url_hook');


=head1 SAMPLE TRIGGER USAGE

The following two functions, placed in the commands and hooks files respectively,
implement a single-command function to drop a zombie connection (or someone else
using your nick) and recover your nick.

The triggering function:

sub reclaim
{
    my $nick = $_[0];
    my $result;

# Set up the generic trigger

    set_trig_mask ("$nick .* disconnected");
    set_trig_result (0);
    set_trig_status (1);

    docommand ("/drop $nick MYPASSWORD");

# Sleep on trigger

    until (($r = get_trig_result()) =~ /disconnected|found/)
    {
        out ("Result: $r");
        sleep (1);
    }

# Clear the trigger

    set_trig_status (0);
    set_trig_result (0);
    set_trig_mask ('');

    docommand("/nick $nick");
    docommand("/m server p MYPASSWORD");
}
addcmd("reclaim");


And the trigger hook:

sub deadjim
{
    my ($who, $msg) = @_;
    my $mask = get_trig_mask();

    if ($msg =~ /$mask/i || $who =~ /User not found/)
    {
        set_trig_status (0);
        set_trig_result ($msg =~ /$mask/i ? $msg : $who);
    }
}
addhook('trigger', 'deadjim');


=head1 IMPORTANT PROGRAMMING NOTE:

Because B<ICBM> is multithreaded, you should not use FORK calls in your scripts, nor use
system() calls to start external perl scripts which then FORK.  Doing so may have
unexpected results, including creating a rise in system load with no visible cause.
It B<should> go without saying that B<ICBM> scripts B<should NEVER call exec()>.

=head1 COLORS

Colors are specified in $DATADIR/colors quite simply as follows:

    nickname	yellow black
    hostmask	green black
    modstar	red black bold
    userflags	red white
    idletime	cyan black bold reverse
    etc.

The supported colors and attributes are as follows:  bold, reverse, black, red, green,
yellow, blue, magenta, cyan, and white.  All attributes should work on any terminal
which supports color.  The order of fields is: color setting name, foreground color,
background color, bold or reverse, bold or reverse.  If you wish to specify bold or reverse,
you MUST specify the background color; otherwise, it can be omitted and will be assumed to
be black.

The supported color settings are (not in any particular order):

    normal        "Normal" text in the ICB window
    output        Your own typing and script output
    ownmsg        Your own outgoing text, if echoback is enabled
    personal      Body of personal messages to you
    encrypted     Body text of encrypted personal and outgoing messages
    persfrom      Nickname of personal message sender
    hilight       Body of personal message from highlighted sender
    hilightfrom   Nickname of highlighted personal message sender
    who_hdr       Header lines in /who listings
    nickname      Nick of ICB user
    hostmask      ICB user's hosername and host address
    modstar       The star indicating who has mod in a group
    userflags     Unregistered or Away flags on a user
    idletime      Time since an ICB user last typed anything
    logintime     Time an ICB user last logged in
    beep          Beep message text
    beepfrom      Nickname of beep sender
    status        Status messages from the server
    alert         Alerts from the server
    error         Server error messages
    warning       Warning messages from the client (usually script-related)
    more          The "--- PAUSED: Press Enter ..." prompt if you have paging turned on
    sbrkt         Color of [ ] brackets in server messages
    abrkt         Color of < > brackets around nicks
    pbrkt         Color of <* *> brackets around /msg nicks
    statline      The ICB window status line
    debug         Output from the icb_debug() function

All color settings are case insensitive.  Whitespace is not significant.

If you expect to get color and you're not, try changing your TERM setting.  For instance,
although most xterms support ANSI color (which is used by B<ICBM>'s non-interactive mode),
curses (used by B<ICBM>'s interactive mode) generally does not believe that an xterm will
support color, but if you change the TERM variable to xterm-color or color_xterm, curses
will be perfectly happy with it.  You can either make this change in your shell initialization
files, or simply set TERM when you start icbm as in the following example:

    TERM=xterm-color icbm -g ICBM -n ICBM -color

=head1 DEPENDENCIES

B<ICBM> should work on any threaded Perl 5.6.0 or later installation.  It requires the
following Perl modules for basic functionality:

    threads
    threads::shared
    POSIX
    Curses
    Config
    Getopt::Long
    Module::Load::Conditional
    Net::ICB
    Pod::Usage
    Term::ANSIColor
    Term::ReadKey


Most of these are included as standard with the Perl distribution.  You will probably have
to install Term::ANSIColor and Net::ICB.  On Debian Linux installations, you may also need
to manually install Term::ReadKey.

The following additional modules are required in order to enable encryption:

    Compress::Zlib
    Crypt::Blowfish
    Crypt::CBC
    Crypt::DH::GMP
    Digest::SHA
    MIME::Base64

These modules are conditionally loaded if all are present.  If one or more is missing, none
are loaded and B<ICBM> will not permit encryption to be enabled.

The standalone B<primegen> tool, used to generate a new table of 48-bit primes if desired,
requires:

    Math::Pari
    Crypt::Random
    Crypt::Primes

=head1 ENVIRONMENT VARIABLES

=over 4

=item B<ICBM_DATA>

Sets the location of B<ICBM>'s data directory, if present.  The default
location is $HOME/.icbm if ICBM_DATA is not set.

=item B<ICBNAME>

B<ICBM> will honor the B<CICB>/fnet ICBNAME variable if it is present.

=back

=head1 FILES

=over 4

=item B<$DATADIR/colors>

Client color setting file

=item B<$DATADIR/commands>

Default file for user scripts and data

=item B<$DATADIR/defaults>

Early-initialization client variables ONLY

=item B<$DATADIR/hooks>

Default file for user hook functions and data

=item B<icbserverdb>

ICB server database (compatible with cicb).
May be located in any of the following locations:

    $HOME/.icbserverdb
    $DATADIR/icbserverdb
    /usr/share/icb/icbserverdb
    /usr/local/share/icb/icbserverdb
    /usr/local/lib/icb/icbserverdb
    /usr/local/lib/icbserverdb
    /usr/lib/icb/icbserverdb
    /usr/lib/icbserverdb
    ./share/icb/icbserverdb

=item B<$DATADIR/primes>

Contains the table of 48-bit primes used for Diffie-Hellman key exchange.
May be located in any of the following locations:

    $DATADIR/primes
    /usr/share/icb/primes
    /usr/local/share/icb/primes
    /usr/local/lib/icb/primes
    /usr/local/lib/primes
    /usr/lib/icb/primes
    /usr/lib/primes
    ./share/icb/primes

=back

=head1 REQUIREMENTS

B<ICBM> requires Perl 5.6.0 or later, compiled with ithreads support; the Perl modules
listed above in B<DEPENDENCIES>, some of which may not be included with your Perl
distribution; and a Curses-compatible terminal.  This should not be a problem on any
Unix-based platform.  Usage on Windows is probably best accomplished via cygwin.

It is not believed at this time that B<ICBM> will run using ActivePerl on Windows, as the
ActivePerl install lacks both ithreads and at least one of the required modules, and
correcting these deficiencies in ActivePerl is not a trivial process.

=head1 KNOWN BUGS AND DEFICIENCIES

=over 4

=item B<-clear and -password>

B<CICB>'s B<-clear> option, which sanitizes the commandline and is intended for use with
the B<-password> option (which implies B<-clear>) is difficult to implement from a perl
script, and I do not at this time know how to do it.  Without B<-clear>, B<-password> is
massively insecure, as your password is left hanging out in the ps listing for anyone on
the system to see.  Therefore neither B<-password> or B<-clear> is implemented in B<ICBM>,
since I do not have a way for B<ICBM> to implement them securely.  Workarounds or solutions
for this problem are welcomed; I'd love to know how to do this in Perl.

=item B<Color in who listings>

Color attributes in WHO listings are applied to entire fields, which may be larger than the
data they contain.  This will be apparent only when using background colors or reverse video.
This is a minor cosmetic defect only, and fixing it is a low-priority item because there
are more important things needing work right now.

=item B<Text attributes do not update dynamically along with text colors>

There's nothing I can reasonably do about this.  It's an artifact of the way text colors
vs. text attributes are implemented in curses, and can't really be fixed without re-implementing
curses.

=item B<Some failure modes can leave terminal settings disturbed>

B<ICBM> makes every attempt to clean up after itself by issuing an 'stty sane' as the last thing
it does before it exits.  However, some failure modes may occur in which B<ICBM> does not get
a chance to clean up after itself.  In this event, manually running 'stty sane' should restore
the terminal.

=item B<SIGWINCH>

B<ICBM> does not handle window size changes particularly gracefully.  (In fact, it handles them
distinctly ungracefully.)  This is largely a limitation imposed by Curses.pm's implementation
(or rather, non-implementation) of B<is_term_resized()> and the KEY_RESIZE event.  Workarounds
or solutions for this problem are welcomed.  I currently believe the problem to be insoluble
because insofar as I have been able to determine, whether or not I trap B<SIGWINCH> myself, it
appears B<icbm> never actually receives either B<SIGWINCH> or B<KEY_RESIZE>.

=item B<Input scrolling>

When the user types more lines of text than the input window will hold, B<ICBM> scrolls the
input window up correctly, but subsequently fails to calculate cursor position correctly and
does not scroll the input window back correctly when appropriate.  I haven't yet figured out
why this is happening, but I suspect it's still another deficiency in Curses.pm.  Solutions for
this problem are welcomed.  Dynamically growing the input window would be nice, but I'm beginning
to strongly suspect that is yet another thing that won't work Because Curses.

=item B<Color not available on Solaris 10>

Curses color does not appear to be available through Curses.pm on Solaris 10.  I don't yet know
why this is, but I'm assuming that it has to do with the way libcurses is compiled on Solaris 10.
I am considering this a WONTFIX at this point because, seriously, Solaris is dead.  But if you
have a fix, feel free to contribute it.

=item B<Unclean exit on OSX> (reported by Ryan Powers)
Perl reports an error "recv size: Bad file descriptor at /Library/Perl/5.8.6/Net/ICB.pm line 285"
on exit when running on OSX.  I believe this bug to be fixed, but do not have an OSX machine to
test on.



=back

=head1 TODO

=over 4

=item Implement scrolling the read window back to view previous messages (use PgUp/PgDn?)
This seems to be a terminal-dependent problem that needs to be separately fixed for xterm,
tmux, screen, and alacritty, and possibly each valid combination thereof.

=back

=head1 REPORTING BUGS

Send bug reports to the author.  The B<ICBM> mailing list has been discontinued
due to a combination of lack of subscribers and a surfeit of attempts to spam it.

=head1 LICENSE

B<ICBM> is copyright (C) 2003-2022 Phil Stracchino.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

=head1 OBTAINING ICBM

B<ICBM> can be downloaded from http://co.ordinate.org/icbm/ as ICBM-current.tar.gz.

=head1 AUTHOR

B<ICBM> is written and maintained by Phil Stracchino (icbm@co.ordinate.org), based upon
ideas from Roger Espel Llima's B<sirc> IRC client and John Vinopal's B<Net::ICB> module.
The scripting code in particular was heavily influenced by B<sirc>.

=cut
