The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Net::Hotline::Client;

## Copyright(c) 1998-2002 by John C. Siracusa.  All rights reserved.  This
## program is free software; you can redistribute it and/or modify it under
## the same terms as Perl itself.

use strict;

use vars qw(@ISA $VERSION $DEBUG);

use Carp;
use IO::File;
use IO::Socket;
use Net::Hotline::User;
use Net::Hotline::Task;
use Net::Hotline::PrivateChat;
use Net::Hotline::FileListItem;
use Net::Hotline::FileInfoItem;
use Net::Hotline::TrackerListItem;
use Net::Hotline::Protocol::Packet;
use Net::Hotline::Protocol::Header;
use Net::Hotline::Shared qw(:all);
use Net::Hotline::Constants qw(:all);

if($^O eq 'MacOS') # "#ifdef", where have you gone...
{
  require Mac::MoreFiles;
  require Mac::Files;
}

use AutoLoader 'AUTOLOAD';

#
# Class attributes
#

$VERSION = '0.83';
$DEBUG   = 0;

# CRC perl code lifted from Convert::BinHex by Eryq (eryq@enteract.com)
# An array useful for CRC calculations that use 0x1021 as the "seed":
my(@CRC_MAGIC) = (
    0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
    0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
    0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
    0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
    0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
    0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
    0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
    0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
    0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
    0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
    0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
    0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
    0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
    0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
    0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
    0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
    0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
    0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
    0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
    0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
    0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
    0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
    0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
    0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
    0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
    0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
    0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
    0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
    0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
    0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
    0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
    0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
);

1;

#
# Non-autoloaded object methods
#

sub new
{
  my($class) = shift;

  my($self) =
  {
    'NICK'         => undef,
    'LOGIN'        => undef,
    'COLOR'        => undef,
    'SERVER_PORT'  => undef,
    'SERVER_ADDR'  => undef,
    'TRACKER_ADDR' => undef,

    'SOCKET'       => undef,
    'BLOCKING'     => 1,
    'SERVER'       => undef,
    'SEQNUM'       => 1,

    'USER_LIST'    => undef,
    'NEWS'         => undef,
    'FILES'        => undef,
    'AGREEMENT'    => undef,
    'PCHATS'       => undef,
    'TASKS'        => undef,

    'FILE_INFO'    => undef,

    'HANDLERS'  =>
    {
      'AGREEMENT'     => undef,
      'BAN'           => undef,
      'CHAT'          => undef,
      'CHAT_ACTION'   => undef,
      'COLOR'         => undef,
      'EVENT'         => undef,
      'FILE_DELETE'   => undef,
      'FILE_GET'      => undef,
      'FILE_GET_INFO' => undef,
      'FILE_LIST'     => undef,
      'FILE_MKDIR'    => undef,
      'FILE_MOVE'     => undef,
      'FILE_SET_INFO' => undef,
      'ICON'          => undef,
      'JOIN'          => undef,
      'KICK'          => undef,
      'LEAVE'         => undef,
      'LOGIN'         => undef,
      'MSG'           => undef,
      'NEWS'          => undef,
      'NEWS_POST'     => undef,
      'NEWS_POSTED'   => undef,
      'NICK'          => undef,
      'PCHAT_ACCEPT'  => undef,
      'PCHAT_CREATE'  => undef,
      'PCHAT_INVITE'  => undef,
      'PCHAT_JOIN'    => undef,
      'PCHAT_LEAVE'   => undef,
      'PCHAT_SUBJECT' => undef,
      'QUIT'          => undef,
      'SEND_MSG'      => undef,
      'SERVER_MSG'    => undef,
      'TASK_ERROR'    => undef,
      'USER_GETINFO'  => undef,
      'USER_LIST'     => undef
    },

    'BLOCKING_TASKS'         => undef,
    'DEFAULT_HANDLERS'       => undef,
    'HANDLERS_WHEN_BLOCKING' => undef,

    'LOGGED_IN'       => undef,

    'EVENT_TIMING'    => 1,
    'CONNECT_TIMEOUT' => 15,
    'PATH_SEPARATOR'  => HTLC_PATH_SEPARATOR,
    'HTXF_BUFSIZE'    => HTXF_BUFSIZE,

    'DOWNLOADS_DIR'   => undef,
    'DATA_FORK_EXT'   => '.data',
    'RSRC_FORK_EXT'   => '.rsrc',

    'LAST_ACTIVITY'   => time(),
    'LAST_ERROR'      => undef,
    'MACOS'           => ($^O eq 'MacOS') ? 1 : 0
  };

  bless  $self, $class;
  return $self;
}

sub agreement { $_[0]->{'AGREEMENT'} }

sub blocking
{
  my($self, $blocking) = @_;

  return $self->{'BLOCKING'}  unless(@_ == 2);

  if(ref($self->{'SERVER'}) && $self->{'SERVER'}->opened())
  {
    _set_blocking($self->{'SERVER'}, $blocking);
  }

  $self->{'BLOCKING'} = (($blocking) ? 1 : 0);
  return $self->{'BLOCKING'};
}

sub blocking_tasks
{
  my($self, $arg) = @_;
  $self->{'BLOCKING_TASKS'} = ($arg) ? 1 : 0  if(@_ == 2);
  return $self->{'BLOCKING_TASKS'};
}

sub connect_timeout
{
  my($self, $secs) = @_;
  $self->{'CONNECT_TIMEOUT'} = $secs  if($secs =~ /^\d+$/);
  return $self->{'CONNECT_TIMEOUT'};
}

sub default_handlers
{
  my($self, $arg) = @_;
  $self->{'DEFAULT_HANDLERS'} = ($arg) ? 1 : 0  if(@_ == 2);
  return $self->{'DEFAULT_HANDLERS'};
}

sub downloads_dir
{
  my($self, $dir) = @_;
  $self->{'DOWNLOADS_DIR'} = $dir  if(-d $dir);
  return $self->{'DOWNLOADS_DIR'};
}

sub data_fork_extension
{
  my($self, $ext) = @_;
  croak("The data fork extension may not be the same as the resource fork extension!")
    if($ext eq $self->{'DATA_FORK_EXT'}); 
  $self->{'DATA_FORK_EXT'} = $ext  if(defined($ext));
  return $self->{'DATA_FORK_EXT'};
}

sub event_timing
{
  my($self, $secs) = @_;

  if(defined($secs))
  {
    croak qw(Bad argument to event_timing() - "$secs")  if($secs =~ /[^0-9.]/);
    $self->{'EVENT_TIMING'} = $secs;
  }

  return $self->{'EVENT_TIMING'};
}

sub files    { $_[0]->{'FILES'}    }
sub handlers { $_[0]->{'HANDLERS'} }

sub handlers_during_blocking_tasks
{
  my($self, $arg) = @_;
  $self->{'HANDLERS_WHEN_BLOCKING'} = ($arg) ? 1 : 0  if(@_ == 2);
  return $self->{'HANDLERS_WHEN_BLOCKING'};
}

sub last_error  { $_[0]->{'LAST_ERROR'} }
sub clear_error { $_[0]->{'LAST_ERROR'} = undef }

sub xfer_bufsize
{
  my($self, $size) = @_;
  $self->{'HTXF_BUFSIZE'} = $size  if($size =~ /^\d+$/);
  return $self->{'HTXF_BUFSIZE'};
}

sub last_activity
{
  my($self) = shift;
  return $self->{'LAST_ACTIVITY'};
}

sub news { $_[0]->{'NEWS'} }

sub path_separator
{
  my($self, $separator) = @_;
  $self->{'PATH_SEPARATOR'} = $separator  if($separator =~ /^.$/);
  return $self->{'PATH_SEPARATOR'};
}

sub rsrc_fork_extension
{
  my($self, $ext) = @_;
  croak("The resource fork extension may not be the same as the data fork extension!")
    if($ext eq $self->{'RSRC_FORK_EXT'}); 
  $self->{'RSRC_FORK_EXT'} = $ext  if(defined($ext));
  return $self->{'RSRC_FORK_EXT'};
}

sub pchats   { $_[0]->{'PCHATS'}      }
sub userlist { $_[0]->{'USER_LIST'}   }

sub server
{
  $_[0]->{'SERVER_ADDR'} .
    ($_[0]->{'SERVER_PORT'} ne HTLS_TCPPORT) ?
      ":$_[0]->{'SERVER_PORT'}" : '';
}

sub connect
{
  my($self, $server) = @_;

  my($address, $port);

  if(($address = $server) =~ s/^([^ :]+)(?:[: ](\d+))?$/$1/)
  {
    $port = $2 || HTLS_TCPPORT;
  }
  else
  {
    croak("Bad server address: $server");
  }

  eval
  {
    $SIG{'ALRM'} = sub { die "timeout" };
    alarm($self->{'CONNECT_TIMEOUT'});

    $self->{'SERVER'} = 
      IO::Socket::INET->new(PeerAddr =>$address,
                            PeerPort =>$port,
                            Proto    =>'tcp');

    alarm(0);
    $SIG{'ALRM'} = 'DEFAULT';
  };

  if($@ =~ /timeout/)
  {
    $self->{'LAST_ERROR'} = "Timed out after $self->{'CONNECT_TIMEOUT'} seconds";
    return;
  }

  if(!$self->{'SERVER'} || $@)
  {
    $self->{'LAST_ERROR'} = $@ || $! || 'Connection failed';
    return;
  }

  $self->{'SERVER'}->autoflush(1);

  $self->{'SERVER_ADDR'} = $address;
  $self->{'SERVER_PORT'} = $port;

  return(1);
}

sub disconnect
{
  my($self) = shift;

  if(ref($self->{'SERVER'}) && $self->{'SERVER'}->opened())
  {
    $self->{'SERVER'}->close();
    $self->{'LOGGED_IN'} = undef;
    $self->{'SERVER_ADDR'} = undef;
    return(1);
  }

  $self->{'LAST_ERROR'} = 'Not connected.';
  return;
}

sub login
{
  my($self, %args) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_login_now(%args); 
  }
  else
  {
    return $self->_login(%args); 
  }
}

sub _login_now
{
  my($self, %args) = @_;

  my($no_news, $no_userlist, $task_num, $task, $packet);

  $no_news     = $args{'NoNews'};
  $no_userlist = $args{'NoUserList'};

  $args{'NoNews'} = $args{'NoUserList'} = undef;

  $task_num = $self->_login(%args);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    $self->disconnect();
    return;
  }

  unless($no_news)
  {
    unless($self->get_news())
    {
      $self->{'LAST_ERROR'} = "Login succeeded, but could not get news.";
      return("0E-0");
    }
  }

  unless($no_userlist)
  {
    unless($self->get_userlist())
    {
      $self->{'LAST_ERROR'} = "Login succeeded, but could not get userlist";
      return("0E-0");
    }
  }

  return(1);
}

sub _login
{
  my($self, %args) = @_;

  my($nick, $login, $password, $icon, $enc_login, $enc_password,
     $proto_header, $data, $response, $task_num, $server);

  $server = $self->{'SERVER'} or croak "Not connected to a server";

  unless($server->opened())
  {
    $self->{'LAST_ERROR'} = "login() called before connect()";
    return;
  }

  $nick  = $args{'Nickname'} || HTLC_DEFAULT_NICK;
  $login = $args{'Login'}    || HTLC_DEFAULT_LOGIN;
  $icon  = $args{'Icon'}     || HTLC_DEFAULT_ICON;
  $password = $args{'Password'};

  $self->{'NICK'}  = $nick;
  $self->{'LOGIN'} = $login;
  $self->{'ICON'}  = $icon;

  _hlc_write($self, $server, \HTLC_MAGIC, HTLC_MAGIC_LEN) || return;
  _hlc_read($self, $server, \$response, HTLS_MAGIC_LEN) || return;

  if($response ne HTLS_MAGIC)
  {
    $self->{'LAST_ERROR'} = "Handshake failed.  Not a hotline server?";
    $self->disconnect();
    return;
  }

  $enc_login    = _encode($login);
  $enc_password = _encode($password);

  $proto_header = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_LOGIN);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(SIZEOF_HL_PROTO_HDR + 
                     length($enc_login) +
                     length($enc_password) +
                     length($nick));
  $proto_header->len2($proto_header->len);

  my($fmt) = 'nnna*nna*nna*nnn';

  $data = $proto_header->header() .
          pack($fmt, 0x0004,                  # Num atoms

                     HTLC_DATA_LOGIN,         # Atom type
                     length($enc_login),      # Atom length
                     $enc_login,              # Atom data

                     HTLC_DATA_PASSWORD,      # Atom type
                     length($enc_password),   # Atom length
                     $enc_password,           # Atom data

                     HTLC_DATA_NICKNAME,      # Atom type
                     length($nick),           # Atom length
                     $nick,                   # Atom data

                     HTLC_DATA_ICON,          # Atom type
                     0x0002,                  # Atom length
                     $icon);                  # Atom data

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: LOGIN - $task_num\n");
    $self->{'TASKS'}->{$task_num} =
      new Net::Hotline::Task($task_num, HTLC_TASK_LOGIN, time());
  }
  else { return }

  unless($args{'NoUserList'})
  {
    $self->req_userlist();
  }

  unless($args{'NoNews'})
  {
    $self->req_news();
  }

  _set_blocking($server, $self->{'BLOCKING'});

  return($task_num);
}

sub run
{
  my($self) = shift;

  my($server) = $self->{'SERVER'}  or croak "Not connected to a server";
  return  unless($server->opened());

  my($ret, $packet);

  $packet = new Net::Hotline::Protocol::Packet;

  while($ret = $packet->read_parse($server, $self->{'BLOCKING'}))
  {
    _process_packet($self, $packet, $ret) || return(1);
  }

  return(1);
}

sub _process_packet
{
  my($self, $packet, $ret, $blocking_task) = @_;

  my($data_ref, $type, $use_handlers);

  $use_handlers = !($blocking_task && !$self->{'HANDLERS_WHEN_BLOCKING'});

  $type = $packet->{'TYPE'};

  if($ret == HTLC_EWOULDBLOCK) # Idle event
  {
    if(defined($self->{'HANDLERS'}->{'EVENT'}))
    {
      &{$self->{'HANDLERS'}->{'EVENT'}}($self, 1);
    }

    select(undef, undef, undef, $self->{'EVENT_TIMING'});
    return(1);
  }

  $self->{'LAST_ACTIVITY'} = time();

  if(defined($self->{'HANDLERS'}->{'EVENT'})) # Non-idle event
  {
    &{$self->{'HANDLERS'}->{'EVENT'}}($self, 0);
  }

  _debug("Packet type = $type\n");

  if($type == HTLS_HDR_USER_LEAVE)
  {
    # Hotline server *BUG* - you may get a "disconnect" packet for a
    # socket _before_ you get the "connect" packet for that socket!
    # In fact, the "connect" packet will never arrive in this case.

    if(defined($packet->{'SOCKET'}) &&
       defined($self->{'USER_LIST'}->{$packet->{'SOCKET'}}))
    {
      my($user) = $self->{'USER_LIST'}->{$packet->{'SOCKET'}};

      delete $self->{'USER_LIST'}->{$packet->{'SOCKET'}};

      if($use_handlers)
      {
        if(defined($self->{'HANDLERS'}->{'LEAVE'}))
        {
          &{$self->{'HANDLERS'}->{'LEAVE'}}($self, $user);
        }
        elsif($self->{'DEFAULT_HANDLERS'})
        {       
          print "USER LEFT: ", $user->nick(), "\n";
        }
      }
    }
  }
  elsif($type == HTLS_HDR_TASK)
  {
    my($task) = $self->{'TASKS'}->{$packet->{'TASK_NUM'}};

    my($task_type) = $task->type();

    $task->finish(time());

    if(defined($packet->{'TASK_ERROR'}))
    {
      $task->error(1);
      $task->error_text($packet->{'TASK_ERROR'});

      if($use_handlers)
      {
        if(defined($self->{'HANDLERS'}->{'TASK_ERROR'}))
        {
          &{$self->{'HANDLERS'}->{'TASK_ERROR'}}($self, $task);
        }
        else
        {
          print "TASK ERROR(", $task->num(), ':', $task->type(), ") ",
                $task->error_text(), "\n";
        }
      }
    }
    else
    {
      $task->error(0);

      if($task_type == HTLC_TASK_USER_LIST && defined($packet->{'USER_LIST'}))
      {
        $self->{'USER_LIST'} = $packet->{'USER_LIST'};

        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'USER_LIST'}))
          {
            &{$self->{'HANDLERS'}->{'USER_LIST'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "GET USER LIST: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_FILE_LIST)
      {
        my($path);

        $task->path("")  unless(length($task->path()));
        $path = $task->path();

        if($packet->{'FILE_LIST'})
        {
          $self->{'FILES'}->{$path} = $packet->{'FILE_LIST'};
        }
        else
        {
          $self->{'FILES'}->{$path} = [];
        }

        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'FILE_LIST'}))
          {
            &{$self->{'HANDLERS'}->{'FILE_LIST'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "GET FILE LIST: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_NEWS && defined($packet->{'DATA'}))
      {
        my(@news) = split(/_{58}/, $packet->{'DATA'});

        $self->{'NEWS'} = \@news;

        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'NEWS'}))
          {
            &{$self->{'HANDLERS'}->{'NEWS'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "GET NEWS: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_USER_INFO && defined($packet->{'DATA'}))
      {
        my($user) = $self->{'USER_LIST'}->{$task->socket()};

        $user->info($packet->{'DATA'});

        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'USER_GETINFO'}))
          {
            &{$self->{'HANDLERS'}->{'USER_GETINFO'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "GET USER INFO: Task complete.\n";
          }
        }

        _debug("USER_GETINFO for: $packet->{'NICK'} (", $task->socket(), ")\n",
               $packet->{'DATA'}, "\n");
      }
      elsif($task_type == HTLC_TASK_FILE_INFO)
      {
        my($path, $file_info);

        $task->path("")  unless(length($task->path));
        $path = $task->path();

        $file_info = $self->{'FILE_INFO'} = new Net::Hotline::FileInfoItem();

        $file_info->icon($packet->{'FILE_ICON'});
        $file_info->type($packet->{'FILE_TYPE'});
        $file_info->creator($packet->{'FILE_CREATOR'});
        $file_info->size($packet->{'FILE_SIZE'});
        $file_info->name($packet->{'FILE_NAME'});
        $file_info->comment($packet->{'FILE_COMMENT'});
        $file_info->ctime($packet->{'FILE_CTIME'});
        $file_info->mtime($packet->{'FILE_MTIME'});

        if($use_handlers)
        {      
          if(defined($self->{'HANDLERS'}->{'FILE_GET_INFO'}))
          {
            &{$self->{'HANDLERS'}->{'FILE_GET_INFO'}}($self, $task, $file_info);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "FILE_GET_INFO: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_LOGIN)
      {
        $self->{'LOGGED_IN'} = 1;

        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'LOGIN'}))
          {
            &{$self->{'HANDLERS'}->{'LOGIN'}}($self);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "LOGIN: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_NEWS_POST)
      {
        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'NEWS_POST'}))
          {
            &{$self->{'HANDLERS'}->{'NEWS_POST'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "POST NEWS: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_SEND_MSG)
      {
        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'SEND_MSG'}))
          {
            &{$self->{'HANDLERS'}->{'SEND_MSG'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "SEND MSG: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_KICK)
      {
        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'KICK'}))
          {
            &{$self->{'HANDLERS'}->{'KICK'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "KICK: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_BAN)
      {
        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'BAN'}))
          {
            &{$self->{'HANDLERS'}->{'BAN'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "BAN: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_SET_INFO)
      {
        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'FILE_SET_INFO'}))
          {
            &{$self->{'HANDLERS'}->{'FILE_SET_INFO'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "SET INFO: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_FILE_DELETE)
      {
        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'FILE_DELETE'}))
          {
            &{$self->{'HANDLERS'}->{'FILE_DELETE'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "DELETE FILE: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_FILE_MKDIR)
      {
        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'FILE_MKDIR'}))
          {
            &{$self->{'HANDLERS'}->{'FILE_MKDIR'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "CREATE FOLDER: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_FILE_MOVE)
      {
        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'FILE_MOVE'}))
          {
            &{$self->{'HANDLERS'}->{'FILE_MOVE'}}($self, $task);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "MOVE FILE: Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_FILE_GET)
      {
        my($size) = $packet->{'HTXF_SIZE'};
        my($ref)  = $packet->{'HTXF_REF'};

        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'FILE_GET'}))
          {
            &{$self->{'HANDLERS'}->{'FILE_GET'}}($self, $task, $ref, $size);
          }
          else
          {
            print "GET FILE: Starting download (ref = $ref, size = $size)\n"
              if($self->{'DEFAULT_HANDLERS'});

            $self->recv_file($task, $ref, $size);
          }
        }
      }
      elsif($task_type == HTLC_TASK_FILE_PUT)
      {
        my($ref)    = $packet->{'HTXF_REF'};
        my($resume) = $packet->{'HTXF_RFLT'};
        my($size)   = ${$task->misc()}[0] + ${$task->misc()}[1];

        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'FILE_PUT'}))
          {
            &{$self->{'HANDLERS'}->{'FILE_PUT'}}($self, $task, $ref, $size, $resume);
          }
          else
          {
            print "GET PUT: Starting upload (ref = $ref)\n"
              if($self->{'DEFAULT_HANDLERS'});

            $self->send_file($task, $ref, $size, $resume);
          }
        }
      }
      elsif($task_type == HTLC_TASK_PCHAT_CREATE)
      {
        my($ref)    = $packet->{'PCHAT_REF'};
        my($user)   = $self->{'USER_LIST'}->{$packet->{'SOCKET'}};
        my($pchat)  = $self->{'PCHATS'}->{$ref} = new Net::Hotline::PrivateChat;

        $pchat->reference($ref);
        $pchat->userlist({ $packet->{'SOCKET'} => $user });

        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'PCHAT_CREATE'}))
          {
            &{$self->{'HANDLERS'}->{'PCHAT_CREATE'}}($self, $task, $pchat);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "CREATE PCHAT($ref): Task complete.\n";
          }
        }
      }
      elsif($task_type == HTLC_TASK_PCHAT_ACCEPT)
      {
        my($ref) = $task->misc();

        my($userlist);
        
        # Create userlist of references to the main userlist rather
        # than new user objects (as returned in the packet)
        foreach my $socket (keys(%{$packet->{'USER_LIST'}}))
        {
          $userlist->{$socket} = $self->{'USER_LIST'}->{$socket};
        }

        my($pchat)  = $self->{'PCHATS'}->{$ref} =
          new Net::Hotline::PrivateChat($ref, $userlist);
        
        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'PCHAT_ACCEPT'}))
          {
            &{$self->{'HANDLERS'}->{'PCHAT_ACCEPT'}}($self, $task, $pchat);
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "ACCEPT PCHAT INVITE($ref): Task complete.\n";
          }
        }
      }
    }
    # Reclaim memory
    delete $self->{'TASKS'}->{$packet->{'TASK_NUM'}};
  }
  elsif($type == HTLS_HDR_AGREEMENT)
  {
    $self->{'AGREEMENT'} = $packet->{'DATA'};

    if(defined($packet->{'DATA'}))
    {
      if($use_handlers)
      {
        if(defined($self->{'HANDLERS'}->{'AGREEMENT'}))
        {
          &{$self->{'HANDLERS'}->{'AGREEMENT'}}($self, \$packet->{'DATA'});
        }
        elsif($self->{'DEFAULT_HANDLERS'})
        {
          print "AGREEMENT:\n", $packet->{'DATA'}, "\n";
        }
      }
    }
  }
  elsif($type == HTLS_HDR_MSG)
  {
    my($user) = $self->{'USER_LIST'}->{$packet->{'SOCKET'}};

    # User-to-user message
    if(defined($user) && defined($packet->{'DATA'}))
    {
      if($use_handlers)
      {
        if(defined($self->{'HANDLERS'}->{'MSG'}))
        {
          &{$self->{'HANDLERS'}->{'MSG'}}($self, $user, \$packet->{'DATA'}, \$packet->{'REPLY_TO'});
        }
        elsif($self->{'DEFAULT_HANDLERS'})
        {
          print "MSG: ", $user->nick(), "(", 
                         $packet->{'SOCKET'}, ") ", 
                         $packet->{'DATA'};

          if($packet->{'IS_REPLY'})
          {
            print " (In reply to: $packet->{'REPLY_TO'}])";
          }

          print "\n";
        }
      }
    }
    elsif(defined($packet->{'DATA'})) # Server message
    {
      if($use_handlers)
      {
        if(defined($self->{'HANDLERS'}->{'SERVER_MSG'}))
        {
          &{$self->{'HANDLERS'}->{'SERVER_MSG'}}($self, \$packet->{'DATA'});
        }
        elsif($self->{'DEFAULT_HANDLERS'})
        {
          print "SERVER MSG: ", $packet->{'DATA'}, "\n";
        }
      }
    }
  }
  elsif($type == HTLS_HDR_USER_CHANGE)
  {
    if(defined($packet->{'NICK'}) && defined($packet->{'SOCKET'}) &&
       defined($packet->{'ICON'}) && defined($packet->{'COLOR'}))
    {
      if(defined($self->{'USER_LIST'}->{$packet->{'SOCKET'}}))
      {
        my($user) = $self->{'USER_LIST'}->{$packet->{'SOCKET'}};

        if($user->nick() ne $packet->{'NICK'})
        {
          my($old_nick) = $user->nick();

          $user->nick($packet->{'NICK'});

          if($use_handlers)
          {
            if(defined($self->{'HANDLERS'}->{'NICK'}))
            {
              &{$self->{'HANDLERS'}->{'NICK'}}($self, $user, $old_nick, $user->nick());
            }
            elsif($self->{'DEFAULT_HANDLERS'})
            {
              print "USER CHANGE: $old_nick is now known as ", $user->nick(), "\n";
            }
          }
        }
        elsif($user->icon() ne $packet->{'ICON'})
        {
          my($old_icon) = $user->icon();

          $user->icon($packet->{'ICON'});

          if($use_handlers)
          {
            if(defined($self->{'HANDLERS'}->{'ICON'}))
            {
              &{$self->{'HANDLERS'}->{'ICON'}}($self, $user, $old_icon, $user->icon());
            }
            elsif($self->{'DEFAULT_HANDLERS'})
            {
              print "USER CHANGE: ", $user->nick(),
                    " icon changed from $old_icon to ",
                    $user->icon(), "\n";
            }
          }
        }
        elsif($user->color() ne $packet->{'COLOR'})
        {
          my($old_color) = $user->color();

          $user->color($packet->{'COLOR'});

          if($use_handlers)
          {
            if(defined($self->{'HANDLERS'}->{'COLOR'}))
            {
              &{$self->{'HANDLERS'}->{'COLOR'}}($self, $user, $old_color, $user->color());
            }
            elsif($self->{'DEFAULT_HANDLERS'})
            {
              print "USER CHANGE: ", $user->nick(),
                    " color changed from $old_color to ",
                    $user->color(), "\n";
            }
          }
        }
      }
      else
      {
        $self->{'USER_LIST'}->{$packet->{'SOCKET'}} =
          new Net::Hotline::User($packet->{'SOCKET'},
                            $packet->{'NICK'},
                            undef,
                            $packet->{'ICON'},
                            $packet->{'COLOR'});

        if($use_handlers)
        {
          if(defined($self->{'HANDLERS'}->{'JOIN'}))
          {
            &{$self->{'HANDLERS'}->{'JOIN'}}($self, $self->{'USER_LIST'}->{$packet->{'SOCKET'}});
          }
          elsif($self->{'DEFAULT_HANDLERS'})
          {
            print "JOINED:\n",
                  "  Nick: $packet->{'NICK'}\n",
                  "  Icon: $packet->{'ICON'}\n",
                  "Socket: $packet->{'SOCKET'}\n",
                  " Color: $packet->{'COLOR'}\n";
          }
        }
      }
    }
  }
  elsif($type == HTLS_HDR_CHAT)
  {
    if(defined($packet->{'DATA'}))
    {
      $packet->{'DATA'} =~ s/^\n//s;

      my($ref) = $packet->{'PCHAT_REF'};

      if($ref) # Priate chat
      {
        # Private chat "action"
        if($packet->{'DATA'} =~ /^ \*\*\* /)
        {
          if($use_handlers)
          {
            if(defined($self->{'HANDLERS'}->{'PCHAT_ACTION'}))
            {
              &{$self->{'HANDLERS'}->{'PCHAT_ACTION'}}($self, $ref, \$packet->{'DATA'});
            }
            elsif($self->{'DEFAULT_HANDLERS'})
            {
              print "PCHAT($ref) ACTION: ", $packet->{'DATA'}, "\n";
            }
          }
        }
        else # Regular private chat
        {
          if($use_handlers)
          {
            if(defined($self->{'HANDLERS'}->{'PCHAT_CHAT'}))
            {
              &{$self->{'HANDLERS'}->{'PCHAT_CHAT'}}($self, $ref, \$packet->{'DATA'});
            }
            elsif($self->{'DEFAULT_HANDLERS'})
            {
              print "PCHAT($ref): ", $packet->{'DATA'}, "\n";
            }
          }
        }
      }
      else # Regular chat
      {
        # Chat "action"
        if($packet->{'DATA'} =~ /^ \*\*\* /)
        {
          if($use_handlers)
          {
            if(defined($self->{'HANDLERS'}->{'CHAT_ACTION'}))
            {
              &{$self->{'HANDLERS'}->{'CHAT_ACTION'}}($self, \$packet->{'DATA'});
            }
            elsif($self->{'DEFAULT_HANDLERS'})
            {
              print "CHAT ACTION: ", $packet->{'DATA'}, "\n";
            }
          }
        }
        else # Regular chat
        {
          if($use_handlers)
          {
            if(defined($self->{'HANDLERS'}->{'CHAT'}))
            {
              &{$self->{'HANDLERS'}->{'CHAT'}}($self, \$packet->{'DATA'});
            }
            elsif($self->{'DEFAULT_HANDLERS'})
            {
              print "CHAT: ", $packet->{'DATA'}, "\n";
            }
          }
        }
      }
    }
  }
  elsif($type == HTLS_HDR_NEWS_POST)
  {
    my($post) = $packet->{'DATA'};

    if(defined($post))
    {
      $post =~ s/@{[HTLC_NEWLINE]}/\n/osg;
      $post =~ s/_{58}//sg;

      if($use_handlers)
      {
        if(defined($self->{'HANDLERS'}->{'NEWS_POSTED'}))
        {
          &{$self->{'HANDLERS'}->{'NEWS_POSTED'}}($self, \$post);
        }
        elsif($self->{'DEFAULT_HANDLERS'})
        {
          print "NEWS: New post made.\n";
        }
      }
    }
  }
  elsif($type == HTLS_HDR_POLITE_QUIT ||
        $type eq 'DISCONNECTED')
  {
    if(defined($packet->{'DATA'}))
    {
      if($use_handlers)
      {
        if(defined($self->{'HANDLERS'}->{'QUIT'}))
        {
          &{$self->{'HANDLERS'}->{'QUIT'}}($self, \$packet->{'DATA'});
        }
        elsif($self->{'DEFAULT_HANDLERS'})
        {
          print "CONNECTION CLOSED: ", $packet->{'DATA'}, "\n";
        }
      }
    }
    elsif($self->{'DEFAULT_HANDLERS'})
    {
      if($use_handlers)
      {
        print "CONNECTION CLOSED\n";
      }
    }

    $self->disconnect();
    return(0);
  }
  elsif($type == HTLS_HDR_PCHAT_INVITE)
  {
    if($use_handlers)
    {
      if(defined($self->{'HANDLERS'}->{'PCHAT_INVITE'}))
      {
        &{$self->{'HANDLERS'}->{'PCHAT_INVITE'}}($self, $packet->{'PCHAT_REF'},
                                                 $packet->{'SOCKET'},
                                                 $packet->{'NICK'});
      }
      elsif($self->{'DEFAULT_HANDLERS'})
      {
        print "PCHAT INVITE($packet->{'PCHAT_REF'}) from $packet->{'NICK'}($packet->{'SOCKET'})",
              "($packet->{'SOCKET)'})\n";
      }
    }
  }
  elsif($type == HTLS_HDR_PCHAT_USER_JOIN)
  {
    my($ref)    = $packet->{'PCHAT_REF'};
    my($socket) = $packet->{'SOCKET'};
    my($pchat)  = $self->{'PCHATS'}->{$ref};

    $pchat->userlist()->{$socket} = $self->{'USER_LIST'}->{$socket};

    if($use_handlers)
    {
      if(defined($self->{'HANDLERS'}->{'PCHAT_JOIN'}))
      {
        &{$self->{'HANDLERS'}->{'PCHAT_JOIN'}}($self, $pchat, $socket);
      }
      elsif($self->{'DEFAULT_HANDLERS'})
      {
        print "PCHAT($ref)  JOIN($socket)\n";
      }
    }
  }
  elsif($type == HTLS_HDR_PCHAT_USER_LEAVE)
  {
    my($ref)    = $packet->{'PCHAT_REF'};
    my($socket) = $packet->{'SOCKET'};
    my($pchat)  = $self->{'PCHATS'}->{$ref};

    delete $pchat->userlist()->{$socket};

    if($use_handlers)
    {
      if(defined($self->{'HANDLERS'}->{'PCHAT_LEAVE'}))
      {
        &{$self->{'HANDLERS'}->{'PCHAT_LEAVE'}}($self, $pchat, $socket);
      }
      elsif($self->{'DEFAULT_HANDLERS'})
      {
        print "PCHAT($ref)  LEAVE($socket)\n";
      }
    }
  }
  elsif($type == HTLS_HDR_PCHAT_SUBJECT)
  {
    my($pchat) = $self->{'PCHATS'}->{$packet->{'PCHAT_REF'}};
    
    $pchat->subject($packet->{'DATA'});

    if($use_handlers)
    {
      if(defined($self->{'HANDLERS'}->{'PCHAT_SUBJECT'}))
      {
        &{$self->{'HANDLERS'}->{'PCHAT_SUBJECT'}}($self, $pchat, \$packet->{'DATA'});
      }
      elsif($self->{'DEFAULT_HANDLERS'})
      {
        print "PCHAT(", $pchat->reference(), ") Subject set to: $packet->{'DATA'}\n";
      }
    }
  }

  return(1);
}

sub _handler
{
  my($self, $code_ref, $type) = @_;

  if(defined($code_ref))
  {
    if(ref($code_ref) eq 'CODE')
    {
      $self->{'HANDLERS'}->{$type} = $code_ref;
    }
  }

  return $self->{'HANDLERS'}->{$type};
}

sub _next_seqnum
{
  my($self) = shift;

  return $self->{'SEQNUM'}++;
}

sub agreement_handler     { return _handler($_[0], $_[1], 'AGREEMENT')     }
sub ban_handler           { return _handler($_[0], $_[1], 'BAN')           }
sub chat_handler          { return _handler($_[0], $_[1], 'CHAT')          }
sub chat_action_handler   { return _handler($_[0], $_[1], 'CHAT_ACTION')   }
sub color_handler         { return _handler($_[0], $_[1], 'COLOR')         }
sub event_loop_handler    { return _handler($_[0], $_[1], 'EVENT')         }
sub delete_file_handler   { return _handler($_[0], $_[1], 'FILE_DELETE')   }
sub get_file_handler      { return _handler($_[0], $_[1], 'FILE_GET')      }
sub put_file_handler      { return _handler($_[0], $_[1], 'FILE_PUT')      }
sub file_info_handler     { return _handler($_[0], $_[1], 'FILE_GET_INFO') }
sub file_list_handler     { return _handler($_[0], $_[1], 'FILE_LIST')     }
sub new_folder_handler    { return _handler($_[0], $_[1], 'FILE_MKDIR')    }
sub move_file_handler     { return _handler($_[0], $_[1], 'FILE_MOVE')     }
sub set_file_info_handler { return _handler($_[0], $_[1], 'FILE_SET_INFO') }
sub icon_handler          { return _handler($_[0], $_[1], 'ICON')          }
sub join_handler          { return _handler($_[0], $_[1], 'JOIN')          }
sub kick_handler          { return _handler($_[0], $_[1], 'KICK')          }
sub leave_handler         { return _handler($_[0], $_[1], 'LEAVE')         }
sub login_handler         { return _handler($_[0], $_[1], 'LOGIN')         }
sub msg_handler           { return _handler($_[0], $_[1], 'MSG')           }
sub news_handler          { return _handler($_[0], $_[1], 'NEWS')          }
sub post_news_handler     { return _handler($_[0], $_[1], 'NEWS_POST')     }
sub news_posted_handler   { return _handler($_[0], $_[1], 'NEWS_POSTED')   }
sub nick_handler          { return _handler($_[0], $_[1], 'NICK')          }
sub pchat_accept_handler  { return _handler($_[0], $_[1], 'PCHAT_ACCEPT')  }
sub pchat_action_handler  { return _handler($_[0], $_[1], 'PCHAT_ACTION')  }
sub pchat_chat_handler    { return _handler($_[0], $_[1], 'PCHAT_CHAT')    }
sub pchat_create_handler  { return _handler($_[0], $_[1], 'PCHAT_CREATE')  }
sub pchat_invite_handler  { return _handler($_[0], $_[1], 'PCHAT_INVITE')  }
sub pchat_join_handler    { return _handler($_[0], $_[1], 'PCHAT_JOIN')    }
sub pchat_leave_handler   { return _handler($_[0], $_[1], 'PCHAT_LEAVE')   }
sub pchat_subject_handler { return _handler($_[0], $_[1], 'PCHAT_SUBJECT') }
sub quit_handler          { return _handler($_[0], $_[1], 'QUIT')          }
sub send_msg_handler      { return _handler($_[0], $_[1], 'SEND_MSG')      }
sub server_msg_handler    { return _handler($_[0], $_[1], 'SERVER_MSG')    }
sub task_error_handler    { return _handler($_[0], $_[1], 'TASK_ERROR')    }
sub user_info_handler     { return _handler($_[0], $_[1], 'USER_GETINFO')  }
sub user_list_handler     { return _handler($_[0], $_[1], 'USER_LIST')     }

#
# Package subroutines
#

sub version { $Net::Hotline::Client::VERSION }

sub debug
{ 
  if(@_ == 1 && !ref($_[0]))
  {
    $Net::Hotline::Client::DEBUG = ($_[0]) ? 1 : 0;
  }
  elsif(@_ == 2 && ref($_[0]) eq 'Net::Hotline::Client')
  {
    $Net::Hotline::Client::DEBUG = ($_[1]) ? 1 : 0;
  }

  return $Net::Hotline::Client::DEBUG;
}

sub _hlc_write
{
  my($self, $fh, $data_ref, $len) = @_;

  return("0-E0")  if($len == 0 || !defined($len));
  
  unless(_write($fh, $data_ref, $len) == $len)
  {
    $self->{'LAST_ERROR'} = "Write error: $!";
    return;
  }

  return($len);
}

sub _hlc_read
{
  my($self, $fh, $data_ref, $len) = @_;

  return("0-E0")  if($len == 0 || !defined($len));

  unless(_read($fh, $data_ref, $len) == $len)
  {
    $self->{'LAST_ERROR'} = "Read error: $!";
    return;
  }

  return($len);
}

sub _hlc_buffered_read
{
  my($self, $fh, $data_ref, $len) = @_;

  return("0-E0")  if($len == 0 || !defined($len));

  unless(read($fh, $$data_ref, $len) == $len)
  {
    $self->{'LAST_ERROR'} = "Read error: $!";
    return;
  }

  return($len);
}

# Macbinary CRC perl code from Convert::BinHex by Eryq (eryq@enteract.com)
# (It needs access to the lexical @CRC_MAGIC, so it can't be auto-loaded)
sub macbin_crc
{
  shift if(ref($_[0]));

  my($len) = length($_[0]);
  my($crc) = $_[1];

  for(my $i = 0; $i < $len; $i++)
  {
    ($crc ^= (vec($_[0], $i, 8) << 8)) &= 0xFFFF;
    $crc = ($crc << 8) ^ $CRC_MAGIC[$crc >> 8];
  }
  return $crc;
}

#
# Satisfy autoloader's ridiculous *8-character* unique name limit :-/
#

sub get_filelist { al01_get_filelist(@_) }
sub get_fileinfo { al02_get_fileinfo(@_) }
sub get_userinfo { al03_get_userinfo(@_) }
sub user_by_nick { al04_user_by_nick(@_) }
sub req_userlist { al05_req_userlist(@_) }
sub req_filelist { al06_req_filelist(@_) }
sub pchat_action { al07_pchat_action(@_) }
sub get_file     { al08_get_file(@_)     }
sub put_file     { al09_put_file(@_)     }

# Internal functions that were also munged up:

# _al01_put_file_resume_now
# _al02_get_file_resume_now
# _al03_delete_file_now
# _al04_new_folder_now
# _al05_put_file_now
# _al06_put_file_resume
# _al07_get_file_now
# _al08_get_file_resume
# _al09_file_action_stub
# _al10_post_news_now
# _al11_pchat_invite_now
# _al12_pchat_accept_now
# _al13_comment_now

__END__

#
# Auto-loaded methods and subroutines
#

sub logged_in { $_[0]->{'LOGGED_IN'} }

sub connected
{
  (ref($_[0]->{'SERVER'}) && $_[0]->{'SERVER'}->opened()) ? 1 : 0;
}

sub _blocking_task
{
  my($self, $task_num) = @_;

  my($packet, $ret);

  $packet = new Net::Hotline::Protocol::Packet;

  while($ret = $packet->read_parse($self->{'SERVER'}, $self->{'BLOCKING'}))
  {
    _process_packet($self, $packet, $ret, 'blocking task');

    if($packet->{'TYPE'} == HTLS_HDR_TASK &&
       $packet->{'TASK_NUM'} == $task_num)
    {
      return($packet);
    }
  }
}

sub al01_get_filelist
{
  my($self, $path) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->req_filelist($path);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return(0);
  }

  $path = $task->path();
  $path = ""  unless(length($path));

  if(wantarray)
  {
    return @{$self->{'FILES'}->{$path}};
  }
  else
  {
    return $self->{'FILES'}->{$path};
  }
}

sub al06_req_filelist
{
  my($self, $path) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($data, $task_num, @path_parts, $path_part, $data_length, $length,
     $save_path);

  $path =~ s/^$self->{'PATH_SEPARATOR'}//;
  $path =~ s/$self->{'PATH_SEPARATOR'}$//;

  if(length($path))
  {
    $save_path = $path;
    @path_parts = split($self->{'PATH_SEPARATOR'}, $path);
    $path =~ s/$self->{'PATH_SEPARATOR'}//g;

    if(length($path) > HTLC_MAX_PATHLEN)
    {
      croak("Maximum path length exceeded");
    }

    # 2 null bytes, the 1 byte for length, and the length of the path part
    $data_length = (3 * scalar(@path_parts)) + length($path);
    $length = SIZEOF_HL_LONG_HDR + $data_length;
  }
  else
  {
    $length = 2; # Two null bytes
  }

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_FILE_LIST);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len($length);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header();

  if(length($path))
  {
    $data .= pack("n4", 0x0001,               # Number of atoms
                        HTLC_DATA_DIRECTORY,  # Atom type
                        $data_length + 2,     # Atom length

                        scalar(@path_parts)); # Number of path parts

    foreach $path_part (@path_parts)          # Path parts data
    {
      if(length($path_part) > HTLC_MAX_PATHLEN)
      {
        croak("Maximum path part length exceeded");
      }

      $data .= pack("nCa*", 0x0000,           # 2 null bytes
                            length $path_part,# Length
                            $path_part);      # Path part
    }
  }
  else
  {
    $data .= pack("n", 0x0000);
  }

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: FILE_LIST - $task_num\n");
    $self->{'TASKS'}->{$task_num} = 
      new Net::Hotline::Task($task_num, HTLC_TASK_FILE_LIST, time(), undef, $save_path);
    return($task_num);
  }
  else { return }
}

sub al03_get_userinfo
{
  my($self, $socket) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->req_userinfo($socket);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return $self->{'USER_LIST'}->{$task->socket()}->info();
}

sub req_userinfo
{
  my($self, $socket) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($data, $task_num);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_USER_GETINFO);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(SIZEOF_HL_LONG_HDR);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n4", 0x0001,                  # Number of atoms

                     HTLC_DATA_SOCKET,        # Atom type
                     0x0002,                  # Atom length
                     $socket);                # Atom data

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: USER_GETINFO - $task_num\n");
    $self->{'TASKS'}->{$task_num} = 
      new Net::Hotline::Task($task_num, HTLC_TASK_USER_INFO, time(), $socket);
    return($task_num);
  }
  else { return }
}

sub al02_get_fileinfo
{
  my($self, $path) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->req_fileinfo($path);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return $self->{'FILE_INFO'};
}

sub req_fileinfo
{
  return _file_action_simple($_[0], $_[1], HTLC_HDR_FILE_GETINFO, HTLC_TASK_FILE_INFO, 'GET FILE INFO');
}

sub delete_file
{
  my($self, $path) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al03_delete_file_now($path); 
  }
  else
  {
    return $self->_delete_file($path); 
  }
}

sub _al03_delete_file_now
{
  my($self, $path) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_delete_file($path);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _delete_file
{
  return _file_action_simple($_[0], $_[1], HTLC_HDR_FILE_DELETE, HTLC_TASK_FILE_DELETE, 'DELETE FILE');
}

sub new_folder
{
  my($self, $path) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al04_new_folder_now($path); 
  }
  else
  {
    return $self->_new_folder($path); 
  }
}

sub _al04_new_folder_now
{
  my($self, $path) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_new_folder($path);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _new_folder
{
  return _file_action_simple($_[0], $_[1], HTLC_HDR_FILE_MKDIR, HTLC_TASK_FILE_MKDIR, 'NEW FOLDER');
}

sub al09_put_file
{
  my($self, $src_path, $dest_path, $comments) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al05_put_file_now($src_path, $dest_path, $comments); 
  }
  else
  {
    return $self->_put_file($src_path, $dest_path, $comments);
  }
}

sub _al05_put_file_now
{
  my($self, $src_path, $dest_path, $comments) = @_;

  my($task, $task_num, $packet, $size);

  $task_num = $self->_put_file($src_path, $dest_path);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  $size = ${$task->misc()}[0] + ${$task->misc()}[1];

  if(wantarray)
  {
    return($task, $packet->{'HTXF_REF'}, $size);
  }
  else
  {
    return [ $task, $packet->{'HTXF_REF'}, $size ];
  }
}

sub _put_file
{
  my($self, $src_path, $dest_path, $comments) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  croak("Not connected.")  unless($server->opened());

  unless(-e $src_path)
  {
    $self->{'LAST_ERROR'} = "File does not exist: $src_path";
    return;
  }

  my($local_sep, $remote_sep, $src_file, $data, $task_num, $length,
     $num_atoms, $data_len, $rsrc_len, $finder_flags, $type, $creator);

  $local_sep = PATH_SEPARATOR;
  $remote_sep = $self->{'PATH_SEPARATOR'};

  ($src_file = $src_path) =~ s/.*?$local_sep([^$local_sep]+)$/$1/o;
  $dest_path = "$dest_path$remote_sep$src_file";

  ($data, $task_num) = _al09_file_action_stub($self, $dest_path, HTLC_HDR_FILE_PUT);

  # Set new length: old length plus 8 bytes for the size atom
  $length    = (unpack("N", substr($data, 16, 4)) + 8);
  substr($data, 16, 4) = pack("N", $length);
  substr($data, 12, 4) = pack("N", $length);

  # Set new num atoms: old num atoms + 1
  $num_atoms = (unpack("n", substr($data, 20, 2)) + 1);
  substr($data, 20, 2) = pack("n", $num_atoms);

  # Fork lengths
  $data_len = (stat($src_path))[7];
  $rsrc_len = 0;

  # Mac OS specific information: resource fork length and finder comments
  if($self->{'MACOS'})
  {
    my($fsspec, $finder_comments, $res_fd, $rsrc_fh, $cat, $finfo);

    $fsspec = MacPerl::MakeFSSpec($src_path);

    # Get finder comments
    unless(defined($comments))
    {
      $finder_comments = Mac::MoreFiles::FSpDTGetComment($fsspec);
      $comments = $finder_comments  if(length($finder_comments));
    }

    $cat   = Mac::Files::FSpGetCatInfo($fsspec);
    $finfo = $cat->ioFlFndrInfo();

    # Get finder flags, type, and creator
    $finder_flags = $finfo->fdFlags();
    $type         = $finfo->fdType();
    $creator      = $finfo->fdCreator();

    # Protect from compile-time errors on non-Mac OS systems that don't
    # define O_RSRC in Fcntl
    eval '$res_fd = POSIX::open($src_path, O_RDONLY | O_RSRC)';

    $rsrc_fh = new IO::File;

    unless($rsrc_fh->fdopen($res_fd, "r"))
    {
      $self->{'LAST_ERROR'} = "Couldn't open Mac resource fork: $@";
      return;    
    }

    $rsrc_fh->seek(0, SEEK_END);    # Fast forward to end
    $rsrc_len = $rsrc_fh->tell();   # Get size
    $rsrc_fh->seek(0, SEEK_SET);    # Rewind
  }
  else
  {
    ($type, $creator) = ("BINA", "????");
  }

  # Total length of the upload to come: 111 bytes for type/creator/etc.
  # + 1 byte for the file name length + the file name + 2 bytes for the
  # comments length + the comments + 2 fork headers + the size of the
  # file to be uploaded (size of data fork plus size of resource fork).
  $length = (SIZEOF_HL_FILE_UPLOAD_HDR + 1 + length($src_file) + 2 +
             length($comments) + (2 * SIZEOF_HL_FILE_FORK_HDR) +
             $data_len + $rsrc_len);

  # 00 00 00 CB  00 00 00 06  00 00 00 00  00 00 00 21  ...............!
  # 00 00 00 21  00 03 00 C9  00 05 74 65  78 74 32 00  ...!......text2.
  # CA 00 0C 00  01 00 00 07  55 70 6C 6F  61 64 73 00  ........Uploads.
  # 6C 00 02 03  94                                     l....

  # Add size argument
  $data .= pack("nnN", HTLC_DATA_HTXF_SIZE,   # Atom type
                       0x0004,                # Atom length
                       $length);              # Atom data

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: PUT FILE - $task_num\n");
    $self->{'TASKS'}->{$task_num} = 
      new Net::Hotline::Task($task_num, HTLC_TASK_FILE_PUT, time(), undef,
                             [ $src_path, $dest_path ],
                             [ $data_len, $rsrc_len, $comments, $finder_flags,
                               $type, $creator, $length ]);
    return($task_num);
  }
  else { return }
}

sub put_file_resume
{
  my($self, $src_path, $dest_path, $comments) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al01_put_file_resume_now($src_path, $dest_path, $comments); 
  }
  else
  {
    return $self->_al06_put_file_resume($src_path, $dest_path, $comments); 
  }
}

sub _al01_put_file_resume_now
{
  my($self, $src_path, $dest_path, $comments) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_al06_put_file_resume($src_path, $dest_path, $comments);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  if(wantarray)
  {
    return($task, $packet->{'HTXF_REF'}, ${$task->misc()}[6], $packet->{'HTXF_RFLT'});
  }
  else
  {
    return [ $task, $packet->{'HTXF_REF'}, ${$task->misc()}[6], $packet->{'HTXF_RFLT'} ];
  }
}

sub _al06_put_file_resume
{
  my($self, $src_path, $dest_path, $comments) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  croak("Not connected.")  unless($server->opened());

  unless(-e $src_path)
  {
    $self->{'LAST_ERROR'} = "File does not exist: $src_path";
    return;
  }

  my($local_sep, $remote_sep, $src_file, $data, $task_num, $length,
     $num_atoms, $data_len, $rsrc_len, $finder_flags, $type, $creator);

  $local_sep = PATH_SEPARATOR;
  $remote_sep = $self->{'PATH_SEPARATOR'};

  ($src_file = $src_path) =~ s/.*?$local_sep([^$local_sep]+)$/$1/o;
  $dest_path = "$dest_path$remote_sep$src_file";

  ($data, $task_num) = _al09_file_action_stub($self, $dest_path, HTLC_HDR_FILE_PUT);

  # Add upload resume magic
  $data .= HTXF_RESUME_MAGIC;

  # Set new length: old length plus the length of HTXF_RESUME_MAGIC
  $length    = (unpack("N", substr($data, 16, 4)) + length(HTXF_RESUME_MAGIC));
  substr($data, 16, 4) = pack("N", $length);
  substr($data, 12, 4) = pack("N", $length);

  # Set new num atoms: old num atoms + 1
  $num_atoms = (unpack("n", substr($data, 20, 2)) + 1);
  substr($data, 20, 2) = pack("n", $num_atoms);

  # Fork lengths
  $data_len = (stat($src_path))[7];
  $rsrc_len = 0;

  # Mac OS specific information: resource fork length and finder comments
  if($self->{'MACOS'})
  {
    my($fsspec, $finder_comments, $res_fd, $rsrc_fh, $cat, $finfo);

    $fsspec = MacPerl::MakeFSSpec($src_path);

    # Get finder comments
    unless(defined($comments))
    {
      $finder_comments = Mac::MoreFiles::FSpDTGetComment($fsspec);
      $comments = $finder_comments  if(length($finder_comments));
    }

    $cat   = Mac::Files::FSpGetCatInfo($fsspec);
    $finfo = $cat->ioFlFndrInfo();

    # Get finder flags, type, and creator
    $finder_flags = $finfo->fdFlags();
    $type         = $finfo->fdType();
    $creator      = $finfo->fdCreator();

    # Protect from compile-time errors on non-Mac OS systems that don't
    # define O_RSRC in Fcntl
    eval '$res_fd = POSIX::open($src_path, O_RDONLY | O_RSRC)';

    $rsrc_fh = new IO::File;

    unless($rsrc_fh->fdopen($res_fd, "r"))
    {
      $self->{'LAST_ERROR'} = "Couldn't open Mac resource fork: $@";
      return;    
    }

    $rsrc_fh->seek(0, SEEK_END);    # Fast forward to end
    $rsrc_len = $rsrc_fh->tell();   # Get size
    $rsrc_fh->seek(0, SEEK_SET);    # Rewind
  }
  else
  {
    ($type, $creator) = ("BINA", "????");
  }

  # Total length of the upload to come: 111 bytes for type/creator/etc.
  # + 1 byte for the file name length + the file name + 2 bytes for the
  # comments length + the comments + 2 fork headers + the size of the
  # file to be uploaded (size of data fork plus size of resource fork).
  $length = (SIZEOF_HL_FILE_UPLOAD_HDR + 1 + length($src_file) + 2 +
             length($comments) + (2 * SIZEOF_HL_FILE_FORK_HDR) +
             $data_len + $rsrc_len);

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: PUT FILE - $task_num\n");
    $self->{'TASKS'}->{$task_num} = 
      new Net::Hotline::Task($task_num, HTLC_TASK_FILE_PUT, time(), undef,
                             [ $src_path, $dest_path ],
                             [ $data_len, $rsrc_len, $comments, $finder_flags,
                               $type, $creator, $length ]);
    return($task_num);
  }
  else { return }
}

sub al08_get_file
{
  my($self, $path) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al07_get_file_now($path); 
  }
  else
  {
    return $self->_get_file($path); 
  }
}

sub _al07_get_file_now
{
  my($self, $path) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_get_file($path);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  if(wantarray)
  {
    return(($task, $packet->{'HTXF_REF'},  $packet->{'HTXF_SIZE'}));
  }
  else
  {
    return [ $task, $packet->{'HTXF_REF'},  $packet->{'HTXF_SIZE'} ];
  }
}

sub _get_file
{
  my($self, $path) = @_;

  my($local_sep, $remote_sep, $dest_dir, $task_num, $data_file, $rsrc_file);

  $local_sep  = PATH_SEPARATOR;
  $remote_sep = $self->{'PATH_SEPARATOR'};

  $dest_dir = $self->{'DOWNLOADS_DIR'};
  $dest_dir .= $local_sep  if($dest_dir =~ /\S/ && $dest_dir !~ /$local_sep$/o);

  ($data_file = $path) =~ s/.*?$remote_sep([^$remote_sep]+)$/$1/;

  if($self->{'MACOS'})
  {
    $rsrc_file = undef;
  }
  else
  {
    $rsrc_file = "$data_file$self->{'RSRC_FORK_EXT'}";
    $data_file = "$data_file$self->{'DATA_FORK_EXT'}";
  }

  $task_num = _file_action_simple($self, $path, HTLC_HDR_FILE_GET, HTLC_TASK_FILE_GET, 'GET FILE');

  return  unless(defined($task_num));

  $self->{'TASKS'}->{$task_num}->path([ $path, $data_file, $rsrc_file ]);

  return($task_num);
}

sub get_file_resume
{
  my($self, $path) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al02_get_file_resume_now($path); 
  }
  else
  {
    return $self->_al08_get_file_resume($path); 
  }
}

sub _al02_get_file_resume_now
{
  my($self, $path) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_al08_get_file_resume($path);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  if(wantarray)
  {
    return(($task, $packet->{'HTXF_REF'},  $packet->{'HTXF_SIZE'}));
  }
  else
  {
    return [ $task, $packet->{'HTXF_REF'},  $packet->{'HTXF_SIZE'} ];
  }
}

sub _al08_get_file_resume
{
  my($self, $path) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  croak("Not connected.")  unless($server->opened());

  my($local_sep, $remote_sep, $dest_dir, $data, $more_data, $task_num,
     $length, $data_file, $data_pos, $rsrc_file, $rsrc_pos);

  $local_sep  = PATH_SEPARATOR;
  $remote_sep = $self->{'PATH_SEPARATOR'};

  $dest_dir = $self->{'DOWNLOADS_DIR'};
  $dest_dir .= $local_sep  if($dest_dir =~ /\S/ && $dest_dir !~ /$local_sep$/o);

  ($data, $task_num) = _al09_file_action_stub($self, $path, HTLC_HDR_FILE_GET);

  $data_file = $path;
  
  if($data_file =~ /$remote_sep([^$remote_sep]+)$/)
  {
    $data_file = "$dest_dir$1";
  }
  else
  {
    $data_file = "$dest_dir$data_file";
  }

  if($self->{'MACOS'})
  {
    $rsrc_file = undef;
  }
  else
  {
    $rsrc_file = "$data_file$self->{'RSRC_FORK_EXT'}";
    $data_file = "$data_file$self->{'DATA_FORK_EXT'}";
  }

  unless(-e $data_file || -e $rsrc_file)
  {
    $self->{'LAST_ERROR'} = "Can't resume download: partial download does not exist.";
    return;
  }

  # Get data fork position
  $data_pos = (stat($data_file))[7];

  # Get resource fork position
  if($self->{'MACOS'})
  {
    my($res_fd, $rsrc_fh);

    # Protect from compile-time errors on non-Mac OS systems that don't
    # define O_RSRC in Fcntl
    eval '$res_fd = POSIX::open($data_file, O_RDONLY | O_RSRC)';

    $rsrc_fh = new IO::File;

    unless($rsrc_fh->fdopen($res_fd, "r"))
    {
      $self->{'LAST_ERROR'} = "Couldn't open Mac resource fork: $@";
      return;    
    }

    $rsrc_fh->seek(0, SEEK_END);    # Fast forward to end
    $rsrc_pos = $rsrc_fh->tell();   # Get size
    $rsrc_fh->seek(0, SEEK_SET);    # Rewind
  }
  else
  {
    $rsrc_pos = (stat($rsrc_file))[7];
  }

  $length = unpack("N", substr($data, 16, 4));
  $length += 78;

  # Set new length
  substr($data, 12, 4)  = pack("N", $length);
  substr($data, 16, 4) = pack("N", $length);

  # Set new num atoms
  my($num_atoms) = unpack("n", substr($data, 20, 2));
  substr($data, 20, 2) = pack("n", $num_atoms + 1);

  # 00 CB 00 4A  52 46 4C 54  00 01 00 00  00 00 00 00  ...JRFLT........
  # 00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
  # 00 00 00 00  00 00 00 00  00 00 00 00  00 02 44 41  ..............DA
  # 54 41 00 00  1B EA 00 00  00 00 00 00  00 00 4D 41  TA............MA
  # 43 52 00 00  00 00 00 00  00 00 00 00  00 00        CR............
  $more_data = pack("x78");

  substr($more_data, 0, 2) = pack("n", HTLC_DATA_RFLT);
  substr($more_data, 2, 2) = pack("n", 0x004A);
  substr($more_data, 4, 4) = HTXF_RFLT_MAGIC;
  substr($more_data, 8, 2) = pack("n", 0x0001);

  substr($more_data, 45, 1) = pack("C", 0x02);
  substr($more_data, 46, 4) = 'DATA';
  substr($more_data, 50, 4) = pack("N", $data_pos);

  substr($more_data, 62, 4) = 'MACR';
  substr($more_data, 66, 4) = pack("N", $rsrc_pos);

  $data .= $more_data;

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: GET FILE - $task_num\n");
    $self->{'TASKS'}->{$task_num} = 
      new Net::Hotline::Task($task_num, HTLC_TASK_FILE_GET, time(), undef, 
                             [ $path, $data_file, $rsrc_file ]);
    return($task_num);
  }
  else { return }
}

sub _al09_file_action_stub
{
  my($self, $path, $type) = @_;

  my($data, @path_parts, $length, $file, $dir_len);

  $path =~ s/^$self->{'PATH_SEPARATOR'}//;
  $path =~ s/$self->{'PATH_SEPARATOR'}$//;
  @path_parts = split($self->{'PATH_SEPARATOR'}, $path);
  $path =~ s/$self->{'PATH_SEPARATOR'}//g;

  if(length($path) > HTLC_MAX_PATHLEN)
  {
    croak("Maximum path length exceeded");
  }

  $file = pop(@path_parts);

  # File part: 2 bytes num atoms, 2 bytes for atom len,
  # 2 bytes for file name length
  $length = (2 + 2 + 2 + length($file));

  if(@path_parts)
  {
    $dir_len = length(join('', @path_parts));
    # Path part: 2 bytes for atom type, 2 bytes for atom len
    # 2 bytes for num path components, and 2 null bytes and
    # 1 byte path part length for each path part
    $length += (2 + 2 + 2 + (3 * @path_parts));
    $length += $dir_len;
  }

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type($type);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len($length);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header();

  $data .= pack("n3a*", @path_parts ? 2 : 1,  # Number of atoms
                        HTLC_DATA_FILE,       # Atom type
                        length($file),        # Atom length
                        $file);               # Atom data

  if(@path_parts)
  {
    $data .= pack("n3", HTLC_DATA_DIRECTORY,  # Atom type
                        $dir_len + 2 + (3 * scalar(@path_parts)),
                                              # Atom length
                        scalar(@path_parts)); # Num path parts

    my($path_part);

    foreach $path_part (@path_parts)          # Path parts data
    {
      if(length($path_part) > HTLC_MAX_PATHLEN)
      {
        croak("Maximum path part length exceeded");
      }

      $data .= pack("nCa*", 0x0000,            # 2 null bytes
                            length($path_part),# Length
                            $path_part);       # Path part
    }
  }

  return($data, $proto_header->seq());
}

sub _file_action_simple
{
  my($self, $path, $type, $task_type, $task_name) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened() && length($path));

  my($data, $task_num) = _al09_file_action_stub($self, $path, $type);

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: $task_name - $task_num\n");
    $self->{'TASKS'}->{$task_num} = 
      new Net::Hotline::Task($task_num, $task_type, time(), undef, $path);
    return($task_num);
  }
  else { return }
}

sub move
{
  my($self, $src_path, $dest_path) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_move_now($src_path, $dest_path); 
  }
  else
  {
    return $self->_move($src_path, $dest_path);
  }
}

sub _move_now
{
  my($self, $src_path, $dest_path) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_move($src_path, $dest_path);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _move
{
  my($self, $src_path, $dest_path) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened() && length($src_path)  && length($dest_path));

  my($data, $task_num, $length, $num_atoms);
  my(@src_path_parts, $save_src_path, $src_file, $src_dir_len);
  my(@dest_path_parts, $save_dest_path, $dest_dir_len);

  # Source:

  $src_path =~ s/^$self->{'PATH_SEPARATOR'}//;
  $src_path =~ s/$self->{'PATH_SEPARATOR'}$//;
  $save_src_path = $src_path;
  @src_path_parts = split($self->{'PATH_SEPARATOR'}, $src_path);
  $src_path =~ s/$self->{'PATH_SEPARATOR'}//g;

  if(length($src_path) > HTLC_MAX_PATHLEN)
  {
    croak("Maximum path length exceeded");
  }

  $src_file = pop(@src_path_parts);

  # Source part: 2 bytes num atoms, 2 bytes for atom type,
  # 2 bytes for file name length
  $length = (2 + 2 + 2 + length($src_file));

  if(@src_path_parts)
  {
    $src_dir_len = length(join('', @src_path_parts));
    # Path part: 2 bytes for atom type, 2 bytes for atom len
    # 2 bytes for num path components, and 2 null bytes and
    # 1 byte path part length for each path part
    $length += (2 + 2 + 2 + (3 * @src_path_parts));
    $length += $src_dir_len;
  }

  # Destination:

  $dest_path =~ s/^$self->{'PATH_SEPARATOR'}//;
  $dest_path =~ s/$self->{'PATH_SEPARATOR'}$//;
  $save_dest_path = $dest_path;
  @dest_path_parts = split($self->{'PATH_SEPARATOR'}, $dest_path);
  $dest_path =~ s/$self->{'PATH_SEPARATOR'}//g;

  if(length($dest_path) > HTLC_MAX_PATHLEN)
  {
    croak("Maximum path length exceeded");
  }

  if(@dest_path_parts)
  {
    $dest_dir_len = length(join('', @dest_path_parts));
    # Path part: 2 bytes for atom type, 2 bytes for atom len
    # 2 bytes for num path components, and 2 null bytes and
    # 1 byte path part length for each path part
    $length += (2 + 2 + 2 + (3 * @dest_path_parts));
    $length += $dest_dir_len;
  }

  # Build packet

  if(@src_path_parts && @dest_path_parts) { $num_atoms = 3 }
  else                                    { $num_atoms = 2 }

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_FILE_MOVE);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len($length);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header();

  $data .= pack("n3a*", $num_atoms,           # Number of atoms
                        HTLC_DATA_FILE,       # Atom type
                        length($src_file),    # Atom length
                        $src_file);           # Atom data

  if(@src_path_parts)
  {
    $data .= pack("n3", HTLC_DATA_DIRECTORY,  # Atom type
                        $src_dir_len + 2 + (3 * scalar(@src_path_parts)),
                                              # Atom length
                        scalar(@src_path_parts));
                                              # Num path parts

    my($path_part);

    foreach $path_part (@src_path_parts)      # Path parts data
    {
      if(length($path_part) > HTLC_MAX_PATHLEN)
      {
        croak("Maximum path part length exceeded");
      }

      $data .= pack("nCa*", 0x0000,           # 2 null bytes
                            length $path_part,# Length
                            $path_part);      # Path part
    }
  }

  if(@dest_path_parts)
  {
    $data .= pack("n3", HTLC_DATA_DESTDIR,    # Atom type
                        $dest_dir_len + 2 + (3 * scalar(@dest_path_parts)),
                                              # Atom length
                        scalar(@dest_path_parts));
                                              # Num path parts

    my($path_part);

    foreach $path_part (@dest_path_parts)     # Path parts data
    {
      if(length($path_part) > HTLC_MAX_PATHLEN)
      {
        croak("Maximum path part length exceeded");
      }

      $data .= pack("nCa*", 0x0000,           # 2 null bytes
                            length $path_part,# Length
                            $path_part);      # Path part
    }
  }

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: MOVE FILE - $task_num\n");
    $self->{'TASKS'}->{$task_num} = 
      new Net::Hotline::Task($task_num, HTLC_TASK_FILE_MOVE, time(),
                         undef, [ $save_src_path, $save_dest_path ]);
    return($task_num);
  }
  else { return }
}

sub rename
{
  my($self, $path, $new_name) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_rename_now($path, $new_name); 
  }
  else
  {
    return $self->_rename($path, $new_name); 
  }
}

sub _rename_now
{
  my($self, $path, $new_name) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->rename($path, $new_name);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _rename
{
  my($self, $path, $new_name) = @_;

  return undef  unless(length($path) && length($new_name));
  return _change_file_info($self, $path, $new_name, undef);
}

sub comment
{
  my($self, $path, $comments) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al13_comment_now($path, $comments); 
  }
  else
  {
    return $self->_comment($path, $comments); 
  }
}

sub _al13_comment_now
{
  my($self, $path, $comments) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->comment($path, $comments);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _comment
{
  my($self, $path, $comments) = @_;

  return undef  unless(length($path));
  $comments = ""  unless(defined($comments));
  return _change_file_info($self, $path, undef, $comments);
}

sub _change_file_info
{
  my($self, $path, $name, $comments) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($data, $task_num, @path_parts, $length, $save_path, $file,
     $dir_len, $num_atoms);

  $path =~ s/^$self->{'PATH_SEPARATOR'}//;
  $path =~ s/$self->{'PATH_SEPARATOR'}$//;
  $save_path = $path;
  @path_parts = split($self->{'PATH_SEPARATOR'}, $path);
  $path =~ s/$self->{'PATH_SEPARATOR'}//g;

  if(length($path) > HTLC_MAX_PATHLEN)
  {
    croak("Maximum path length exceeded");
  }

  $file = pop(@path_parts);

  # File part: 2 bytes for num atoms, 2 bytes for atom type,
  # 2 bytes for file name length
  $length = (2 + 2 + 2 + length($file));

  if(@path_parts)
  {
    $dir_len = length(join('', @path_parts));
    # Path part: 2 bytes for atom type, 2 bytes for atom len
    # 2 bytes for num path components, and 2 null bytes and
    # 1 byte path part length for each path part
    $length += (2 + 2 + 2 + (3 * @path_parts));
    $length += $dir_len;
  }

  if(length($name))
  {
    # Name part: 2 bytes for atom type, 2 bytes for
    # atom len, and the new name
    $length += (2 + 2 + length($name));
  }

  if(defined($comments))
  {
    # Comments part: 2 bytes for atom type, 2 bytes for
    # atom len, length of the new comments, else 1 null
    # byte if removing comments.
    $length += 2 + 2;
    if(length($comments)) { $length += length($comments) }
    else                  { $length += 1                 }
  }

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_FILE_SETINFO);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len($length);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header();

  $num_atoms = (@path_parts) ? 2 : 1;
  $num_atoms++  if(length($name));
  $num_atoms++  if(defined($comments));

  $data .= pack("n3a*", $num_atoms,           # Number of atoms
                        HTLC_DATA_FILE,       # Atom type
                        length($file),        # Atom length
                        $file);               # Atom data

  if(@path_parts)
  {
    $data .= pack("n3", HTLC_DATA_DIRECTORY,  # Atom type
                        $dir_len + 2 + (3 * scalar(@path_parts)),
                                              # Atom length
                        scalar(@path_parts)); # Num path parts

    my($path_part);

    foreach $path_part (@path_parts)          # Path parts data
    {
      if(length($path_part) > HTLC_MAX_PATHLEN)
      {
        croak("Maximum path part length exceeded");
      }

      $data .= pack("nCa*", 0x0000,           # 2 null bytes
                            length $path_part,# Length
                            $path_part);      # Path part
    }
  }

  if(length($name))
  {
    $data .= pack("nna*", HTLC_DATA_FILE_RENAME,# Atom type
                          length($name),      # Length
                          $name);             # Name
  }

  if(defined($comments))
  {
    $data .= pack("n", HTLS_DATA_FILE_COMMENT);# Atom type

    if(length($comments))
    {
      $data .= pack("na*", length($comments), # Length
                           $comments);        # Comments
    }
    else # Remove comments
    {
      $data .=  pack("nx", 0x0001);           # Length + null byte
    }
  }

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: SET INFO - $task_num\n");
    $self->{'TASKS'}->{$task_num} = 
      new Net::Hotline::Task($task_num, HTLC_TASK_SET_INFO, time(), undef, $save_path);
    return($task_num);
  }
  else { return }
}

sub post_news
{
  my($self, @post) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al10_post_news_now(@post); 
  }
  else
  {
    return $self->_post_news(@post); 
  }
}

sub _al10_post_news_now
{
  my($self, @post) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->post_news(@post);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _post_news
{
  my($self, @post) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($post) = join('', @post);

  my($data, $task_num);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_NEWS_POST);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(SIZEOF_HL_SHORT_HDR + length($post));
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n3a*", 0x0001,                # Number of atoms
                       HTLS_DATA_NEWS_POST,   # Atom type
                       length($post),         # Atom length
                       $post);                # Atom data

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: POST NEWS - $task_num\n");
    $self->{'TASKS'}->{$task_num} =
      new Net::Hotline::Task($task_num, HTLC_TASK_NEWS_POST, time());
  }
  else { return }

  return($task_num);
}

sub get_news
{
  my($self) = shift;

  my($task, $task_num, $packet);

  $task_num = $self->req_news();
  $task = $self->{'TASKS'}->{$task_num};

  return(undef)  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return(undef);
  }

  if(wantarray)
  {
    return @{$self->{'NEWS'}};
  }
  else
  {
    return (@{$self->{'NEWS'}}) ? join('_' x 58, @{$self->{'NEWS'}}) : "";
  }
}

sub req_news
{
  my($self) = shift;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($data, $task_num);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_NEWS_GETFILE);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(SIZEOF_HL_TASK_FILLER);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n", 0x0000);

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: NEWS - $task_num\n");
    $self->{'TASKS'}->{$task_num} = 
      new Net::Hotline::Task($task_num, HTLC_TASK_NEWS, time());
    return($task_num);
  }
  else { return }
}

sub al04_user_by_nick
{
  my($self, $nick_match) = @_;

  my($socket, @users);

  eval { m/$nick_match/ };

  return undef  if($@ || !$self->{'USER_LIST'} || length($nick_match) == 0);

  foreach $socket (sort { $a <=> $b } keys(%{$self->{'USER_LIST'}}))
  {
    if($self->{'USER_LIST'}->{$socket}->nick() =~ /^$nick_match$/)
    {
      if(wantarray())
      {
        push(@users, $self->{'USER_LIST'}->{$socket});
      }
      else
      {
        return $self->{'USER_LIST'}->{$socket};
      }
    }
  }

  if(@users) { return @users }
  else       { return }
}

sub user_by_socket
{
  my($self, $socket) = @_;
  return $self->{'USER_LIST'}->{$socket};
}

sub icon
{
  my($self, $icon) = @_;

  return $self->{'ICON'}  unless($icon =~ /^-?\d+$/);

  return _update_user($self, $icon, $self->{'NICK'});
}

sub nick
{
  my($self, $nick) = @_;

  return $self->{'NICK'}  unless(defined($nick));

  return _update_user($self, $self->{'ICON'}, $nick);
}

sub _update_user
{
  my($self, $icon, $nick) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($data);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_USER_CHANGE);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len((SIZEOF_HL_SHORT_HDR * 2) + length($nick));
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n6a*", 0x0002,                # Num atoms

                       HTLC_DATA_ICON,        # Atom type
                       0x0002,                # Atom length
                       $icon,                 # Atom data

                       HTLC_DATA_NICKNAME,    # Atom type
                       length($nick),         # Atom length
                       $nick);                # Atom data

  $self->{'NICK'} = $nick;
  $self->{'ICON'} = $icon;

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    return(1);
  }
  else { return }
}

sub get_userlist
{
  my($self) = shift;

  my($task, $task_num, $packet);

  $task_num = $self->req_userlist();
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return $self->{'USER_LIST'};
}

sub al05_req_userlist
{
  my($self) = shift;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($data, $task_num);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_USER_GETLIST);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(SIZEOF_HL_TASK_FILLER);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n", 0x0000);

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: GET USER LIST - $task_num\n");
    $self->{'TASKS'}->{$task_num} =
      new Net::Hotline::Task($task_num, HTLC_TASK_USER_LIST, time());
    return($task_num);
  }
  else { return }
}

sub kick
{
  my($self, $user_or_socket) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_kick_now($user_or_socket); 
  }
  else
  {
    return $self->_kick($user_or_socket); 
  }
}

sub _kick_now
{
  my($self, $user_or_socket) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_kick($user_or_socket);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _kick
{
  my($self, $user_or_socket) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($socket, $task_num);

  if(ref($user_or_socket)) { $socket = $user_or_socket->socket() }
  else                     { $socket = $user_or_socket           }

  my($data);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_USER_KICK);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(SIZEOF_HL_LONG_HDR);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n4", 0x0001,                  # Num atoms

                     HTLC_DATA_SOCKET,        # Atom type
                     0x0002,                  # Atom length
                     $socket);                # Atom data

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: KICK($socket) - $task_num\n");
    $self->{'TASKS'}->{$task_num} =
      new Net::Hotline::Task($task_num, HTLC_TASK_KICK, time());
  }
  else { return }

  return ($task_num);
}

sub ban
{
  my($self, $user_or_socket) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_ban_now($user_or_socket); 
  }
  else
  {
    return $self->_ban($user_or_socket); 
  }
}

sub _ban_now
{
  my($self, $user_or_socket) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_ban($user_or_socket);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _ban
{
  my($self, $user_or_socket) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($socket, $task_num);

  if(ref($user_or_socket)) { $socket = $user_or_socket->socket() }
  else                     { $socket = $user_or_socket           }

  my($data);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_USER_KICK);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(SIZEOF_HL_LONG_HDR + 6);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n7", 0x0002,                  # Num atoms

                     HTLC_DATA_SOCKET,        # Atom type
                     0x0002,                  # Atom length
                     $socket,                 # Atom data

                     HTLC_DATA_BAN,           # Atom type
                     0x0002,                  # Atom length
                     0x0001);                 # Atom data (always 1???)

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: BAN($socket) - $task_num\n");
    $self->{'TASKS'}->{$task_num} =
      new Net::Hotline::Task($task_num, HTLC_TASK_BAN, time());
  }
  else { return }
  
  return ($task_num);
}

sub msg
{
  my($self, $user_or_socket, @message) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_msg_now($user_or_socket, @message); 
  }
  else
  {
    return $self->_msg($user_or_socket, @message); 
  }
}

sub _msg_now
{
  my($self, $user_or_socket, @message) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_msg($user_or_socket, @message);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _msg
{
  my($self, $user_or_socket, @message) = @_;

  my($message) = join('', @message);

  $message =~ s/\n/@{[HTLC_NEWLINE]}/osg;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($socket);

  if(ref($user_or_socket)) { $socket = $user_or_socket->socket() }
  else                     { $socket = $user_or_socket           }

  my($data, $task_num);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_MSG);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len((SIZEOF_HL_SHORT_HDR * 2) +
                     length($message));
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n6", 0x0002,                  # Num atoms

                     HTLC_DATA_SOCKET,        # Atom type
                     0x0002,                  # Atom length
                     $socket,                 # Atom data

                     HTLC_DATA_MSG,           # Atom type
                     length($message)) .      # Atom length
          $message;                           # Atom data

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: MSG - $task_num\n");
    $self->{'TASKS'}->{$task_num} =
      new Net::Hotline::Task($task_num, HTLC_TASK_SEND_MSG, time());
  }
  else { return }

  return($task_num);
}

sub chat_action
{
  my($self, @message) = @_;

  my($message) = join('', @message);

  $message =~ s/\n/@{[HTLC_NEWLINE]}/osg;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($data);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_CHAT);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len((SIZEOF_HL_SHORT_HDR  * 2) +
                     length($message));
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n6", 0x0002,                  # Num atoms

                     HTLC_DATA_OPTION,        # Atom type
                     0x0002,                  # Atom length
                     0x0001,                  # Atom data

                     HTLC_DATA_CHAT,          # Atom type
                     length($message)) .      # Atom length
          $message;                           # Atom data

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    return(1);
  }
  else { return }
}

sub chat
{
  my($self, @message) = @_;

  my($message) = join('', @message);

  $message =~ s/\n/@{[HTLC_NEWLINE]}/osg;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  my($data);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_CHAT);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(SIZEOF_HL_SHORT_HDR +
                     length($message));
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header() .
          pack("n3", 0x0001,                  # Num atoms

                     HTLC_DATA_CHAT,          # Atom type
                     length($message)) .      # Atom length
          $message;                           # Atom data

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    return(1);
  }
  else { return }
}

sub send_file
{
  my($self, $task, $ref, $size, $resume) = @_;

  my($server, $port, $data, $xfer, $length, $buf_size);
  my($local_sep, $remote_sep, $filename, $src_path, $dest_path);
  my($type, $creator, $created, $modified, $finder_flags,  $comments,
     $data_fh, $rsrc_fh, $data_len, $rsrc_len, $data_pos, $rsrc_pos,
     $res_fd);

  $task->finish(undef);

  $local_sep = PATH_SEPARATOR;

  $buf_size = $self->{'HTXF_BUFSIZE'};

  if($resume)
  {
    # 52 46 4c 54  00 01 00 00  00 00 00 00  00 00 00 00  RFLT............
    # 00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
    # 00 00 00 00  00 00 00 00  00 02 44 41  54 41 00 06  ..........DATA..
    # 9a cf 00 00  00 00 00 00  00 00 4d 41  43 52 00 00  ..........MACR..
    # 00 00 00 00  00 00 00 00  00 00                     ..........

    unless(substr($resume, 0, 4) eq 'RFLT')
    {
      $task->error(1);
      $task->finish(time());
      $task->error_text("Bad data from server!");
      $self->{'LAST_ERROR'} = $task->error_text();
      return;
    }

    $data_pos = unpack("N", substr($resume, 46, 4));
    $rsrc_pos = unpack("N", substr($resume, 62, 4));
  }

  $data_fh = new IO::File;
  $rsrc_fh = new IO::File;

  ($src_path, $dest_path) = @{$task->path()};

  ($filename = $src_path) =~ s/^.*?$local_sep([^$local_sep]+)$/$1/;

  ($data_len, $rsrc_len, $comments, $finder_flags, $type, $creator, $length)
    = @{$task->misc()};

  unless($data_fh->open($src_path))
  {
    $task->error(1);
    $task->finish(time());
    $task->error_text("Could not open to $src_path: $!");
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  if($self->{'MACOS'})
  {
    # Protect from compile-time errors on non-Mac OS systems that don't
    # define O_RSRC in Fcntl
    eval '$res_fd = POSIX::open($src_path, O_RDONLY | O_RSRC)';

    unless($rsrc_fh->fdopen($res_fd, "r"))
    {
      $task->error(1);
      $task->finish(time());
      $task->error_text("Could not read to resource fork from $src_path: $!");
      $self->{'LAST_ERROR'} = $task->error_text();
      return;
    }
  }
  elsif($rsrc_len > 0 || ($resume && $rsrc_pos > 0))
  {
    $task->error(1);
    $task->finish(time());
    $task->error_text("Server is expecting resource fork data from a non-Mac OS client!\n" .
                      "Are you sure you're uploading the right file?");
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  if($resume)
  {
    if($rsrc_pos > 0)
    {
      unless($rsrc_fh->seek($rsrc_pos, 0))
      {
        $task->error(1);
        $task->finish(time());
        $task->error_text("Could not seek to position $rsrc_pos in resource fork of $src_path: $!");
        $self->{'LAST_ERROR'} = $task->error_text();
        return;
      }
    }

    if($data_pos > 0)
    {
      unless($data_fh->seek($data_pos, 0))
      {
        $task->error(1);
        $task->finish(time());
        $task->error_text("Could not seek to position $data_pos in $src_path: $!");
        $self->{'LAST_ERROR'} = $task->error_text();
        return;
      }
    }
  }

  ($created, $modified) = (stat($src_path))[9,10];

  unless($self->{'MACOS'})
  {
    $created  += HTLC_UNIX_TO_MACOS_TIME;
    $modified += HTLC_UNIX_TO_MACOS_TIME;
  }

  $data = HTXF_MAGIC . pack("NNx4", $ref, ($length - $rsrc_pos - $data_pos));

  $server = $self->{'SERVER_ADDR'};

  # HTXF_TCPPORT only if server port is 5500
  $port = $self->{'SERVER_PORT'} + 1; 

  unless($xfer = IO::Socket::INET->new(PeerAddr =>$server,
                                       PeerPort =>$port,
                                       Timeout  =>$self->{'CONNECT_TIMEOUT'},
                                       Proto    =>'tcp'))
  {
    $task->finish(time());
    $task->error_text("Could not open file transfer connection: $@");
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  _debug(_hexdump($data));

  unless(_hlc_write($self, $xfer, \$data, length($data)))
  {
    $xfer->close();
    $task->error(1);
    $task->finish(time());
    $task->error_text($self->{'LAST_ERROR'});
    return;
  }

  # 46 49 4c 50  00 01 00 00  00 00 00 00  00 00 00 00  FILP............
  # 00 00 00 00  00 00 00 03  49 4e 46 4f  00 00 00 00  ........INFO....
  # 00 00 00 00  00 00 00 5c  41 4d 41 43  53 49 54 44  .......\AMACSITD
  # 53 49 54 21  00 00 00 00  00 00 21 00  00 00 00 00  SIT!......!.....
  # 00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
  # 00 00 00 00  00 00 00 00  00 00 00 00  00 07 70 00  ..............p.
  # 00 b1 ce 81  92 07 70 00  00 02 df 7d  3d 00 00 00  ......p....}=...

  # 12 53 77 6f  6f 70 20 46  41 51 2e 74  65 78 74 2e  .Swoop FAQ.text.
  # 73 69 74 00  00 44 41 54  41 00 00 00  00 00 00 00  sit..DATA.......
  # 00 00 00 59  5c                                     ...Y\
  $data = pack("a4nx16na4x8Na4a4a4x6nx32nx2Nnx2NN",
               "FILP", 0x0001, 0x0003, "INFO",
               length($comments) + length($filename) + 74,
               "AMAC", $type, $creator, $finder_flags, 0x0770,
               $created, 0x0770, $modified, length($filename));

  $data .= $filename .
           pack("n", length($comments)) .
           $comments .
           pack("a4x8N", "DATA", ($data_len - $data_pos));

  _debug(_hexdump($data));

  unless(_hlc_write($self, $xfer, \$data, length($data)))
  {
    $xfer->close();
    $task->error(1);
    $task->finish(time());
    $task->error_text($self->{'LAST_ERROR'});
    return;
  }

  # Upload data fork
  unless($self->_upload($xfer, $data_fh, $data_len, $buf_size))
  {
    $xfer->close();
    $task->error(1);
    $task->finish(time());
    $task->error_text("Upload did not complete.");
  }

  # 4D 41 43 52  00 00 00 00  00 00 00 00  00 00 01 EC  MACR............
  $data = pack("a4x8N", "MACR", ($rsrc_len - $rsrc_pos));

  _debug(_hexdump($data));

  unless(_hlc_write($self, $xfer, \$data, length($data)))
  {
    $xfer->close();
    $task->error(1);
    $task->finish(time());
    $task->error_text($self->{'LAST_ERROR'});
    return;
  }

  if($rsrc_len > 0)
  {
    # Upload resource fork
    unless($self->_upload($xfer, $rsrc_fh, $rsrc_len, $buf_size))
    {
      $xfer->close();
      $task->error(1);
      $task->finish(time());
      $task->error_text("Upload did not complete.");
      return;
    }
  }

  return(1);
}

sub recv_file
{
  my($self, $task, $ref, $size) = @_;

  my($server, $data, $xfer, $tot_length, $length, $buf_size, @ret);
  my($data_file, $rsrc_file, $type, $creator, $created, $modified,
     $finder_flags,  $comments, $comments_len, $data_fh, $data_len,
     $rsrc_fh, $rsrc_len, $name_len, $real_mac_res_fork, $res_fd,
     $finished_file, $port);

  $tot_length = $size;

  $buf_size = $self->{'HTXF_BUFSIZE'};

  $data_fh = new IO::File;
  $rsrc_fh = new IO::File;

  ($data_file, $rsrc_file) = @{$task->path()}[1, 2];

  if($self->{'MACOS'})
  {
    if(length($data_file) > MACOS_MAX_FILENAME)
    {
      for($data_file)
      {
        my($len) = MACOS_MAX_FILENAME - 6;
        
        # Try to preserve filename extension, if any
        # ("\xC9" is "..." in Mac OS)
        # Otherwise, just truncate
        s/^(.{$len}).*?\.(\w{1,4})/$1\xC9.$2/o ||
        s/^(.@{[MACOS_MAX_FILENAME]}).*/$1/;
      }
    }
  }

  unless($data_fh->open(">>$data_file"))
  {
    $task->error(1);
    $task->finish(time());
    $task->error_text("Could not write to $data_file: $!");
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  if($self->{'MACOS'})
  {
    # Protect from compile-time errors on non-Mac OS systems that don't
    # define O_RSRC in Fcntl
    eval '$res_fd = POSIX::open($data_file, O_WRONLY | O_CREAT | O_RSRC)';
  }

  # If we're on Mac OS and we can write directly to the resource fork
  if(defined($res_fd) && $rsrc_fh->fdopen($res_fd, "w"))
  {
    $real_mac_res_fork = 1;
    # Temporarily set file type and creator to Hotline's "partial download"
    MacPerl::SetFileInfo(HTXF_PARTIAL_CREATOR, HTXF_PARTIAL_TYPE, $data_file);
  }
  else
  {
    unless($rsrc_fh->open(">>$rsrc_file"))
    {
      $task->error(1);
      $task->finish(time());
      $task->error_text("Could not write to $rsrc_file: $!");
      $self->{'LAST_ERROR'} = $task->error_text();
      return;
    }
  }

  $task->finish(undef);

  $server = $self->{'SERVER_ADDR'};

  $data = HTXF_MAGIC . pack("Nx8", $ref);

  # HTXF_TCPPORT only if server port is 5500
  $port = $self->{'SERVER_PORT'} + 1; 

  unless($xfer = IO::Socket::INET->new(PeerAddr =>$server,
                                       PeerPort =>$port,
                                       Timeout  =>$self->{'CONNECT_TIMEOUT'},
                                       Proto    =>'tcp'))
  {
    $task->finish(time());
    $task->error_text("Could not open file transfer connection: $@");
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  unless(_hlc_write($self, $xfer, \$data, length($data)))
  {
    $xfer->close();
    $task->error(1);
    $task->finish(time());
    $task->error_text($self->{'LAST_ERROR'});
    return;
  }

  # 46 49 4C 50  00 01 00 00  00 00 00 00  00 00 00 00  FILP............
  # 00 00 00 00  00 00 00 03  49 4E 46 4F  00 00 00 00  ........INFO....
  # 00 00 00 00  00 00 00 60                            .......`
  unless(_hlc_buffered_read($self, $xfer, \$data, SIZEOF_HL_FILE_XFER_HDR))
  {
    $xfer->close();
    $task->error(1);
    $task->finish(time());
    $task->error_text($self->{'LAST_ERROR'});
    return;
  }

  $tot_length -= SIZEOF_HL_FILE_XFER_HDR;
  $length = (unpack("N", substr($data, 36, 4)) + SIZEOF_HL_FILE_FORK_HDR);

  unless(substr($data, 0, 4) eq 'FILP')
  {
    $xfer->close();
    $task->error(1);
    $task->finish(time());
    $task->error_text("Bad data from server!");
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  #                           41 4D 41 43  54 45 58 54          AMACTEXT
  # 74 74 78 74  00 00 00 00  00 00 01 00  00 00 00 00  ttxt............
  # 00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
  # 00 00 00 00  00 00 00 00  00 00 00 00  07 70 00 00  .............p..
  # AE A3 8A 18  07 70 00 00  AE A3 8C 1D  00 00 00 05  .....p..........
  # 74 65 78 74  32 00 11 66  74 70 2E 6D  69 63 72 6F  text2..ftp.micro
  # 73 6F 66 74  2E 63 6F 6D  44 41 54 41  00 00 00 00  soft.comDATA....
  # 00 00 00 00  00 00 01 00                            ........

  unless(_hlc_buffered_read($self, $xfer, \$data, $length))
  {
    $xfer->close();
    $task->error(1);
    $task->finish(time());
    $task->error_text($self->{'LAST_ERROR'});
    return;
  }

  $tot_length -= $length;

  $type     = substr($data, 4, 4);
  $creator  = substr($data, 8, 4);

  $created      = unpack("N", substr($data, 56, 4));
  $finder_flags = substr($data, 18, 2);
  $modified     = unpack("N", substr($data, 64, 4));
  $name_len     = unpack("C", substr($data, 71, 1));
  $comments_len = unpack("n", substr($data, 72 + $name_len, 2)); # 72
  $comments = substr($data, 72 + $name_len + 2, $comments_len);

  $data_len = unpack("N", substr($data, -4));
  $length = $self->_download($xfer, $data_fh, $data_len, $buf_size);
  $tot_length -= $length;
  $data_fh->close();

  unless($length == $data_len)
  {
    $xfer->close();
    $task->error(1);
    $task->finish(time());
    $task->error_text("Download incomplete.");
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  # Yet another server bug: it'll tell you it's going to send a resource
  # fork header even when the file has no resource fork (i.e. $size will
  # be SIZEOF_HL_FILE_FORK_HDR bytes larger than the data the server will
  # actually send).  So we only try to read if we have more than
  # SIZEOF_HL_FILE_FORK_HDR left.
  if($tot_length > SIZEOF_HL_FILE_FORK_HDR)
  {
    # 4D 41 43 52  00 00 00 00  00 00 00 00  00 00 01 EC  MACR............
    $length = _hlc_buffered_read($self, $xfer, \$data, SIZEOF_HL_FILE_FORK_HDR);

    return  unless($length);
    $tot_length -= $length;

    $rsrc_len = unpack("N", substr($data, -4));
    $length = $self->_download($xfer, $rsrc_fh, $rsrc_len, $buf_size);
    $tot_length -= $length;
    $rsrc_fh->close();

    unless($length == $rsrc_len)
    {
      $xfer->close();
      $task->error(1);
      $task->finish(time());
      $task->error_text("Download incomplete.");
      $self->{'LAST_ERROR'} = $task->error_text();
      return;
    }
  }
  else
  {
    $tot_length = 0;
    $rsrc_len = 0;
  }

  $xfer->close();

  unless($tot_length == 0)
  {
    $task->error(1);
    $task->finish(time());
    $task->error_text("Tried to download $size bytes, got " .
                      $size - $tot_length . " bytes instead.");
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  $data_len = (stat($data_file))[7];
  $rsrc_len = (stat($rsrc_file))[7];

  unless($rsrc_len)
  {
    unlink($rsrc_file)  if(-e $rsrc_file);
    undef $rsrc_file;
    $rsrc_len = 0;
  }

  unless($data_len || $real_mac_res_fork)
  {
    unlink($data_file)  if(-e $data_file);
    undef $data_file;
    $data_len = 0;
  }

  $task->finish(time());

  # Set the rest of the Mac OS information if we're doing that sort of thing
  if(($real_mac_res_fork && -e $data_file))
  {
    utime($created, $modified, $data_file);

    my($fsspec) = MacPerl::MakeFSSpec($data_file);

    if(length($comments))
    {
      Mac::MoreFiles::FSpDTSetComment($fsspec, $comments);
    }

    my($cat)   = Mac::Files::FSpGetCatInfo($fsspec);
    my($finfo) = $cat->ioFlFndrInfo();

    $finfo->fdFlags(unpack("n", $finder_flags) & 0xFEFF);
    $finfo->fdType($type);
    $finfo->fdCreator($creator);
    $cat->ioFlFndrInfo($finfo);
    Mac::Files::FSpSetCatInfo($fsspec, $cat);

    # Rename data file to remove the .data part
    ($finished_file = $data_file) =~ s/$self->{'DATA_FORK_EXT'}$//;
    unless(CORE::rename($data_file, $finished_file))
    {
      $task->error_text(qq(Could not rename "$data_file" to "$finished_file": $!));
      $self->{'LAST_ERROR'} = $task->error_text();
      return;
    }

    # Return a sigle true value rather than an array of parameters
    # to indicate that you can't call macbinary() if we've already
    # made a Mac file.
    return(1);
  }
  elsif(! -e $rsrc_file)
  {
    ($finished_file = $data_file) =~ s/$self->{'DATA_FORK_EXT'}$//;
    CORE::rename($data_file, $finished_file);
    $data_file = $finished_file;
  }

  return [ $data_file, $data_len,
           $rsrc_file, $rsrc_len,
           $buf_size, $type, $creator, $comments,
           $created, $modified, $finder_flags ];
}

sub _download
{
  my($self, $src_fh, $dest_fh, $len, $buf_size) = @_;

  my($data, $tot_read, $read);

  $tot_read = 0;

  if($len <= $buf_size)
  {
    $read = read($src_fh, $data, $len);
    return  unless(defined($read));
    print $dest_fh $data || return;
    $tot_read += $read;
  }
  else
  {
    my($loop)     = int($len/$buf_size);
    my($leftover) = $len % $buf_size;

    for(; $loop > 0; $loop--)
    {
      $read = read($src_fh, $data, $buf_size);
      return  unless(defined($read));
      print $dest_fh $data || return;
      $tot_read += $read;
    }

    if($leftover > 0)
    {
      $read = read($src_fh, $data, $leftover);
      return  unless(defined($read));
      print $dest_fh $data || return;
      $tot_read += $read;
    }
  }

  unless($tot_read == $len)
  {
    croak("Tried to read $len bytes, actually read $tot_read.  Download may be corrupted!");
  }

  return($tot_read);
}

sub _upload
{
  my($self, $dest_fh, $src_fh, $len, $buf_size) = @_;

  my($data);

  if($len <= $buf_size)
  {
    unless(defined(read($src_fh, $data, $len))) { return }
    _hlc_write($self, $dest_fh, \$data, length($data)) || return;
  }
  else
  {
    my($loop)     = int($len/$buf_size);
    my($leftover) = $len % $buf_size;

    for(; $loop > 0; $loop--)
    {
      unless(defined(read($src_fh, $data, $buf_size))) { return }
      _hlc_write($self, $dest_fh, \$data, length($data)) || return;
    }

    if($leftover > 0)
    {
      unless(defined(read($src_fh, $data, $leftover))) { return }
      _hlc_write($self, $dest_fh, \$data, length($data)) || return;
    }
  }

  return(1);
}

sub macbinary
{
  my($self) = shift  if(ref($_[0]));

  my($macbin_file, $params) = @_;

  unless(ref($params) =~ /^ARRAY/ && @{$params} == 11)
  {
    croak("Incorrect arguments to macbinary()");
  }

  my($data_file, $data_len,
     $rsrc_file, $rsrc_len,
     $buf_size, $type, $creator, $comments,
     $created, $modified, $finder_flags) = @{$params};

  my($finished_file, $filename, $macbin_fh, $data_fh, $rsrc_fh,
     $macbin_hdr, $buf, $len, $pad);

  unless($rsrc_len > 0 || $data_len > 0)
  {
    $self->{'LAST_ERROR'} = "No resource or data fork length."  if($self);
    $! = "No resource or data fork length.";
    return;
  }

  if(defined($data_file))
  {
    ($finished_file = $data_file) =~ s/$self->{'DATA_FORK_EXT'}$//;
  }
  elsif(defined($rsrc_file))
  {
    ($finished_file = $rsrc_file) =~ s/$self->{'RSRC_FORK_EXT'}$//;
  }
  else
  {
    croak "Bad arguments to macbinary() - No rsrc or data file arguments.";
  }

  $finished_file =~ /([^@{[PATH_SEPARATOR]}]+)$/o;
  $filename = $1;

  unless(length($macbin_file))
  {
    $macbin_file .= "$finished_file.bin";
  }

  if(-e $macbin_file)
  {
    $self->{'LAST_ERROR'} = "$macbin_file: file already exists."  if($self);
    $! = "$macbin_file: file already exists.";
    return;
  }

  $buf_size = 4096  unless($buf_size =~ /^\d+$/);

  $macbin_fh = new IO::File;
  $data_fh   = new IO::File;
  $rsrc_fh   = new IO::File;

  unless($macbin_fh->open(">$macbin_file"))
  {
    $self->{'LAST_ERROR'} = $!  if($self);
    return;
  }

  $macbin_hdr = pack("x128"); # Start with empty 128 byte header

  # Offset 000-Byte, old version number, must be kept at zero for compatibility

  # Offset 001-Byte, Length of filename (must be in the range 1-63)
  substr($macbin_hdr, 1, 1) = pack("C", length($filename));

  # Offset 002-1 to 63 chars, filename (only "length" bytes are significant).
  substr($macbin_hdr, 2, length($filename)) = $filename;

  # Offset 065-Long Word, file type (normally expressed as four characters)
  substr($macbin_hdr, 65, 4) = $type;

  # Offset 069-Long Word, file creator (normally expressed as four characters)
  substr($macbin_hdr, 69, 4) = $creator;

  # Offset 073-Byte, original Finder flags
  #     Bit 7 - Locked.
  #     Bit 6 - Invisible.
  #     Bit 5 - Bundle.
  #     Bit 4 - System.
  #     Bit 3 - Bozo.
  #     Bit 2 - Busy.
  #     Bit 1 - Changed.
  #     Bit 0 - Inited.
  substr($macbin_hdr, 73, 1) =             # Clear inited bit
    pack("C", unpack("C", substr($finder_flags, 0, 1)) & 0xFE);

  # Offset 074-Byte, zero fill, must be zero for compatibility

  # Offset 075-Word, file's vertical position within its window.
  substr($macbin_hdr, 75, 2) = pack("n", 0xFFFF);

  # Offset 077-Word, file's horizontal position within its window.
  substr($macbin_hdr, 77, 2) = pack("n", 0xFFFF);

  # Offset 079-Word, file's window or folder ID.
  # Offset 081-Byte, "Protected" flag (in low order bit).
  # Offset 082-Byte, zero fill, must be zero for compatibility

  # Offset 083-Long Word, Data Fork length (bytes, zero if no Data Fork).
  substr($macbin_hdr, 83, 4) = pack("N", $data_len);

  # Offset 087-Long Word, Resource Fork length (bytes, zero if no R.F.).
  substr($macbin_hdr, 87, 4) = pack("N", $rsrc_len);

  # Offset 091-Long Word, File's creation date
  substr($macbin_hdr, 91, 4) = pack("N", $created);

  # Offset 095-Long Word, File's "last modified" date.
  substr($macbin_hdr, 95, 4) = pack("N", $modified);

  # Offset 099-Word, length of Get Info comment to be sent after the resource fork
  #            (if implemented, see below).
  # Offset 101-Byte, Finder Flags, bits 0-7. (Bits 8-15 are already in byte 73)
  # Offset 116-Long Word, Length of total files when packed files are unpacked.
  #            This is only used by programs that pack and unpack on the fly,
  #            mimicing a standalone utility such as PackIt. A program that is
  #            uploading a single file must zero this location when sending a
  #            file. Programs that do not unpack/uncompress files when
  #            downloading may ignore this value.
  substr($macbin_hdr, 116, 4) = pack("N", $data_len + $rsrc_len);

  # Offset 120-Word, Length of a secondary header. If this is non-zero,
  #            Skip this many bytes (rounded up to the next multiple of 128)
  #            This is for future expansion only, when sending files with
  #            MacBinary, this word should be zero.

  # Offset 122-Byte, Version number of Macbinary II that the uploading program
  # is written for (the version begins at 129)
  substr($macbin_hdr, 122, 1) = pack("C", 129);

  # Offset 123-Byte, Minimum MacBinary II version needed to read this file
  # (start this value at 129 129)
  substr($macbin_hdr, 123, 1) = pack("C", 129);

  # Offset 124-Word, CRC of previous 124 bytes
  substr($macbin_hdr, 124, 2) = pack("n", macbin_crc(substr($macbin_hdr, 0, 124), 0));

  # Macbinary II header
  print $macbin_fh $macbin_hdr;

  # Data fork, null padded to a multiple of 128 bytes
  if($data_len)
  {
    unless($data_fh->open($data_file))
    {
      $self->{'LAST_ERROR'} = $!  if($self);
      return;
    }

    while($len = read($data_fh, $buf, $buf_size))
    {
      croak("read() error: $!")  unless(defined($len));
      print $macbin_fh $buf;
    }
    $data_fh->close();

    if($data_len % 128)
    {
      $pad = "x" . (128 - ($data_len % 128));
      print $macbin_fh pack($pad);
    }
  }

  # Resource fork, null padded to a multiple of 128 bytes
  if($rsrc_len)
  {
    unless($rsrc_fh->open($rsrc_file))
    {
      $self->{'LAST_ERROR'} = $!  if($self);
      return;
    }

    while($len = read($rsrc_fh, $buf, $buf_size))
    {
      croak("read() error: $!")  unless(defined($len));
      print $macbin_fh $buf;      
    }
    $rsrc_fh->close();

    if($rsrc_len % 128)
    {
      $pad = "x" . (128 - ($rsrc_len % 128));
      print $macbin_fh pack($pad);
    }
  }

  $macbin_fh->close();

  return(1);
}

sub tracker
{
  $_[0]->{'TRACKER_ADDR'} = $_[1]  if(@_ == 2);
  return $_[0]->{'TRACKER_ADDR'};
}

sub tracker_list
{
  my($self, $timeout) = @_;

  my($tracker, $tracker_address, $server, $port, @servers, $data,
     $num_servers, $length, $tli_ip, $tli_port, $tli_num_users,
     $tli_name, $tli_desc, $byte1);

  $tracker_address = $self->{'TRACKER_ADDR'};

  unless($tracker_address =~ /\S/)
  {
    croak("Tracker address not set!");
  }

  if(($server = $tracker_address) =~ s/^([^ :]+)(?:[: ](\d+))?$/$1/)
  {
    $port = $2 || HTRK_TCPPORT;
  }
  else
  {
    croak("Bad server address: $tracker_address");
  }

  $timeout = $self->{'CONNECT_TIMEOUT'}  unless(defined($timeout));

  eval
  {
    $SIG{'ALRM'} = sub { die "timeout" };
    alarm($timeout);
  
    $tracker = IO::Socket::INET->new(PeerAddr =>$server,
                                     PeerPort =>$port,
                                     Timeout  =>$timeout,
                                     Proto    =>'tcp');
    alarm(0);
    $SIG{'ALRM'} = 'DEFAULT';
  };

  if($@ =~ /timeout/)
  {
    $self->{'LAST_ERROR'} = "Timed out after $timeout seconds.";
    return;
  }

  if(!$tracker || $@)
  {
    $self->{'LAST_ERROR'} = $@ || $! || 'Connection failed';
    return;
  }

  # 48 54 52 4B  00 01                                  HTRK..
  _hlc_write($self, $tracker, \HTRK_MAGIC, HTRK_MAGIC_LEN) || return;

  # 48 54 52 4B  00 01                                  HTRK..
  _hlc_buffered_read($self, $tracker, \$data, HTRK_MAGIC_LEN) || return;

  unless($data eq HTRK_MAGIC)
  {
    $self->{'LAST_ERROR'} = "Bad data from tracker.  Not a hotline tracker?";
    return;
  }

  # 00 01 1F F5  00 53 00 4A  | D1 9C 4B 86  15 7C 00 04  .....S.J..K..|..
  # ^^^^^^^^^^^  ^^^^^ ^^^^^  | ^^^^^^^^^^^  ^^^^^ ^^^^^
  # ???????????    |   ?????  | IP Address   Port  num users ...
  #              num servers  |
  _hlc_buffered_read($self, $tracker, \$data, 8) || return;

  $num_servers = unpack("n", substr($data, 4, 2));

  # Bug fixes here thanks to Les Brown <Les@hotlinecentral.com>
  while(@servers < $num_servers)
  {
    # 4 bytes for IP, 2 bytes for port, 2 bytes for num users
    unless(_hlc_buffered_read($self, $tracker, \$data, 4 + 2 + 2))
    {
      $tracker->close()  if($tracker->opened());
      return  unless(@servers);
      return (wantarray) ? @servers : \@servers;
    }

    # Skip these 8 bytes if the first byte was zero
    $byte1 = unpack("C", substr($data, 0, 1));
    next  if($byte1 == 0);

    $tli_ip        = join('.', map { unpack("C", $_) } split('', substr($data, 0, 4)));
    $tli_port      = unpack("n", substr($data, 4, 2));
    $tli_num_users = unpack("n", substr($data, 6, 2));

    # 2 null bytes, 1 byte for name len
    unless(_hlc_buffered_read($self, $tracker, \$data, 2 + 1))
    {
      $tracker->close()  if($tracker->opened());
      return  unless(@servers);
      return (wantarray) ? @servers : \@servers;
    }

    $length = unpack("C", substr($data, 2, 1));

    # $length bytes for name, 1 byte for description length
    unless(_hlc_buffered_read($self, $tracker, \$data, $length + 1))
    {
      $tracker->close()  if($tracker->opened());
      return  unless(@servers);
      return (wantarray) ? @servers : \@servers;
    }

    $length = unpack("C", chop($tli_name = $data));

    # $length bytes for description
    unless(_hlc_buffered_read($self, $tracker, \$tli_desc, $length))
    {
      $tracker->close()  if($tracker->opened());
      return  unless(@servers);
      return (wantarray) ? @servers : \@servers;
    }

    push(@servers, new Net::Hotline::TrackerListItem($tli_ip,
                                                     $tli_port,
                                                     $tli_num_users,
                                                     $tli_name,
                                                     $tli_desc));
  }

  $tracker->close()  if($tracker->opened());

  return (wantarray) ? @servers : \@servers;
}

sub pchat_invite
{
  my($self, $socket, $ref) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al11_pchat_invite_now($socket, $ref);
  }
  else
  {
    return $self->_pchat_invite($socket, $ref);
  }
}

sub _al11_pchat_invite_now
{
  my($self, $socket, $ref) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_pchat_invite($socket, $ref);
  $task = $self->{'TASKS'}->{$task_num};

  return(1)  if(defined($ref));

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _pchat_invite
{
  my($self, $socket, $ref) = @_;

  my($data, $proto_header, $length, $task_num, $create);

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened());

  $create = defined($ref);

  # 8 bytes for socket atom + 6 or 8 bytes for pchat ref atom (optional)
  $length = 8 + (defined($ref)) ? (($ref > 0xFFFF) ? 8 : 6) : 0;

  $proto_header = new Net::Hotline::Protocol::Header;

  $proto_header->type(($create) ? HTLC_HDR_PCHAT_CREATE :
                                  HTLC_HDR_PCHAT_INVITE);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len($length);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header();

  # Socket of the user we're inviting
  $data .= pack("nnnn", ($create) ? 2 : 1,    # Num atoms
                        HTLC_DATA_SOCKET,     # Atom type
                        0x0002,               # Atom length
                        $socket);             # Atom value

  unless($create)
  {
    my($fmt) = ($ref > 0xFFFF) ? "nnN" : "nnn";

    # Private chat reference number
    $data .= pack($fmt, HTLC_DATA_PCHAT_REF,  # Atom type
                       ($ref > 0xFFFF) ? 4 :2,# Atom length
                       $ref);                 # Atom value
  }

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if($create)
  {
    if(_hlc_write($self, $server, \$data, length($data)))
    {
      _debug("NEW TASK: PCHAT INVITE/CREATE - $task_num\n");
      $self->{'TASKS'}->{$task_num} =
        new Net::Hotline::Task($task_num,HTLC_TASK_PCHAT_CREATE, time());
    }
    else { return }

    return($task_num);
  }
  else
  {
    if(_hlc_write($self, $server, \$data, length($data)))
    {
      _debug("PCHAT INVITE SOCKET($socket) TO PCHAT($ref)\n");
      return(1);
    }
    else { return }
  }
}

sub pchat_accept
{
  my($self, $ref) = @_;

  if($self->{'BLOCKING_TASKS'})
  {
    return $self->_al12_pchat_accept_now($ref);
  }
  else
  {
    return $self->_pchat_accept($ref);
  }
}

sub _al12_pchat_accept_now
{
  my($self, $ref) = @_;

  my($task, $task_num, $packet);

  $task_num = $self->_pchat_accept($ref);
  $task = $self->{'TASKS'}->{$task_num};

  return  unless($task_num);

  $packet = _blocking_task($self, $task_num);

  if($task->error())
  {
    $self->{'LAST_ERROR'} = $task->error_text();
    return;
  }

  return(1);
}

sub _pchat_accept
{
  my($self, $ref) = @_;

  my($data, $proto_header, $task_num);

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened() && defined($ref));

  $proto_header = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_PCHAT_ACCEPT);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(($ref > 0xFFFF) ? 10 : 8);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header();

  my($fmt) = ($ref > 0xFFFF) ? "nnnN" : "nnnn";

  # Pchat ref number atom
  $data .= pack($fmt, 0x0001,                 # Num atoms
                      HTLC_DATA_PCHAT_REF,    # Atom type
                      ($ref > 0xFFFF) ? 4 : 2,# Atom length
                      $ref);                  # Atom value

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    _debug("NEW TASK: PCHAT ACCEPT($ref) - $task_num\n");
    $self->{'TASKS'}->{$task_num} =
      new Net::Hotline::Task($task_num, HTLC_TASK_PCHAT_ACCEPT, time(), undef, undef, $ref);
  }
  else { return }

  return($task_num);
}

sub pchat_decline
{
  my($self, $ref) = @_;

  my($data, $proto_header, $task_num, $length);

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened() && defined($ref));

  $proto_header = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_PCHAT_DECLINE);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(($ref > 0xFFFF) ? 10 : 8);
  $proto_header->len2($proto_header->len);

  $data = $proto_header->header();

  my($fmt) = ($ref > 0xFFFF) ? "nnnN" : "nnnn";

  # Pchat ref number atom
  $data .= pack($fmt, 0x0001,                 # Num atoms
                      HTLC_DATA_PCHAT_REF,    # Atom type
                      ($ref > 0xFFFF) ? 4 : 2,# Atom length
                      $ref);                  # Atom value

  _debug(_hexdump($data));

  $task_num = $proto_header->seq();

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    return(1);
  }
  else { return }
}

sub al07_pchat_action
{
  my($self, $ref, @message) = @_;

  my($message) = join('', @message);

  $message =~ s/\n/@{[HTLC_NEWLINE]}/osg;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened() && defined($ref));

  my($data);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_CHAT);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len((($ref > 0xFFFF) ? 20 : 18) + length($message));
  $proto_header->len2($proto_header->len);

  my($fmt) = ($ref > 0xFFFF) ? "n6Nnn" : "n9";

  $data = $proto_header->header() .
          pack($fmt, 0x0003,                  # Num atoms

                     HTLC_DATA_OPTION,        # Atom type
                     0x0002,                  # Atom length
                     0x0001,                  # Atom data

                     HTLC_DATA_PCHAT_REF,     # Atom type
                     ($ref > 0xFFFF) ? 4 : 2, # Atom length
                     $ref,                    # Atom value

                     HTLC_DATA_CHAT,          # Atom type
                     length($message)) .      # Atom length
          $message;                           # Atom data

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    return(1);
  }
  else { return }
}

sub pchat
{
  my($self, $ref, @message) = @_;

  my($message) = join('', @message);

  $message =~ s/\n/@{[HTLC_NEWLINE]}/osg;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened() && defined($ref));

  my($data);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_CHAT);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len((($ref > 0xFFFF) ? 14 : 12) + length($message));
  $proto_header->len2($proto_header->len);

  my($fmt) = ($ref > 0xFFFF) ? "n3Nnn" : "n6";

  $data = $proto_header->header() .
          pack($fmt, 0x0002,                  # Num atoms

                     HTLC_DATA_PCHAT_REF,     # Atom type
                     ($ref > 0xFFFF) ? 4 : 2, # Atom length
                     $ref,                    # Atom value

                     HTLC_DATA_CHAT,          # Atom type
                     length($message)) .      # Atom length
          $message;                           # Atom data

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    return(1);
  }
  else { return }
}

sub pchat_leave
{
  my($self, $ref) = @_;

  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened() && defined($ref));

  my($data);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_PCHAT_CLOSE);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len(($ref > 0xFFFF) ? 10 : 8);
  $proto_header->len2($proto_header->len);

  my($fmt) = ($ref > 0xFFFF) ? "n3N" : "n4";

  $data = $proto_header->header() .
          pack($fmt, 0x0001,                  # Num atoms

                     HTLC_DATA_PCHAT_REF,     # Atom type
                     ($ref > 0xFFFF) ? 4 : 2, # Atom length
                     $ref);                   # Atom value

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    delete $self->{'PCHATS'}->{$ref};
    return(1);
  }
  else { return }
}

sub pchat_subject
{
  my($self, $ref, @subject) = @_;
  
  my($server) = $self->{'SERVER'} or croak "Not connected to a server";
  return  unless($server->opened() && defined($ref));

  my($subject) = join('', @subject);

  my($data);

  my($proto_header) = new Net::Hotline::Protocol::Header;

  $proto_header->type(HTLC_HDR_PCHAT_SUBJECT);
  $proto_header->seq($self->_next_seqnum());
  $proto_header->task(0x00000000);
  $proto_header->len((($ref > 0xFFFF) ? 14 : 12) + length($subject));
  $proto_header->len2($proto_header->len);

  my($fmt) = ($ref > 0xFFFF) ? "n3Nnn" : "n6";

  $data = $proto_header->header() .
          pack($fmt, 0x0002,                  # Num atoms

                     HTLC_DATA_PCHAT_REF,     # Atom type
                     ($ref > 0xFFFF) ? 4 : 2, # Atom length
                     $ref,                    # Atom value

                     HTLC_DATA_PCHAT_SUBJECT, # Atom type
                     length($subject)) .      # Atom length
          $subject;                           # Atom value

  _debug(_hexdump($data));

  if(_hlc_write($self, $server, \$data, length($data)))
  {
    return(1);
  }
  else { return }
}

1;