/*
    slforum version 1.x - Simple chat room usable via telnet.
    Copyright (C) 1998-2006 Jeremy D. Monin, Dan Chin, Matt Lochansky
	(forum@shadowlands.org).
    Based on jdkchat version 1.3, J.D.Koftinoff Software, Ltd. (GPL'd)

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

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

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

/* reminder for easy time stamps localized to user's timezone:
	\201 (ascii 129) = HH:MM
	\202 (ascii 130) = DD MMM HH:MM
*/

#include "world.h"
#include "slforum.h"
#include "slforumcmd.h"

/* user login tracking - set in main() */
extern const char *wtmpfname;

static const char *backspace="\010 \010";
static int bs_len = strlen(backspace);

/* for authlocal and max-remote-logins checking */
static struct in_addr localhost_ip;


const char *SLForumUser::welcome = LOGIN_HELLO_MSG_TXT;

const char *SLForumUser::commands =
"\rForum commands available:      (type \177help <commandname> for details)\r\n"
#ifdef ALLOW_LOCKOUT
"   Not Being Here:  \177away   \177exit   \177quit    \177lock\r\n"
#else
"   Not Being Here:  \177away   \177exit   \177quit\r\n"
#endif
"   Who _Is_ Here?:  \177who    \177here   \177users   \177showaway\r\n"
"   Whispering:      \177to     \177also   \177beep    \177\177         \177printwho  \177ban\r\n"
"   Strangers:       \177kick   \177accept\r\n"
#ifdef ALLOW_TOPICS
"   Checking Stuff:  \177time  \177date \177msgchk \177macro    \177topic \177comment \177prevtopic\r\n"
"   Utility:         \177clear \177set  \177ignore \177unignore \177listen \177me \177timer \177roll\r\n";
#else
"   Checking Stuff:  \177time  \177date \177msgchk \177roll     \177macro  \177me\r\n"
"   Utility:         \177clear \177set  \177ignore \177unignore \177listen \177timer\r\n";
#endif


/* original help text -------
"Commands available:\r\n"
"   %who    list all users\r\n"
"   %exit   exit chat room\r\n"
"   %away [message]  mark yourself as away from the keyboard\r\n"
"   %to [username or 'all']  send your messages to one person\r\n"
"   %time   display current time\r\n"
"   %clear  clear the screen\r\n"
"   %msgchk check if you have mail\r\n"
"   %local  toggle local echo mode for your telnet session\r\n";
--------- */

const int SLForumUser::CONNECT_TIMEOUT = 60;  // seconds



/** see slforumcmd.cc for implementation of individual commands;
 *  this lists them all in a structured way, and also sets the
 * 'default' command.
 */

#define DFLT_CMD_XPAN	"to"	// 19990130.1615

// cmds affected by topic
// !topic
// !comment
// !prevtopic
// !set (some pref vals)

// max # args that we'll parse and pass 
#define SLF_MAX_ARGC  16	/* max # args to an slfcommand */

#ifdef ALLOW_TOPICS
#define _NUM_TOPIC_CMDS 3
#else
#define _NUM_TOPIC_CMDS 0
#endif

#ifdef ALLOW_LOCKOUT
#define _NUM_LOCKOUT_CMD 1
#else
#define _NUM_LOCKOUT_CMD 0
#endif

#ifdef ALLOW_MACROS
#define _NUM_MACRO_CMD 1
#else
#define _NUM_MACRO_CMD 0
#endif

#define NUM_USERCMDS (36 + _NUM_LOCKOUT_CMD + _NUM_TOPIC_CMDS + _NUM_MACRO_CMD)


/** command name must be no more than 16 chars long.
 *  Otherwise, partial-match (for example) would truncate it
 *  and fail to find it.  (this table added 19990126.2100)
 *
 *  HEY!  Remember to add your command in the proper position in
 *        this table, alphabetically speaking.
 *
 * types for command args:
      typekey	descr		field or meaning of slmparam structure
	V	(none)		none; pointer is NULL (must be last param)
	C	commandname	cmd (an slf_cmd_desc *)
	U	username	u   (an slforumuser *)
	A	username or 'all'	u (a *, or SLF_USER_ALL for all)
	W	word		strval (a char *)
	I	integer		intval (an int)
	*	rest of line	strval (a char *)
	{|}	specific string (one of several alternatives)	strval (char*)
 *
 *  max of 16 args at the moment.  Precede optional args with ? .
 *  Follow a repeatable arg with + (for example, !beep is U+, to
 *  beep no users or several users.)   NOTE: only the last argument
 *  can be repeatable.
 */

/* static */ const int SLForumUser::c_toall_idx = 0;

/* static */ const slf_cmd_desc SLForumUser::usercmds[] = {
  { // this one should be the same as the default command char,
    // so doing 2 of those in a row will do it.  Make sure c_toall_idx
    // points to the correct index in the table for this entry.
    // (See comments for find_cmd_index for why this must be so.)
    //
    "!",        SLForumCmd::c_toall,   "*",	"[message]",
    "Shortcut: Sends message to all." },
  { "13",	SLForumCmd::c_rot13,   "*",	"message",
    "Rot-13 encrypts the message and replaces the input line with it" },
  { "8ball",    SLForumCmd::c_8ball,   "?*",    "[question]",
    "Ask the Eight Ball something" },
  { "?",	SLForumCmd::c_help,	"?C",	"[cmdname]",
	"Get detailed help information" },
  { "accept",	SLForumCmd::c_accept,	"U",	"<guestusername>",
	"Let a waiting guest into the chatroom" },
  { "admin",	SLForumCmd::c_admin,	"?W?*",	"(many args)",
	"Administrative use; \177admin for details" },
  { "alarm",	SLForumCmd::c_timer,	"?W?*",	"<time> [public] [<reason>]",
	"Finally, you can remember to go to class" },
  { "also",	SLForumCmd::c_also,	"A?*",	"<username> [<message>]",
	"Whisper privately to an additional person" },
  { "away",	SLForumCmd::c_away,	"?*",	"[<message>]",
	"Mark yourself as away from the keyboard" },
  { "ban",	SLForumCmd::c_ban,	"*",
	"list or clear or [add] username ...",
	"Edit the list of users who can't whisper to you" },
  { "beep",	SLForumCmd::c_beep,	"U+",	"<username> ...",
	"Get a user's attention" },
  { "bye",	SLForumCmd::c_exit,	NULL,	NULL,
	"Exit the chat room" },
  { "clear",	SLForumCmd::c_clear,	NULL,	NULL,
	"Clear the screen" },
#ifdef ALLOW_TOPICS
  { "comment",	SLForumCmd::c_comment,	"?*",	"[<message>]",
        "Set a comment to the topic" },
#endif
  { "date",	SLForumCmd::c_time,	NULL,	NULL,
	"Display the current date/time (to you only)" },
  { "exit",	SLForumCmd::c_exit,	NULL,	NULL,
	"Exit the chat room" },
  { "help",	SLForumCmd::c_help,	"?C",	"[<cmdname>]",
	"Get detailed help information" },
  { "here",	SLForumCmd::c_here,	NULL,	NULL,
    "List who's actually here (at most 15 mins idle ('hereidle' setting))" },
  { "ignore",	SLForumCmd::c_ignore,
	"{public|guests|beeps|all|none|spam|rolls|privbeep|privbeeps|movement|moves|chime|chimes}",
	"(type \177ignore help to list the many parameters)",
	"Hide public messages and/or whispers and public messages from guests" },
  { "kick",	SLForumCmd::c_kick,	"U",	"<username>",
	"Eject a guest from the chatroom" },
  { "listen",	SLForumCmd::c_listen,	"*",
	"list or clear or [add] word ...",
	"Edit the list of words to listen to when ignoring public banter" },
#ifdef ALLOW_LOCKOUT
  { "lock",	SLForumCmd::c_lockout,	NULL,	NULL,
	"Lock out the keyboard" },
#endif
#ifdef ALLOW_MACROS
  { "macro",	SLForumCmd::c_macro,	"{add|list}",	"add or list",
	"List or add to your macro list" },
#endif
  { "me",	SLForumCmd::c_emote,	"*",	"<action>",
	"Emote something (\177me shrugs)" },
  { "msgchk",	SLForumCmd::c_msgchk,	NULL,	NULL,
	"Run the 'msgchk' command to check for new mail" },
#ifdef ALLOW_TOPICS
  { "prevtopic", SLForumCmd::c_prevtopic, "?I", "[<topicnum>]",
	"Print recent topics and, optionally, comments to them" },
#endif
  { "printwho", SLForumCmd::c_printwho,	NULL,	NULL,
	"Print out who we're currently \177also-whispering to" },
  { "quit",	SLForumCmd::c_exit,	NULL,	NULL,
	"Exit the chat room" },
  { "roll",     SLForumCmd::c_roll,	"I?I",	"<numdice> [<numsides>]",
	"Rolls <numdice> dice" },
#ifdef ALLOW_TOPICS
  { "set",	SLForumCmd::c_zebra,	"{command|hereidle|autowho|autotopic|local}?*?",	"[<settingname> [<value>]]",
#else
  { "set",	SLForumCmd::c_zebra,	"{command|hereidle|autowho|local}?*?",	"[<settingname> [<value>]]",
#endif
	"Set or list user preferences" },
  { "showaway",	SLForumCmd::c_showaway, NULL,   NULL,
	"List the away messages only (second part of \177who)"},
  { "spam",	SLForumCmd::c_banner,	NULL,	NULL,
	"dum dedum..." },
  { "time",	SLForumCmd::c_time,	NULL,	NULL,
	"Display the current date/time (to you only)" },
  { "timer",     SLForumCmd::c_timer,   "?W?*", "<time> [public] <reason>",
        "Finally, you can remember to go to class" },
  { "to", 	SLForumCmd::c_to,	"A?*",	"<username> [<message>]",
	"Whisper privately to a person, instead of publicly to everyone" },
#ifdef ALLOW_TOPICS
  { "topic",	SLForumCmd::c_topic,	"?*",	"[<topic>]",
	"Set (or view) the Forum topic" },
#endif
  { "unignore", SLForumCmd::c_unignore,
	"{public|guests|beeps|all|none|banner|banners|spam|rolls|privbeep|privbeeps|movement|moves|chime|chimes}",
	"(type \177unignore help to list the many parameters)",
  	"Reverse the effects of the \177ignore command" },
  { "uptime",    SLForumCmd::c_uptime,  NULL,   NULL,
	"Show uptime and boot time for the room" },
  { "users",    SLForumCmd::c_users,    NULL,   NULL,
	"List current occupants, login and idle times for the room" },
  { "version",	SLForumCmd::c_version,	NULL,	NULL,
	 "Prints the version of slforum you're logged into"},
  { "who",	SLForumCmd::c_who,	"U?",	"[<username>]",
	 "List just current users logged in (doesn't display away messages)"}
};




// Don't forget the comma at end of each line, or you'll die;
// two lines without an intervening comma will be merged into
// one string, so you promised the program you have 27 random
// commands in this array but lo! you seem to have 26. -- Jer

const int SLForumUser::NUM_RND_COMMANDS = 28;
const char *SLForumUser::rndCommands[] = {
  "   \177java   check if the coffee machine is empty\r\n",
  "   \177ewok   appear to be speaking through a small, fuzzy creature\r\n",
  "   \177asparagus  look for us in your grocery section\r\n",
  "   \177god    everything you say is Right\r\n",
  "   \177jeopardy  everyone must speak in the form of a question\r\n",
  "   \177scorch lob grenades at other users\r\n",
  "   \177spock  challenge the logic of other users' statements\r\n",
  "   \177cobol  declare other users obsolete\r\n",
  "   \177frisbee  exit the Forum, go outside and play, and come back\r\n",
  "   \177pine   check if you have mail, from a high treetop\r\n",
  "   \177why    list all users' reasons to be here, or null if none\r\n",
  "   \177weight calculate the total weight of all occupants of the Forum\r\n",
  "   \177carl   check if you have a rubber mallet\r\n",
  "   \177sunlight  check if you've left syracuse, ny\r\n",
  "   \177hissy  get really angry over something trivial\r\n",
  "   \177feature  add yet another new feature to Shadowlands Forum\r\n",
  "   \177bug    ask a random user if we're there yet or if dinner's ready\r\n",
  "   \177risk   invade Madagascar\r\n",
  "   \177remote  get farther \177away\r\n",
  "   \177?      are you lost too?\r\n",
  "   \177smite  terminate the target user (for admins only)\r\n",
  "   \177twiddlefingers    port of Java method thumbs.twiddle() into SLForum\r\n",
  "   \177psychic  type what I mean, not what I pressed\r\n",
  "   \177thinggygahootzis  Talk like Al\r\n",
  "   \177stevejobs  Create the reality distortion field\r\n",
  "   \177wetpants  shower random user with random grenades\r\n",
  "   \177cotton  it just socks\r\n",
  "   \177aok     try to take over the Forum and maybe the world\r\n",
};









/* Note this flag isn't part of the class itself (ANSI C++ forbids initializing
 * a non-const static class member or somesuch, why bother)
 */
static bool checked_admin_ss_table = false;



// second param is optional; SLForumRoom uses it to indicate
// you're part of a room when constructed.
SLForumUser::SLForumUser( SLForumRoom *current_room, int idx_in_room=-3 )
{
  localhost_ip.s_addr = htonl(0x7f000001);  // 127.0.0.1

  room = current_room;
  if (idx_in_room != -3)
    my_idx_in_room = idx_in_room;
  else if (room != NULL)
    my_idx_in_room = room->IndexOf (this);
  else
    my_idx_in_room = -1;

  *buf.buffer   = '\0';
  *yankbuf.buffer='\0';
  *name     = '\0';
  *prompt   = '\0';
  *admprompt= '\0';
  pref.listenlist= NULL;
  pref.wblist    = NULL;
#ifdef ALLOW_MACROS
  pref.macrolist = NULL;
#endif
  state     = STATE_NO_CONNECTION;
  prevstate = STATE_NO_CONNECTION;
  user_fd = -1;
  to_bits = from_bits = (userfield_t) 0;
  n_talking_to = TALKING_TO_ALL;

  // make sure all strings are null-terminated, even if
  // a later strncpy runs out of room... (it doesn't copy the
  // terminating \0 if it does)  We can either do this
  // now, or do it after every single strncpy-type operation.
  //
  buf.buffer   [USERBUFSIZ-1]   = '\0';
  name     [USERNAMESIZ-1]  = '\0';
  prompt   [USERPROMPTSIZ-1]= '\0';
  admprompt[USERBUFSIZ-1]   = '\0';
  away     [USERAWAYSIZ-1]  = '\0';
  lastmfrom[USERNAMESIZ-1]  = '\0';
  lastmto  [USERNAMESIZ-1]  = '\0';
  dnsfrom  [DNSFROMSIZ-1]   = '\0';
  longfrom [LONGFROMSIZ-1]  = '\0';
  yankbuf.buffer[USERBUFSIZ-1]  = '\0';
  timer_message[USERAWAYSIZ-1] = '\0';

  // Now init everything else.
  resetFieldsToDiscon();

  /* look for the end-of-table marker for adminInputSSBehavTable;
   * if it's not in the 'right place', we know we've either changed
   * that table or changed enum admin_substate_t, and made a mistake
   * in doing so.
   */
  if (! checked_admin_ss_table)
  {
    if (   ( admInputSSBehavTable[SS_NUM_SUBSTATES].show_input != true )
	|| ( admInputSSBehavTable[SS_NUM_SUBSTATES].nextstate  != ss_unused )
	|| ( admInputSSBehavTable[ss_unused].show_input != false )
	|| ( admInputSSBehavTable[ss_unused].nextstate  != SS_NUM_SUBSTATES ) )
    {
      fprintf(stderr, "\nInternal error - admInputSSBehavTable jives not with enum admin_substate_t.\n");
      fprintf(stderr, "Sorry, this needs to be fixed.  Refusing to run.\n\n");
      exit(1);
    }
    checked_admin_ss_table = true;
  }
}


SLForumUser::~SLForumUser()
{
  // By all rights, the thing to write a logout entry
  // if connected should be here... but it's in the
  // room destructor, because that's where the other
  // log-handling stuff is.  So maybe it does belong
  // there.  But be sure to call it before deleting
  // the user!

	if( user_fd!=-1 )
		close( user_fd );
}

// -- comment out debug protoypes 20020704.1210 after authlocal fixed:
#if 0
// debug prototypes (added 20020514):
static void dumpAuthlFailLogInfo
	(unsigned long loip,
	 unsigned int uport, unsigned int dport);

static int slf_send_file ( int fd, const char *fname );
#endif   // 20020704.1210

// continue with normality:



bool SLForumUser::IsUserAccepted() const
{
  return ((user_fd != -1) &&
    ((state == STATE_GET_LINE) || (state == STATE_LOAD_PREFS)));
}


bool SLForumUser::WasUserManualDiscon() const
{
  if (! WasUserAccepted())
    return false;

  return (substate == 1);
}


bool SLForumUser::WasUserAccepted() const
{
  if ((state == STATE_GET_LINE) || (state == STATE_LOAD_PREFS))
    return true;

  if (state != STATE_NO_CONNECTION)
    return false;

  return ((prevstate == STATE_GET_LINE) || (prevstate == STATE_LOAD_PREFS));
}


bool SLForumUser::isUserConnected() const
{
  return (state != STATE_NO_CONNECTION);
}


bool SLForumUser::isUserAway() const
{
  return ( (state == STATE_GET_LINE) && is_away && (user_fd != -1));
}


bool SLForumUser::isUserGuest() const
{
  return is_guest;
}

int SLForumUser::getIgnoringMask() const
{
  return pref.ignore_mask;
}

bool SLForumUser::IsUserAround() const  // jer 980703.2345
{
  return ((user_fd != -1) &&
	  ((state == STATE_GET_LINE) || (state == STATE_GUEST_CONFIRM)
	   || (state == STATE_FIND_DUPL) || (state == STATE_LOAD_PREFS)));
}

bool SLForumUser::state_is_load_prefs() const
{
	return (state == STATE_LOAD_PREFS);
}

time_t SLForumUser::GetAwayTime() const
{
	return away_at;
}

int SLForumUser::GetFD() const	// returns file descriptor, or -1 if none
{
	return user_fd;
}

const char *SLForumUser::GetName() const
{
	return name;	
}

// jer 19990113.1412
int SLForumUser::idx_in_room() const {  return my_idx_in_room; }


const char *SLForumUser::getAwayMessage() const { return away; }

/** Mark us as away, format the away message into a suitable form,
 *  and return the formatted message for announcing to the room.
 *  This does NOT actually announce it!  I suspect that if you actually
 *  want to set a user away, calling slforumcmd::c_away is the best way
 *  to do it at the moment.
 */
const char* SLForumUser::setAway (const char *msg)
{
  is_away = true;
  away_at = time(NULL);

  // format msg and copy into the 'away' field;
  // null-terminate if it would've exceeded field size.
  // Either "user: awaymsg" or "user"; this is intended
  // to be preceded by "AWAY: " in a sysmessage:
  // room->BroadCast
  //   ( SLForumUser::MSG_SYS, "AWAY", NULL, user_i->getAwayMessage() );

  if (-1 == snprintf
      (away, USERAWAYSIZ, "%9s%c %s",
       name,
       (msg != NULL ? ':' : ' '),
       (msg != NULL ? msg : " " ))) {
    away[USERAWAYSIZ - 1] = '\0';
  }
  return away;
}


/** time in seconds since last keypress.. jer 980702.0745 */
int SLForumUser::idleTime() const
{
  time_t now;
  time(&now);
  return (int) (now - lastkey);
}


/** time in seconds since last keypress.. jer 20000201.1310 */
int SLForumUser::idleTime(time_t now) const
{
  return (int) (now - lastkey);
}

bool SLForumUser::isAwayFromIdle() const
{
	return (isUserAway() && (away_at > lastkey));
}

/** time, in time(2) form, of logon. jer 980702.0745 */
time_t SLForumUser::loginTime() const
{
  return loginat;
}


/** time of login, in string form.  jer 980702.1600 */
const char *SLForumUser::loginTimeStr() const
{
  return ctstr;
}


/* where logged in from. jer 980702.0745 
 * Note that if you want the full (non-truncated) version,
 * you should get longfrom.
 */
const char *SLForumUser::loginFrom() const
{
#ifdef SOLARIS_UTMP
  return dnsfrom;
#else
  return uentry.ut_host;
#endif
}


/* where logged in from, truncated. jer 980914.1015 */
const char *SLForumUser::loginFrom16() const
{
  return dnsfrom;
}




/** Try to read one char.  Returns 1 for read, 0 for no input,
 *  or -1 for i/o error (should discon on error).
 */
int SLForumUser::slfgetch(char *ptrc)
{
  if (user_fd == -1)
    return -1;

  int ret = read (user_fd, ptrc, 1 );  // try read one char
  if (ret == -1) {
    if ((errno == EAGAIN) || (errno == EINTR))
      return 0;
    else
      return -1;  // error reading, close socket
  }
  if (ret == 0)
    return -1;	// eof.

  // ret is 1, success.

  DBG(( "Got Char %02x\r\n", ((int)c)&0xff ));
  time(&lastping);

  if (all_keys_reset_idle)
    lastkey = lastping;  // reset idle

  return 1;
}


/* Output a random 'command' to the user
 *
 * jer 980701.1640
 */
void SLForumUser::rndCommand() {
  // int r = abs(rand() % NUM_RND_COMMANDS);

  // Knuth recommended way, using high-order bits and avoiding % pecularities...
  int r =(int) (((double)NUM_RND_COMMANDS)*rand()/(RAND_MAX+1.0));
  uWriteSubst ( rndCommands[r] );
}






/** For one timeslice; handle interaction with this user.
 *
 *  If this user disconnects, clean up (but don't deallocate)
 *  and return false.  Return true to continue connection.
 */
bool SLForumUser::ManageUser()
{
  int ret=0;	// the eventual return value from ManageUser
	
  if ( (user_fd == -1) && (state != STATE_NO_CONNECTION) ) {
    // if a guest disconnects and is then !accepted,
    // this situation comes up.  May happen in other
    // instances too.  This catches them all.

    resetFieldsToDiscon();
    return false;
  }

  if (waittil != 0) {
    if (time(NULL) < waittil)
      return true;  // enforced wait for bad logins; jer 981005.1025
    else
      waittil = 0;	// reset it, done waiting.
  }

  switch( state )
    {
    case STATE_NO_CONNECTION:
      ret = HandleNoConnection();  // establishes initial conn.
      break;
    case STATE_GET_NAME:
      ret = HandleGetName();
      break;
    case STATE_JER_GETPW:
      ret = HandleGetPw();
      break;
    case STATE_GUEST_CONFIRM:
      usleep (1000 * 20);  // 20 ms
      break;  /* just wait, honey, ya can't type yet. */
    case STATE_FIND_DUPL:
      ret = HandleFindDupl();
      break;
    case STATE_GET_LINE:
      if (! is_locked) {
	if (admin_substate == ss_nonadmin)
	  ret = HandleGetLine();
	else
	  ret = HandleAdminLine();
      } else
	ret = HandleLockedLine();
      break;
    case STATE_TEL_CMD:
      ret = HandleTelCmd();
      break;
    case STATE_TEL_OPT:
      ret = HandleTelOpt();
      break;
    default: {
	char buf[80];
	sprintf (buf,
		 "L520: assert failed: user '%s' unknown state (state=%d)",
		 name, state );
	room->Announce( buf );
	ret = -1;
      }
    };

  if (user_fd == -1)  // any errors, go for discon
    ret = -1;
  
  if( ret==-1 )
  {  // disconnect 
    if (! WasUserManualDiscon())
      resetFieldsToDiscon();
  
    return false;
  }
	
  return true;
}


// jer added 20000426.1930
/** Write this but substitute the user's command char for any \177
 *  in the message.  MALLOCS AND THEN FREES A COPY.
 *  If the memory isn't avail. to dup it, it won't substitute.
 */
int SLForumUser::uWriteSubst (const char *text)
{
  char *tcopy = strdup(text);
  if (tcopy == NULL)
    return uWrite (text);
  else {
    int retval;
    char *fnd;  // this code is also in Send. (search for 20000426.1930)
    fnd = tcopy;
    do
    {
      fnd = index(fnd, '\177');
      if (fnd != NULL) {
	*fnd = pref.cmd_char;  // do the substitution
	++fnd;
      }
    } while (fnd != NULL);

    retval = uWrite (tcopy);
    free (tcopy);
    return retval;
  }
}


/** Write a message to this user, raw; no redisplay of prompt, no \r\n at end;
 *  equivalent to uWrite (text, strlen(text));
 *  jer added 980709.1700
 */
int SLForumUser::uWrite (const char *text) {
  return uWrite (text, strlen(text));
}


int	SLForumUser::uWrite (const char *buf, int sz)
{
  if (user_fd == -1)  return -1;

  if (lpwrite(user_fd, buf, sz) < 0) {
    user_fd = -1;
    return -1;
  } else
    return sz;
}




/* return false if "from" isn't banned from whispering, true if they are */
bool SLForumUser::CheckBanned(SLForumUser *from)
{
  if (from == this)
    return false;

  const char *fromname = from->name;

  if ((pref.wblist != NULL) && wbListMatch(fromname))
    {      
      char warnbuf[80], timebuf[16];
      time_t		now = time(NULL);
      writeTime (timebuf, sizeof(timebuf), now);

      sprintf (warnbuf, "%s has tried to whisper to you at %s.",
	       fromname, timebuf);
      Send (warnbuf, 0, false);
      sprintf (warnbuf, "%s has blocked your whisper.", name);
      from->Send (warnbuf, 0, false);

      // un-set talking bits
      from->talkingTo (my_idx_in_room, false);
      talkingFrom (from->my_idx_in_room, false);

      return true;   // banned
    }
  // else
  return false;   // not banned
}


/** Send a line to this user, but if we're on the ban list, don't.
 *  from must be non-null or nothing happens.
 */


/** Send a line to this user, without preceding SLFORUM: etc.
 *  Returns # bytes or -1 if discon.
 *
 *  jer added 981207.0015
 */
int SLForumUser::Send ( const char *text ) {
  return Send ( 0, NULL, NULL, text, 0 );
}


/** Send a system message to this user unless hintbit is set;
 *  If it's not set, send text and set it.
 *  Otherwise, do nothing, return 0.
 *  Normally returns # bytes or -1 if discon.
 *  jer added 980709.1700
 *  19990222.2350: added wants_beep
 *  20000503.0740: added optional msgdescflags param
 */
int SLForumUser::Send
  ( const char *text, int hintbitset, bool wants_beep, 
    int msgdescflags /* =0 */ )
{
  if (getHint(hintbitset))
    return 0;

  int retval = Send
    (MSG_SYS|msgdescflags, SYSMSG_FROM_USERNAME, NULL, text, 0);
#ifdef ALL_MESSAGES_BEEP
  wants_beep = true;
#endif
  if (wants_beep && ! (pref.ignore_mask & MSG_BEEP))
  {
    if (uWrite ("\007", 1) < 1)
      return -1;
  }
  setHint (hintbitset);
  return retval;
}


// jer added 19990607.1600 for c_roll, etc
/** Send a system message with optional msgtype flags to this user.
 *  The msgdescflags should _not_ have MSG_PUBLIC set, in case the
 *  user is ignoring public, unless you want that.
 *  Returns # bytes or -1 if discon.
 */
int SLForumUser::Send (int msgdescflags, const char *text, bool wants_beep)
{
  return Send(SYSMSG_FROM_USERNAME, msgdescflags, text, wants_beep);
}


// jer added 20000815.1640 for private TIMER announcements, which beep.
int SLForumUser::Send
	(const char *fromsysusername,
	 int msgdescflags, const char *text, bool wants_beep)
{
  int retval = Send (msgdescflags | MSG_SYS, fromsysusername, NULL, text);
  if (retval < 0)
    return retval;

#ifdef ALL_MESSAGES_BEEP
  wants_beep = true;
#endif

  if (wants_beep &&
      ( (! (pref.ignore_mask & MSG_BEEP)) ||
	((msgdescflags & MSG_P_BEEP) && ! (pref.ignore_mask & MSG_P_BEEP))))
  {
    if (uWrite ("\007", 1) < 0)
      return -1;
  }
  return retval;
}


/** Send a non-system message to this user.  If 'to' is NULL, it's a
 *  public message.  Otherwise, the attribution to the left of the colon
 *  will be "<from> to <to>".
 *  (But if 'from' is NULL, no formatting is done; 'text' is sent out
 *   as is, starting at the left edge of the screen.)
 *  Returns # bytes or -1 if discon.
 */
int SLForumUser::Send( int msgdescflags, const char *from,
		       const char *to,   const char *text)
{
  return Send (msgdescflags, from, to, text, 0);
}




/////  *  THIS IS THE ACTUAL Send METHOD THAT ALL THE OTHERS CALL!  /////

/** Send a message to this user.  If 'to' is NULL, it's a public message.
 *  Otherwise, the attribution to the left of the colon will be
 *  "<from> to <to>".  The 'fromtominwid' specifies a minimum width
 *  (in characters) for the attribution field; it will be padded w/ spaces
 *  if need be.  Note that fromtominwid is ignored for public messages
 *  (when 'to' is NULL).
 *
 *  If 'from' is NULL, just spit out "text" as is, starting at the left edge
 *  of the screen.
 *
 *  Returns # bytes or -1 if discon.
 *
 *  THIS IS THE ACTUAL Send METHOD THAT ALL THE OTHERS CALL!
 */
int SLForumUser::Send( int msgdescflags, const char *from,
		       const char *to,   const char *text,
		       int fromtominwid )
{
    /* first, should we expand a timestamp?  This code shared with 
     * room::logprintf; if you change it here, change it there.
     */
    {
	bool wants_copy = false;
	char *idx;
	idx = index(text, '\201');
	if (idx==NULL)
	{
	    idx = index(text, '\202');
	    if (idx != NULL) wants_copy = true;
	} else
	    wants_copy = true;

	if (wants_copy)
	{
	    time_t now;  now = time(NULL);

	    // copy from 'text' to '_expandbuf' and then re-point
	    // 'text' AT '_expandbuf'.  \201 = HH:MM, \202 = DD MMM HH:MM.
	    // we'll actually print out _expandbuf, so point "text" at it.
	    text = expandTimeTokens (now, text, idx);

	    // (rest of logprintf shared code is in ::expandTimeTokens)
	}
    }  /* timestamp expand code ends */

  // Note that the msgdescflags are all bitwise comparisons.
  // Be very careful and hesitant to change any of their logic.

  // Can't ignore system messages unless they have bits set besides
  // MSG_SYS and MSG_PUBLIC.  This lets us ignore rolls, movement and
  // chimes if desired.

  if ( (msgdescflags & pref.ignore_mask)	// matches ignore mask
       &&
       ( (! (msgdescflags & MSG_SYS))	// either not sys message,
	 				// (can't ignore those)
	 || (msgdescflags & (~ (MSG_SYS | MSG_PUBLIC)) // or has other bits set
     )))
  {
    // Looks like we want to ignore this line.

    // Before turning up our nose, search the listen list in case the message
    // contains a keyword we're listening for, ONLY IF IT's PUBLIC and
    // we're ignoring public.

    if ((msgdescflags & MSG_PUBLIC) && (pref.ignore_mask & MSG_PUBLIC))
    {
      char *mend;   // end of match
      char *maymatch = listenListMatch (text, &mend);    // 19990215.1900
      if (maymatch != NULL)
      {
	char ech = ' ';
	if (mend != NULL) {
	  ech = *mend;
	  *mend = '\0';	// mark end of match so only match word prints out;
			// this temporarily obscures the rest of the line.
	}

	pref.ignore_mask ^= MSG_PUBLIC;

	{
	    char mbuf[160];
	    if (-1 == snprintf
		( mbuf, 160, "\t-- we heard the word '%s' --", maymatch))
		mbuf[159] = '\0';
	    Send ( mbuf, 0, true );
	}

	if (mend != NULL)
	  *mend = ech;	// remove the temporary \0 so whole line will print
      }
    }  // end if-public-check-listenlist

    // so do we still want to ignore it?

    if (msgdescflags & pref.ignore_mask)	// matches ignore mask
      return 0;

  }  // end if-may-ignore

  if (user_fd == -1)	// discon.
    return -1;

  char buf[128];
  bool blank_from = false;
  buf[127] = '\0';
      
  if ((from == NULL) || (*from == '\0')) {

    snprintf (buf, sizeof(buf)-1, "%s\r\n", text);
    blank_from = true;
    lastmfrom[0] = '\0';

  } else {

#define TOFROMBUFSIZ (USERNAMESIZ*2 + 4)
    char frombuf[TOFROMBUFSIZ];  // write attribution into here
    frombuf[0] = '\0';
    frombuf[TOFROMBUFSIZ-1] = '\0';

    // 19990613.0400; not just MSG_PUBLIC anymore,
    // now all but sys messages get checked for
    // if their attribution needs updating, or
    // just a blank attrib (meaning, same as last message)
    // will do.
    if (! (msgdescflags & MSG_SYS)) {
      
      if ((lastm_rpt >= 20) ||
	  strncmp(lastmfrom, from, USERNAMESIZ-1) ||
	  ((to == NULL) && (*lastmto != '\0')) ||
	  ((to != NULL) &&
	   strncmp(lastmto,  to,   USERNAMESIZ-1)) ) {  // 19990215.0730
	
	// from a different user than the last line;
	// remember who this is from, for next time.
	
	// First, check to see if the conversation merely
	// switched directions.  19990616.1500
	if ((to != NULL) &&
	    (0 == strcmp (lastmfrom, to)) &&
	    (0 == strcmp (lastmto, from)) ) {
	  
	  // first, whitespace padding
	  memset (frombuf, ' ', TOFROMBUFSIZ-1);
	  
	  // which is us?  Find out and write it out.
	  if (0 == strcmp (from, name))
	    // we're the from, they're the to
	    sprintf (frombuf + strlen(from) + 1,
		     "to %s", to );
	  else
	    // they're the to, we're the from
	    sprintf (frombuf + strlen(to) - 1,
		     "from %s", from);
	  
	  strncpy(lastmfrom, from, USERNAMESIZ-1);
	  from = frombuf;

	  // pad the buffer w/ spaces if need be...
	  if (fromtominwid >= TOFROMBUFSIZ)
	    fromtominwid = TOFROMBUFSIZ - 1;

	  {
	    int fromsz = strlen(frombuf);
	    if (fromsz < fromtominwid) {
	      char *pad = frombuf + fromsz;
	      while (fromsz < fromtominwid) {
		*pad = ' ';
		++pad;
		++fromsz;
	      }
	      *pad = '\0';
	    }
	  }
	  
	} else {
	  // new conversation (different to/from)
	  
	  strncpy(lastmfrom, from, USERNAMESIZ-1);

	  if (to != NULL) {
	    snprintf (frombuf, TOFROMBUFSIZ-1, "%s to %s",
		      from, to);
	    from = frombuf;
	    {
	      // pad out w/ spaces if need be...
	      int fromsz = strlen(frombuf);
	      if (fromsz < fromtominwid) {
		char *pad = frombuf + fromsz;
		while (fromsz < fromtominwid) {
		  *pad = ' ';
		  ++pad;
		  ++fromsz;
		}
		*pad = '\0';
	      }
	    }
	  }
	}
	  
	lastm_rpt = 0;
	if (to != NULL)
	  strncpy(lastmto, to, USERNAMESIZ-1);
	else
	  *lastmto = '\0';
	
      } else {
	
	// pad w/ blanks to the same size as the from/to
	// attribution that _would_ have been here, had we
	// not been padding with blanks instead.
	int fromsz;
	if (from != NULL)
	  fromsz = strlen(from);
	else
	  fromsz = 1;
	
	if (to != NULL) // private, length must account for " to " in there
	  fromsz += strlen(to) + 4;

	if (fromsz < fromtominwid)
	  fromsz = fromtominwid;
	if (fromsz > (TOFROMBUFSIZ-1))
	  fromsz = TOFROMBUFSIZ-1;
	blank_from = true;
	
	memset (frombuf, ' ', fromsz);
	frombuf[fromsz] = '\0';
	from = frombuf;
	++lastm_rpt;
#undef TOFROMBUFSIZ
      }
      
    } else {	// system message: throw away "last" info
      *lastmfrom = '\0';
      *lastmto   = '\0';
      lastm_rpt = 0;
    }
    
    //////// Write it to the buffer ////////

    snprintf( buf, sizeof(buf)-1, "%10s%c %s\r\n",
	      from, (blank_from ? ' ' : ':'), text );

  }   // end huge if-from-not-null


  // one more thing to check before sending it out:
  // if it's a system message, any ascii 127 (octal 177)
  // characters are changed to this user's command char.
  if (msgdescflags & MSG_SYS)
  {
    char *fnd;	// this code is also in uWriteSubst. (search for 20000426.1930)
    fnd = buf;
    do
    {
      fnd = index(fnd, '\177');
      if (fnd != NULL) {
	*fnd = pref.cmd_char;  // do the substitution
	++fnd;
      }
    } while (fnd != NULL);
  }




  //////// Finally write it out to the client ////////

  ClearEntryLine();       // ... since it'll come back on a different line

  int len=strlen(buf);
  if( uWrite(buf)!=len )  // sudden disconnect?
    return -1;	

#ifdef ALL_MESSAGES_BEEP
  if (1 != uWrite ("\007", 1))
    return -1;
#endif

  RedisplayEntryLine();
  return len;
}


/** This clears the line onscreen; it does _not_ do anything
 *  about the contents of the buffer.
 */
void 	SLForumUser::ClearEntryLine()
{
	const char *buf = "\r\033[K\r";	
	if( user_fd!=-1 )
	  uWrite (buf);
}


/** Display prompt and input buffer.
 *  The prompt is the contents of the buffer prompt[]
 *  unless:
 *  - admprompt != empty
 *  - and either of:
 *    - admin substate is >= ss_notadmin_FIRST, which ignores cmd_char
 *    - buffer[0] is not cmd_char
 *  If these conditions are met, admprompt is the prompt.
 */
void 	SLForumUser::RedisplayEntryLine()
{
  if (running_cmd)
    return;

	if( user_fd!=-1)
	{
		ClearEntryLine();
		if (! is_locked)
		{
		  bool use_adm_prompt
		     = ((admprompt[0] != '\0') && (
			     (admin_substate >= ss_notadmin_FIRST)
			     || (buf.buffer[0] != pref.cmd_char)));

		  /* use_adm_prompt : We use the admin prompt if 1) it's set,
		   * and 2) we aren't escaped to a !command from a menu.  Some
		   * substates can't escape to a !command, so check our
		   * substate first.
		   * This condition MUST be equal to that at the code in
		   * HandleAdminLine that handles keystrokes in admin submodes,
		   * that's where it would escape to a cmd or not.
		   * (in slforumuser-adminsubstate.inc.cc)
		   * Search for 20020706.0930 to find the code.
		   */

		  int testv;	/* testv is TEMPORARY CODE added 20000615.0200 */
		  if (use_adm_prompt)
		      testv = uWrite (admprompt);
		  else
		      testv = uWrite (prompt);

		  if (testv == -1)
		  {
		      // IO error or disconnect.  Let's just return, since
		      // testv obviously isn't the prompt length this time. :)
		      return;
		  }

		  if (admin_substate == ss_macro_expand_readkey)
		      return;  // hide input line in that case

		  if (show_input ||
		      ((admin_substate != ss_nonadmin)
		       && (buf.buffer[0] == pref.cmd_char)))
		  {
		      if (! buf.macro_input)
		      {
			  if (NULL == strchr(buf.buffer, IAC))
			      uWrite (buf.buffer);
			  else
			      for (char *c = buf.buffer; *c != '\0'; ++c)
				  if (*c != (char) IAC)
				      uWrite ( c, 1 );
				  else
				      uWrite ( telnet_double_iac, 2 );
		      }
		      else
		      {
			  for (char *c = buf.buffer; *c != '\0'; ++c)
			      if (*c != (char) IAC)
				  uWrite ( keyDisplayString (*c) );
			      else
				  uWrite ( telnet_double_iac, 2 );
		      }
		  }

		  if (testv != promptlen)
		  {
		    room->logprintf   // can't Announce(),
				      // that would include doing prompts
		      ("L1011: assert failed: prompt len vs actual len vary\n");
		    room->logprintf
		      ("L1011: expected %d, found %d\n",
		       promptlen, testv);
		    room->logprintf
		      ("L1011: admpromptlen=%d, realpromptlen=%d, admin?=%c\n",
		       admpromptlen, realpromptlen,
		       (((admprompt[0]!='\0')&&(buf.buffer[0]!=pref.cmd_char))
			? 'Y' : 'N'));
		    room->logprintf
		      ("L1011: user prompt is '%s', admin prompt is '%s'\n",
		       prompt, admprompt);
		    /* We aren't in devel, not approprate to just die here
		     * room->room_cleanup();
		     * exit(1);
		     */
		  }

		} else {
		  uWrite(" ------------------------- Locked (Display Only) -------------------------");
		}
	}
}

void 	SLForumUser::ClearBuffer()
{
	buf.buffer[0] = '\0';
	buf.cur_pos = 0;
	buf.cur_len = 0;
	for (int tabi = (TABMASK_BUFSIZE - 1); tabi >= 0; --tabi)
	    buf.tabmask[tabi] = 0;
}

void	SLForumUser::VisuallyClearBuffer()
{
  if ((buf.cur_len > 0) && (! is_locked))
  {
    if (! pref.local_mode ) {
      for ( ; buf.cur_len > 0; --buf.cur_len) {
	if (uWrite (backspace, bs_len) < 0)
	  return;
      }
    } else {
      buf.cur_len = 0;
    }
    buf.cur_pos = 0;
    buf.buffer[0]='\0';
    for (int tabi = (TABMASK_BUFSIZE - 1); tabi >= 0; --tabi)
	buf.tabmask[tabi] = 0;
  }

  if ( pref.local_mode )
    RedisplayEntryLine();
}


// for HandleNoConnection
void SLForumUser::tryPrintLoginBanner()
{
  char fnamebuf[PATH_MAX];
  fnamebuf[PATH_MAX-1] ='\0';

  // first, slfhomedir
  if (slfhomedir != NULL)
  {
    snprintf(fnamebuf, PATH_MAX-1, "%s%c%s", slfhomedir, PATH_SEP, 
	     LOGIN_ISSUE_BANNER_FILENAME);

    if (lpcat(user_fd, fnamebuf) != -1)  // slfhomedir/issue.forum
      return;

    snprintf(fnamebuf, PATH_MAX-1, "%s%c%s", slfhomedir, PATH_SEP, 
	     LOGIN_ISSUE_BANNER_FALLBACK);

    if (lpcat(user_fd, fnamebuf) != -1)  // slfhomedir/issue.net
      return;
  }

  // now, fallback dir

  snprintf(fnamebuf, PATH_MAX-1, "%s%c%s", LOGIN_ISSUE_BANNER_FALLBACK_DIR,
	   PATH_SEP, LOGIN_ISSUE_BANNER_FILENAME);

  if (lpcat(user_fd, fnamebuf) != -1)	  // /etc/issue.forum
    return;

  snprintf(fnamebuf, PATH_MAX-1, "%s%c%s", LOGIN_ISSUE_BANNER_FALLBACK_DIR,
	   PATH_SEP, LOGIN_ISSUE_BANNER_FALLBACK);

  lpcat(user_fd, fnamebuf);	  // /etc/issue.net
}



// establish initial connection
int	SLForumUser::HandleNoConnection()
{
	socket_arg3_t len = (socket_arg3_t) sizeof( user_address );
	user_fd = accept
	  ( room->GetServerFD(), (struct sockaddr *)&user_address, &len );
	
	// note: accept returns -1 if there is no connection or if error.
	
	if (user_fd == -1)
	  state = STATE_NO_CONNECTION;

	if( user_fd!=-1 )
	{
	  state = STATE_SETUP_CONN;  // so we know this slot's taken
	  --(room->empty_slots);  // take up a slot

	  room->logprintf("(logging in, slot=%d)\n", my_idx_in_room);
	  struct hostent* phaddr;
	  //len = sizeof(user_address.sin_addr);/* 980914.0840: adding this so
						/* len is right size (otherwise
						 * it's as assigned above).
						 * Now gethostbyaddr succeeds.
						 */
	  phaddr = gethostbyaddr ((char *) &(user_address.sin_addr),
				  sizeof(user_address.sin_addr), AF_INET);
#ifndef SOLARIS_UTMP
	  if (phaddr == NULL)
#endif
	    strncpy (prompt, inet_ntoa (user_address.sin_addr),
		     USERPROMPTSIZ-1);
#ifndef SOLARIS_UTMP
	  else
	    strncpy (prompt, phaddr->h_name, USERPROMPTSIZ-1);
#endif

	  loginfromip = user_address.sin_addr;

	  // prompt settings need to be done right...
	  // we only use it as a temp. buffer here,
	  // but if someone else says something and the program iterates
	  // through all users and notices this one's promptlen doesn't
	  // match, it'll be annoyed.
	  // We should just use a temp buffer instead. :)
	  promptlen = realpromptlen = strlen(prompt);

	  initlogentry (&uentry, prompt, user_address.sin_addr.s_addr);
	  strncpy (dnsfrom, prompt, DNSFROMSIZ-1);
#ifdef SOLARIS_UTMP
	  room->logprintf("(login: from is %s)\n", dnsfrom);
	  strncpy (longfrom, prompt, LONGFROMSIZ-1);
#else
	  room->logprintf("(login: from is %s)\n", uentry.ut_host);
	  strncpy (longfrom, uentry.ut_host, LONGFROMSIZ-1);
#endif
	  pref.local_mode=false;
	  time (&loginat);	// login time set (for connect timeout)
	  lastping = lastkey = loginat;	// reset idle time to 0 ;)
	  strcpy (name, "(asking name)");  // set this so name isn't invalid

	  uWrite( telnet_init );
	  uWrite( telnet_local_off );
	  
	  uid_t authenticated = 0;
	  if (user_address.sin_addr.s_addr == localhost_ip.s_addr ) {
	      // they're local, so skip the check against non-empty slots
	      // and if authlocal support is here, check that too:
#ifdef USE_AUTHLOCAL
	      authenticated = getUID
		  ( localhost_ip.s_addr, ntohs(user_address.sin_port),
		    room->getServerPort() );

	      if (0 == authenticated)
	      {
		      // JDM 20020514: print out /proc/net/tcp if no workie
		      // -- commented out 20020704.1210, after authlocal fixed
		/*
		      long loip;
		      int uport, dport;
		      loip = (unsigned long) (localhost_ip.s_addr);
		      uport = (unsigned int) (ntohs(user_address.sin_port));
		      dport = (unsigned int) (room->getServerPort()) ;
		      room->logprintf ("L1437: authlocal(localip=%lu, uport=%u (0x%x), dport=%u) fail.\nSee bottom of /tmp/slf-log-authfail.txt\n",
				       loip, uport, uport, dport );
		      dumpAuthlFailLogInfo (loip, uport, dport);
		*/
	      }
#endif
	  } else {
	      if (room->empty_slots < MIN_NON_REMOTE) {
		  Send ("Sorry, no open slots for non-localhost users.");
		  kick ("Sorry, no open slots for non-localhost users.");
		  return 0;
	      } 
	  }

	  // check authenticated here...
	  // skip all that and log 'em in if so.
	  // Note that acceptAndWelcomeUser(uid) automatically
	  // returns false if uid==0.
	  if (authenticated != 0) {
	    struct slf_userinfo *udata;
	    
	    udata = slf_getpwuid(authenticated);
	    if (udata == NULL)
	      authenticated = 0;
	    else {
		setupRealUserInfo (udata);  // listen list, prefs, is_admin
	    }
	  }
	  
	  if (authenticated != 0) {
	      // skip the password state
	      setState(STATE_FIND_DUPL);
	      return HandleFindDupl();
	  } else {
	      // do the usual login procedure.
	      // Need to ask for password.

	      tryPrintLoginBanner();
	      
#if (ALLOW_GUESTS)
	      if (room->getPrintGuestHowtoMsg())
		Send (GUEST_HOWTO_TXT);    // jer 980911.2300
	        // the message is defined in slfprefs.h
#endif
	      if (user_fd != -1) {
		  setPrompt( "login: " );
		  setState (STATE_GET_NAME);
		  sked_alarm (CONNECT_TIMEOUT);
		  ClearBuffer();
		  RedisplayEntryLine();
	      } else
	          resetFieldsToDiscon();
	  }
	}
	return 0;	// yes, regardless.  We handle our own errors
			// if they don't connect successfully.
}


int	SLForumUser::HandleGetName()
{
	int ret=ManageInputBuffer();
	if (ret==1) {
	  if ( buf.buffer[0] == '\004') // ctrl-d
	    return -1;
	  else if( strlen( buf.buffer )<1 )  // too short
	    {
	      ClearBuffer();
	      RedisplayEntryLine();
	    }
	  else if (strncasecmp("!exit", buf.buffer, 5) == 0)
	    return -1;
	  else
	    {
	      static char c = '\n';
	      if (uWrite (&c, 1 ) < 0)
		return -1;
	      
	      strncpy( name, buf.buffer, USERNAMESIZ-1 );
	      if ( (ALLOW_GUESTS) &&
		   ( strcasecmp (name, "guest") == 0)) {

		if (room->empty_slots < MIN_NON_GUEST) {
		  Send ("Sorry, no open slots for guests.");
		  kick ("Sorry, no open slots for guests.");
		  return 0;
		}

		setPrompt ( "Guest name: " );
		is_guest = true;
	      } else {
		setPrompt ( "Password: " );
		is_guest = false;
	      }
	      setState (STATE_JER_GETPW, true);  /* keep substate to track
						  * bad logins here
						  */
	      room->logprintf("logging in %s (from %s)\n", name,
#ifdef SOLARIS_UTMP
			      dnsfrom
#else
			      uentry.ut_host
#endif
			      );
	      ClearBuffer();
	      RedisplayEntryLine();
	    }
	}
	if (ret < 0)
	  return -1;
	else
	  return 0;
}


int	SLForumUser::HandleGetPw()
{
  // for normal users, is_guest = false, we want to hide input (pw)
  // for guests, is_guest = true, we want to show input (guest name)
  int ret=ManageInputBuffer();
  if (ret<0)
    return ret;

  if( ret==1 )
    {
      struct slf_userinfo *ppw;
      
      if (buf.buffer[0] == '\004')
	return -1;
      
      if (strlen(buf.buffer) == 0)
	ppw = NULL;
      else
	if (is_guest) {
	  // guest: check for unique name!
	  if ( validateAndDowncaseName (buf.buffer) == NULL )
	    ppw = NULL;	// failed validation
	  else {
	    ppw = slf_getpwnam (buf.buffer);
	    if (ppw == NULL)
	      // wasn't a real user, but could still be a guest
	      // currently logged in;
	      // if this is so, make ppw non-null so we fail validation.
	      // (TODO TODO TODO cleaner way to do this?)
	      ppw = (struct slf_userinfo *) room->GetUserByName(buf.buffer);

#if USE_VIRTUSERS
	    if (ppw == NULL)
	      // they could _still_ be a virtuser, so check that too
	      // (20000815.1650)
	      ppw = slf_virtuser_get_info(buf.buffer, NULL, false);
#endif
	    
	    if (ppw != NULL)
	      ppw = NULL;  // can't be an already logged-in guest!
	    else if (strcmp (buf.buffer, "all") == 0)
	      ppw = NULL;  // can't call yourself 'all'!
	    else if (strcmp (buf.buffer, "guest") == 0)
	      ppw = NULL;  // can't call yourself 'guest'
	    else if (strcmp (buf.buffer, SYSNAME_LCASE) == 0)
	      ppw = NULL;  // can't call yourself the chat program!
	    else
	      ppw = (struct slf_userinfo *) room; // just so non-null, passes validation
	  }
	} else {
	  /* is_guest isn't set, so try to validate name and password. */
	  ppw = validate (name, buf.buffer, longfrom);
	  memset (buf.buffer, 0, sizeof(buf.buffer));  // zero out pw
	}
      
      if (ppw == NULL) {
	if (is_guest)
	  // uses uwrite to overwrite the line
	  uWrite ("\r\nThat name is taken, please try another\r\n");
	else {
	  ClearEntryLine();
	  uWrite ("Login incorrect\r\n");
	  /* enforced wait; jer 981005.1025 */
	  if (substate > 0) {
	    time (&waittil);
	    waittil += substate;
	  }
	  substate = (substate << 1) + 1;
	}
	
	if (! is_guest) {
	  // not guest, back to login prompt (guest stays at name prompt)
	  setPrompt( "login: ");
	  setState (STATE_GET_NAME, true);  // keep substate for enforced wait
	}
	ClearBuffer();
	RedisplayEntryLine();

      } else {

	if (is_guest) {

	  bool aa = room->getAutoAccept();

	  time (&loginat);  // reset timeout counter

	  char s[128];
	  strncpy (name, buf.buffer, 9);   name[9] = '\0';

	  if (! aa) {
	    setState(STATE_GUEST_CONFIRM);
	  }

	  room->AnnounceTimestamp (loginat, MSG_MOVES, false);

	  sprintf (s,
		   (aa
		    ? "Auto-accepting the guest %s (from %s)."
		    : "A guest named %s (from %s) requests entry."),
		   name,
#ifdef SOLARIS_UTMP
		   dnsfrom
#else
		   uentry.ut_host
#endif
		   );
	  room->Announce (MSG_MOVES, s, true);

	  if (aa)
	    acceptAndWelcomeUser();	// autoaccept
	  else
	  {
	    sprintf (s, "Use the command '\177accept %s' to let them in.", name );
	    room->Announce (s, HINT_ACCEPT, false);
	    sked_alarm (CONNECT_TIMEOUT);

	    uWrite ("\r\n");  // newline to not overwrite the guest name prompt
	    ClearBuffer();
	    setPrompt ("Please wait while someone already here lets you in...");
	    RedisplayEntryLine();
	  }
	} else {  // is a user, not a guest
	  setupRealUserInfo (ppw);  /* listen list, prefs, and is_admin */

	  // find duplicates of ourself
	  setState(STATE_FIND_DUPL);
	  ret = HandleFindDupl();
	}
      }
    }
  if( ret<0 )
    {
      return -1;	
    }
  return 0;
}


/** 980809.1020: added this state, to check for duplicate logins **/
int	SLForumUser::HandleFindDupl()
{
  if (user_fd == -1)
    return -1;

  if (statedata != NULL) {
    // we already found a duplicate, and have been waiting for user
    // input about what to do about it (kick or not).

    char c;
    int ret = slfgetch (&c);  // try read one (re-signed) char
    if (ret < 1)
      return ret;
    if (c == 0)
      return 0;

    if (c == '\004')
      return -1;
      
    if (c == (char) IAC) {
	awaitTelCmd();	// telnet escape (for connecting from localhost)
	return 0;
    }
    if ((c == 'Y') || (c == 'y') || (c == 13))
    {
	{		// jer added announce 980830.2000
	  char buffer[64];
	  if (-1 == snprintf
	      (buffer, 63,
	       "%s has kicked an idle self out the door.",
	       name)) {
	    buffer[63] = '\0';
	  }
	  room->Announce (buffer);
	}
	((SLForumUser *) statedata)->kick("more-idle self kicked");
	statedata = NULL;
      } else if ((c == 'N') || (c == 'n') || (c == 27)) {
	// don't kick, but go on
	statedata = NULL;
      } else {
	// redisplay and retry
	if (c >= 32)
	  return Send ("Please respond with Y or N.");
	else
	  return 0;
      }
  }

  
  // look for a duplicate, idle user
  SLForumUser *retu;
  {
    int usrnum = (int) substate;
    retu = room->GetUserIncremental (name, usrnum, false);
    substate = usrnum;
  }
  if (retu == this)  // oops, found ourselves.  Look again.
  {
    int usrnum = (int) substate;
    retu = room->GetUserIncremental (name, usrnum, false);
    substate = usrnum;
  }

  if (retu == NULL) {
    // Done looking!  Enter the room.
    acceptAndWelcomeUser();
  } else {
     // if someone's whispering to it, whisper to us
     // (we won't actually _hear_ the whisper til we get in-room,
     //  but this prevents the user annoyance of "but i was just
     //  talking to him a second ago, i've got to set that up again??")

    retu->talkToMeToo (this);

     // Ask to dispose of it
    char buf[64];   buf[63] = '\0';
    statedata = retu;
    snprintf (buf, 63,
	      "Kick duplicate from %s (idle %d min)? ",
	      retu->loginFrom(),
	      (retu->idleTime()+30) / 60);
    setPrompt (buf);
    RedisplayEntryLine();
  }
  return 0;
}


/** Read the next key from a user in the forum, and handle if
 *  line needs wrapping or it's a command.
 *
 *  Return 0 to continue connection, -1 if user's disconnected.
 *
 *  980524.1800: added word wrap on long lines (when len=65)
 *  980709.1715: fixed word wrap on long lines with long prompt
 *  981005.1930: un-!away only for <cr>...
 *	changed (is_away && (ret != -1) && cur_pos )
 *	to	(is_away && (ret == 1) && (*buffer != '!') )
 * 19990402: fixes from dschin for whitespace
 * 19990607.0245: un-away for line wrap too...
 *	changed (ret == 1)
 *	to	((ret == 1) || (cur_pos >= (78-promptlen)))
 *	This is 78 instead of 79 so we never have to worry about a key being
 *	pressed in the last column of the last line.
 * 19990717.2305: yet more un-away fixing.
 *	changed (*buffer != '!')
 *	to      ((*buffer != '!') || (*(buffer+1) == ' '))
 *	Now, a line starting with "! " will properly return you from away.
 */
int	SLForumUser::HandleGetLine()
{
  return ManageChangedInputBuffer( ManageInputBuffer() );
}


/** Returns same values as Handle* methods
 * (-1 for discon, 0 for ok)
 *  ret is the return value from ManageInputBuffer.
 */
int	SLForumUser::ManageChangedInputBuffer (int ret)
{
  if (ret<0)
    return ret;  // disconnected
  if (buf.buffer[0] == '\004') {
    resetFieldsToDiscon(true);  // exited via ^D^D
    return -1;
  }

  // check for brake
  if( (ret==1) || (buf.cur_len >= (78-promptlen)) ) {
    if (! checkMaxLinesBrake()) {
      // too quick!  quietly eat them up.  19990922.2100
      VisuallyClearBuffer();
      if (user_fd != -1) return 0; else return -1;
    }
  }


  /* Handle user returning (or not, as the case may be) */

  if (is_away
      && ((ret==1) || (buf.cur_len >= (78-promptlen)))
      && (to_bits == (userfield_t) 0 ))
  {
      /* We may return.  Check that it's not a command. */
      char *bc = buf.buffer;
      while (*bc == ' ')
	  ++bc;
      if ( (buf.buffer[0] == '\0')
	   || ((*bc != pref.cmd_char) && (*bc != pref.cmd_char_alt))
	   || (*(bc+1) == ' '))
	   ManageUserReturn (ret);
  }
  

  /* do we have a full line?   If so, parse or display it. */

  if( (ret==1) || (buf.cur_len >= (78-promptlen)) )
  {
      ManageFullLine(ret);
  }


  /* check for disconnect or I/O errors, and we're done. */

  if( ret<0 )        return -1;
  if( user_fd == -1) return -1;
  return 0;

}  // ManageChangedInputBuffer


void SLForumUser::ManageUserReturn (int ret)
{
    time_t now = time(NULL);
    char s[48];
    lastkey = lastping;
    *away = '\0';
    strcpy (s, name );
    strcat (s, " has returned at \201." );
    room->Announce ( MSG_MOVES, s, true );
    is_away = false;

    // list users if !autowho set -- dschin 19990922.1145
    if( (pref.maxAwayIdle>0) && ((now - away_at)>=pref.maxAwayIdle) ) {
      room->who_sort_is_valid = false;  // trigger re-sort
      room->ListUsers(this);
      room->who_sort_is_valid = false;  // mark as invalid
    }

#ifdef ALLOW_TOPICS
    // list topics
    if (room->topicsAreOn()
	&& (pref.maxTopicIdle>0) && ((now - away_at)>=pref.maxTopicIdle) )
      SLForumCmd::c_topic(room,this,0,NULL);
#endif

}  // ManageUserReturn



void SLForumUser::ManageFullLine (int ret)
{
    bool wrap_cmdargs = (ret==0);

   /* About this var: If we have trailing whitespace, the line doesn't end
    * in the middle of a word, therefore no need to wrap anything from the
    * user to the next line -- thus, we set ret=1 below if this is the case
    * (pretending the user hit return after typing their line), so the
    * line-wrap code won't try to do anything.  But, we do still want to
    * wrap any command arguments, so
    * !to <user> <message> will come out with
    * !to <user> starting the new line.
    * So we set wrap_cmdargs up here, to remember whether the line really
    * does wrap or not, since we can't check the "ret" var for this.
    */

    // 19990606.0130: blank-lines brake code
    {
      register char *p;
      for (p = buf.buffer ; *p == ' '; ++p)  // skip past leading blanks
	;

      if (*p != '\0') {
	blanklines = 0;    // non-blank char on line
      } else
      {
	++blanklines;      // this line is entirely blank.
       
	if (blanklines > MAX_BLANK_LINES)
	{
	  ClearBuffer();   // ignore it, we've had too many in a row 
	  RedisplayEntryLine();
	  return;
	}

	if (p != buf.buffer)
	{
	  // This line is entirely whitespace.
	  // Trim the whitespace (and update cursor pos) and it'll be
	  // sent out in the normal send-out-the-line code below.
	  VisuallyClearBuffer();
	  if (user_fd == -1)
	    return;  // discon or I/O error
	}
      }
    }  // blank-lines brake ends


    /* for commands _only_, or reflow mode, trim leading whitespace */

    if (! cmd_char_disabled)
    {
      bool is_alt_char;
      register int wscount=0;  // Whitespace counter (19990402)
      
      while (buf.buffer[wscount]==' ')
	++wscount;

      // buf.buffer[wscount] now points at the first non-blank char.

      // check for alt cmd char
      is_alt_char =
	((pref.cmd_char_alt != '\0') && (buf.buffer[wscount] == pref.cmd_char_alt));

      if (                             // for commands or reflow, trim space.
	  reflow_mode ||
	  ((buf.buffer[wscount] == pref.cmd_char) || is_alt_char)  // cmd char first
	   && (buf.buffer[wscount+1] != ' ')   // and not (cmd char then blank)
	   && (buf.buffer[wscount+1] != '\0')) // and not (cmd char by itself)
      {
	// if alt cmd char, switch it to real cmd char (20000815.1630)
	// (this makes line-wrap code's job's easier)
	if (is_alt_char)
	{
	  buf.buffer[wscount] = pref.cmd_char;      // replace it
	  if ((buf.buffer[wscount+1] == pref.cmd_char_alt) &&
	      ( (buf.buffer[wscount+2] == ' ')
		|| (buf.buffer[wscount+2] == '\0')))
	    buf.buffer[wscount+1] = pref.cmd_char;  // fix a double alt cmd char
	}

	// trim leading whitespace
	memmove (buf.buffer, buf.buffer+wscount, buf.cur_pos - wscount + 1);
	// Figure out the correct new cur_pos, cur_len
	buf.cur_len = stringDisplayLength(buf.buffer, &buf.cur_pos);

	// redisplay if not command (this line might stay in buffer if reflow)
	if (buf.buffer[0] != pref.cmd_char)
		RedisplayEntryLine();
      }
    }
    
    // for _both_, trim trailing whitespace
    {
      register char *ewsp;
      ewsp = buf.buffer + buf.cur_pos - 1;  // points at last char in the buffer

      if (*ewsp == ' ')
      {
	ret = 1;	// we had trailing whitespace, therefore the line
			// doesn't end in the middle of a word, therefore
			// no need to wrap anything to the next line.
	  		// Thus we pretend the user hit return.
			// Note that the wrap_cmdargs bool above will ensure
			// that !to, etc, will still be filled in on the new
			// line, by knowing they didn't really hit return.
	
	while ((ewsp != buf.buffer) && (*ewsp == ' '))
	  --ewsp;	// slide left to find start of trailing whitespace
	
	if ((ewsp != buf.buffer)   // if true, loop exited 'cuz found non-' ' char
	    || (*ewsp != ' ')) // if true, loop exited 'cuz ewsp at start of
	{                      // buffer (and buffer isn't entirely whitespace)
	    // we're at a non-whitespace char; move to right (back onto one).
	    ++ewsp;
	}

	*ewsp = '\0';       // truncate line here...
	                    // and update len and pos vars:
	buf.cur_len = stringDisplayLength (buf.buffer, &buf.cur_pos);
      }
    }  // trim trailing whitespace ends


    if ( (! cmd_char_disabled)
	 && ( buf.buffer[0]==pref.cmd_char ) && ( buf.buffer[1] != '\0')
	 && ( buf.buffer[1] != ' ') )
    {
      // it's a command, parse and execute it.  Word-wrap anything
      // that would get chopped off if it ran past the end of line.
      bool was_wrapped = (ret == 0);
      char linewrap[USERBUFSIZ];
      linewrap[0] = '\0';
      if (was_wrapped)
      {
	// find the wrap part, and, well, wrap it
	// if and only if it isn't _also_ the first space
	// (e.g., don't leave a command without a parameter.)
	char *lastsp = rindex (buf.buffer, ' ');
	char *firstsp = index (buf.buffer, ' ');
	if ((lastsp != NULL) &&
	    (firstsp != lastsp) &&
	    (lastsp - buf.buffer < buf.cur_pos))
	{
	  char *wrpt;         /* wrap point */
	  wrpt = lastsp;
	  // eat additional spaces
	  while ((wrpt != buf.buffer) && (*wrpt == ' '))
	    --wrpt;
	  // back to the first of several contig. spaces
	  if (*wrpt != ' ')
	    ++wrpt;
	  // now get the wrap line..
	  strcpy (linewrap, lastsp + 1);
	  
	  // and chop off that from current line
	  *wrpt = '\0';
	}
      }

      running_cmd = true;
      ret = HandleUserCommand( wrap_cmdargs );
      running_cmd = false;

      if (ret != -1)
      {
	// no disconnect, time to redisplay line

	if (*linewrap != '\0')
	  strncat (buf.buffer, linewrap, USERBUFSIZ - buf.cur_pos - promptlen - 1);

	// if we're in an admin substate, 
	// it will redisplay the input line with the admin prompt,
	// so make sure show_input is set right
	if (admin_substate != ss_nonadmin)
	  show_input = admInputSSBehavTable[(int)admin_substate].show_input;

	// Figure out the correct new cur_pos, cur_len
	buf.cur_len = stringDisplayLength(buf.buffer, &buf.cur_pos);

	// display it
	RedisplayEntryLine();

	// and done.
	ret = 0;
      }
      
    } else {  // wasn't a command, just a line of text.
	      // possibly wrap it, and send it out.

	    if (reflow_mode && (buf.cur_pos > 0))
	    {
		    // only follow all this if
		    // the line's not blank.
		    if ( (! lastkey_was_cr) && (buf.cur_len < 77-promptlen))
		    {
			    // They hit enter and the line isn't full yet.
			    // Insert a space instead of enter.
			    buf.buffer[buf.cur_pos++] = ' ';
			    buf.buffer[buf.cur_pos] = '\0';
			    ++buf.cur_len;
			    uWrite (" ");
			    lastkey_was_cr = true;  // remember we had CR.
			    return;  // Keep it in buffer, don't send out yet.
		    } else {   // Two enters in a row. (note we didn't return)
			      // A totally full line will also get you here.
			    lastkey_was_cr = false;  // un-remember.
		    }
	    }

      int tmp_tabmask[TABMASK_BUFSIZE];  // tabmask for line
      char tmp[USERBUFSIZ];    // the non-wrapped part of the line
      strcpy( tmp, buf.buffer );   // (assume it's the whole buffer for now)
      for (int tabi = (TABMASK_BUFSIZE - 1); tabi >= 0; --tabi)	
	  tmp_tabmask[tabi] = buf.tabmask[tabi];
	  
      if (ret != 1) {
				/* look for word wrappable */
	int i, j, L;
	char *cp;  // points to the wrap character within tmp
	  
	cp = rindex (tmp, ' ');
	if (cp == NULL)
	  cp = rindex (tmp, '-');
	if (cp != NULL) {
	  i = cp - tmp; /* index of space/hyphen */
	  L = strlen (tmp);
	  j = L - i - 1;
	  if (j > 0)
	  {
	    ++cp;
				/* wrapped part stays in buffer */
	    buf.buffer[buf.cur_pos] = '\0';  // clear after it
	    strncpy ( buf.buffer, cp, j+1 );  // copy it back into buffer from tmp
	    
				/* and other part is truncated after i */
	    *cp = '\0';
	    buf.cur_pos = j;   // set cursor @ end of the new input line
	    buf.buffer[buf.cur_pos] = '\0';  // clear after it
	    buf.cur_len = stringDisplayLength (buf.buffer);

	  } else
				/* happens to have space as last col */
	    ClearBuffer();
	} else
	  /* fills the whole line, thus nothing for next line */
	  ClearBuffer();
      } else
	/* not wrapped */
	ClearBuffer();

      _sendOut (tmp, tmp_tabmask);  // send out the old msg line (wrapped part)
      if (reflow_mode && (! wrap_cmdargs) && (blanklines == 0))
	      _sendOut ("", tmp_tabmask);  // blank line after para in reflow.
    }
}  // ManageFullLine





/** Read a key in locked state 
 */
int	SLForumUser::HandleLockedLine()
{
  //  show_input = false;   // just in case; some interaction with the admin
			// system is sometimes showing input here.
  int ret = ManageInputBuffer();
  if (ret<0)
      return ret;
	
  /* 981005.1930 here! */
  if (buf.buffer[0] == '\004') {
    resetFieldsToDiscon(true);
    return -1;
  }
  if (ret!=0)
    try_unlock_me();

  if (user_fd!=-1)
    return 0;
  else
    return -1;
    
}  /* handlelockedline */




////////////////////////////////////////
// user admin substates
//
////////////////////////////////////////


#include "slforumuser-adminsubstate.inc.cc"

/* contents of that include file:
  static  const admin_input_substate_behav_t
	SLForumUser::admInputSSBehavTable[] 

  void SLForumUser::setAdmSubstate(admin_substate_t subst)

  admin_substate_t SLForumUser::getAdmSubstate() const

  int	SLForumUser::HandleAdminLine()
*/



bool SLForumUser::CheckForMissedDiscon ()
{
	if ((state != STATE_NO_CONNECTION) && (user_fd == -1))
	{
	      // They were disconnected somewhere along the line, but missed.
	      // Fix that.  (Added 20011012.1430)
	      char buf[80];  buf[79] = '\0';
	      const char *leavemsg = "%s has lost connection at \201.";
	      resetFieldsToDiscon (false);
	      sprintf( buf, leavemsg,
		       GetName(),
		       loginTimeStr() );
	      writeLogoutEntry(wtmpfname);
	      room->Announce( SLForumUser::MSG_MOVES, buf );
	      return true;
      } else
	      return false;
}



////////////////////////////////////////
// ManageInputBuffer stuff
//
////////////////////////////////////////


// jer aded 981207.0015
/** util for ManageInputBuffer: From cursor position, extract
 *  a partial match (that is, the last word on the input line,
 *  starting after a space or begin-of-line.)  partial should
 *  point to a char buf, and set psize to the size of that buf;
 *  on return, psize will be the length of the partial-match string
 *  found, or 0 if none found. (That is, if we're at beginning of line.)
 */
void SLForumUser::entry_extract_partial (char *partial, int &psize) const
{
  const char *pc = buf.buffer + buf.cur_pos - 1;
  int maxp = psize - 1;
  psize = 0;


  if (buf.cur_len == 0) {
    *partial = '\0';
    psize = 0;
    return;
  }

  while ( (pc >= buf.buffer) && (*pc != ' ') && (psize < maxp) ) {
    --pc;
    ++psize;
  }
  ++pc;  // move past the ' ' (or to start of buffer)
  strncpy (partial, pc, psize);
  partial[psize] = '\0';
}




/*  This expands a tab into spaces.
 *  Jer expanded from code within HandleGetLine 19990728.0100
 *  so we can call it both before and after we decide if
 *  we just need a tab, or if we're trying to expand a
 *  command or username.  (This fixes a bug wherein pasted text
 *  containing tabs would be tab-expanded, which of course would
 *  fail, and give the user pasting it an error message _without_
 *  expanding the tab to spaces as it should.)
 *
 *  Look for calls to this function within HandleGetLine;
 *  only the first one was there originally... the rest were added
 *  because they should have been there and weren't.
 */
void SLForumUser::expand_one_tab() {
  int thispos = buf.cur_pos;

  do {
    buf.buffer[buf.cur_pos]=' ';
    ++buf.cur_pos;  ++buf.cur_len;
    tabmask_set_bit_for_pos(buf.cur_pos);  // to remember that we hit tab, not sp.
  }
  while( (buf.cur_pos < USERBUFSIZ) 
	 && (buf.cur_len <= 78 - promptlen)
	 && (buf.cur_len%TAB_WIDTH!=0) );

  buf.buffer[buf.cur_pos]='\0';
  thispos = buf.cur_pos - thispos;  // thispos = # added

  // now move over that many spaces on user's terminal
  // by displaying the part of 'buf.buffer' that we've just added
  uWrite (& (buf.buffer[buf.cur_pos - thispos]));
}



/** this backspaces one space, or one tab, depending on the tab mask.
 *  If the current char isn't whitespace, it just backs over that one char.
 *  Does so on screen and in buffer.  jer 19990729.1200
 */
void SLForumUser::back_one_tab()
{
  if (buf.cur_pos == 0)
    return;

  bool just_one_char;
  if (buf.buffer[buf.cur_pos-1] != ' ')
      // not whitespace
      just_one_char = true;
  else
      // check the tabmask.
      just_one_char = (0 == (tabmask_get_bit_for_pos(buf.tabmask,buf.cur_pos)));

  if (just_one_char) {  // We're not erasing a tab, so
                        // just erase one char and return.
    --buf.cur_pos;
    if (!pref.local_mode)
    {
	int chlen;
	if (buf.macro_input)
	    chlen = keyDisplayLength(buf.buffer[buf.cur_pos]);
	else
	    chlen = 1;
	buf.cur_len -= chlen;
	if (show_input)
	    for ( ; chlen > 0; --chlen)
		uWrite (backspace, bs_len);
    }
    else {
	if (show_input) {
	    if (buf.macro_input)
		buf.cur_len -= keyDisplayLength(buf.buffer[buf.cur_pos]);
	    else
		buf.cur_len--;
	    RedisplayEntryLine();
	}
	else
	    --buf.cur_len;
    }
    buf.buffer[buf.cur_pos] = '\0';
    return;
  }

  /* back through a tab: */

  int tabdiff = buf.cur_len % TAB_WIDTH;
  if (tabdiff == 0)
  {
    tabdiff = TAB_WIDTH;
    if (tabdiff > buf.cur_len)  tabdiff = buf.cur_len;
  }

  while ( (tabdiff != 0) &&
	  (0 != tabmask_get_bit_for_pos(buf.tabmask, buf.cur_pos)) )
  {
    tabmask_clr_bit_for_pos(buf.cur_pos);
    --buf.cur_pos;
    if (buf.buffer[buf.cur_pos] != ' ')
    {               // we've found non-whitespace, so move back right :)
	++buf.cur_pos;
	break;      // DONE, exit loop
    }
    int chlen;
    if (buf.macro_input)
	chlen = keyDisplayLength(buf.buffer[buf.cur_pos]);
    else
	chlen = 1;
    buf.cur_len -= chlen;
    tabdiff -= chlen;
    if ((!pref.local_mode) && show_input)
	for ( ; chlen > 0; --chlen)
	    uWrite(backspace, bs_len);
  }

  buf.buffer[buf.cur_pos] = '\0';
  if (pref.local_mode && show_input)
    RedisplayEntryLine();
}


void SLForumUser::pasteIntoBuffer
	(const char *paste, int paste_strlen, int paste_displen,
	 bool paste_macro_input, int* paste_tabmask /* =NULL */ )
{
  if (paste_displen == 0)
    return;

  if (buf.macro_input != paste_macro_input)  // needs recalc
      paste_displen = stringDisplayLength(paste);

  // does it fit?
  if (paste_displen + buf.cur_len <= 78 - promptlen)
  {
    strcpy (buf.buffer+buf.cur_pos, paste);
    if (paste_tabmask != NULL)
    {                                   /* copy the tabmask: */
	int srcidx = 0, destidx = buf.cur_pos;
	while (srcidx <= paste_strlen)
	{
	    if (tabmask_get_bit_for_pos(paste_tabmask, srcidx))
		tabmask_set_bit_for_pos(destidx);
	    ++srcidx;
	    ++destidx;
	}
    }
    buf.cur_pos += paste_strlen;
    buf.cur_len += paste_displen;
    if (show_input)
	if (buf.macro_input)
	    for ( ; *paste != '\0'; ++paste)
		uWrite (keyDisplayString(*paste));
	else
	    uWrite (paste);
  } else {
      // whole thing won't fit, so we'll try one char at a time
      int srcidx=0;  // for tabmask; equiv. of cur_pos within the string
      int chlen;     // display length of current char's keystring
      while (paste_strlen>0)
      {
	  if (show_input)
	      chlen = keyDisplayLength( *paste );
	  else
	      chlen = 1;
	  if ( (buf.cur_len + chlen) <= (78-promptlen) )
	  {
	      buf.buffer[buf.cur_pos] = *paste;
	      buf.cur_len += chlen;
	      buf.cur_pos++;
	      --paste_strlen;
	      ++paste;   // try the next char

	      if (paste_tabmask != NULL)
	      {                                /* copy a bit of the tabmask: */
		  if (tabmask_get_bit_for_pos(paste_tabmask,srcidx))
		      tabmask_set_bit_for_pos(buf.cur_pos);
		  ++srcidx;
	      }
	  } else
	      paste_strlen = 0;   // time to stop
      }
      buf.buffer[buf.cur_pos] = '\0';
      if (show_input)
	  RedisplayEntryLine();
  }
}



/* This gets one character into the buffer.
   // returns -1 on error
   // returns 0 if no full line received yet
   // returns 1 if a full line received (CR);
   // remember to remove any return or line feeds and always maintain
   //	a null terminated string.
   // handles backspace, delete, ctrl-U, ctrl-W, ctrl-D, tab
   //   980524.1500: added ^U support
   // 19990811.1545: added ctrl-k, ctrl-y yankbuffer
   // 20000321:      added ctrl-p, ctrl-n (prev line, prev heard public line)
   // 20000430:      added ctrl-o (command char on-off)
   // NOTE: high-bit chars require unsigned chars :)
   //	Use -funsigned-char when compiling.

   // 20000627:    changed ctrl key handler to a switch
   //	and made sure it handled the admin-substate-escape properly,
   //	including resetting the prompt if they delete that cmd char and thus
   //	are back at an admin prompt.
*/
int	SLForumUser::ManageInputBuffer()
{	
  char c;
  int ret = slfgetch(&c);
  if (ret<1)
    return ret;

  return ManageInputNewChar(c);
}




/** used by ManageInputBuffer, and by
 *  the handler for the admin substate,
 *  since there's an escape from that substate
 *  back to 'standard' STATE_GET_LINE.
 *  Also, in ss_macro_addedit_readline, lets you hit ctrl-q to quote ctrl chars
 *  Returns -1 for error, 0 for ok, 1 for <CR>
 */
int	SLForumUser::ManageInputNewChar
	(char c, bool handle_telnet_iac /* =true */ )
{
  if (user_fd == -1) return -1;
  if (c == 0)        return  0;

  if (substate_tracks_dblchar && (c != state_d_char))
  {
	  // If we're looking for two of a row for a char (^D^D or ESC ESC),
	  // and we get any other key, unset that var.      (20020803.0030)
	  substate_tracks_dblchar = false;
  }

  if (c == '\r')     return  1;  // if it is CR then return 1 to signal that.

  if (running_macro) handle_telnet_iac = false;  // un-set that flag

  if ((c == (char) IAC) && handle_telnet_iac)
  {
    awaitTelCmd();	// telnet escape
    return (user_fd == -1 ? -1 : 0);
  }

  if (((state != STATE_GET_LINE) && (state != STATE_TEL_CMD))
      || (admin_substate != ss_macro_addedit_ctrlq))
  {
    if( c >= 0x80 )
    {
	  #ifdef SLF_ALLOW_HIGHBITS
	    // use our locale to determine what's ok:
	    if ( iscntrl(c))
	      return 0;			// ISO high-bit ctrl characters
	  				// (sorry windows, i follow standards)
	    if ( isspace(c))
	      c = ' ';		// makes parsing for whitespace MUCH simpler.
	  #else
	    return 0;	// original behavior. (prior to 1.9.4-devel13)
	  #endif
    }
  }

  if (reflow_mode)
  {
	  if (! isspace(c))
		  lastkey_was_cr = false;     // need to know wasn't CR
	  else if
		  ( (buf.cur_pos >= 1 ) && (buf.buffer[buf.cur_pos-1] == ' '))
		  return 0;  // ignore more than 1 space in reflow mode.
  }
		  
	

  // Before anything which could possibly erase the line,
  // we need to check if we're in an admin substate, but
  // escaped to user command mode.  (Because if the line
  // gets entirely erased, we need to go back to the admin
  // substate after resetting the prompt, etc)

  bool admuserescape = ((buf.cur_len>0)
			&& (admin_substate != ss_nonadmin)
			&& (buf.buffer[0] == pref.cmd_char));

  // now look for ctrl chars.  There is a check for user_fd==-1
  // right after this, so less need to check within the switch cases.
  // All non-printing chars are either handled or ignored, and we return.

  if ((! isprint(c)) && (c != (char) 128) &&
      (c < (char) (128+32)) &&   // all high-bit chars fail isprint(c)
      ( (! buf.macro_input) || (c != (char) 9) )) // macro_input tabs are literal
  {
      int retval = 0;
      ManageInputNewCtrlChar(c, retval, admuserescape);
      if (retval != 0)    // 1 == return, -1 == error
	  return retval;

      // first, since ctrl keys (^W, ^K, ^U etc)
      // can be used to erase parts of the buffer,
      // we should check to see if they were in the
      // admin substate's escape-to-forum-command line,
      // and erased the cmd char at left; if they did,
      // we need to re-set the promptlen and put the
      // proper prompt onscreen. (20000623.1915, 20000627.2030)

      if (admuserescape && (buf.buffer[0] != pref.cmd_char))
      {
	  promptlen = admpromptlen;          // set prompt length
	  show_input = admInputSSBehavTable [ (int)admin_substate ].show_input;
	  RedisplayEntryLine();   // will redisplay with admin prompt; was
		       	  // showing the "normal" user prompt a second ago
      }
      
      // anyway, if it's a ctrl char, we certainly don't want to
      // stick it in the buffer, so return now.

      if (user_fd != -1)
	return 0;
      else
	return -1;
  }

  //////////// end of ctrl key interpretation ////////////

  // put it in buffer and echo it

  int keyslen;
  if (buf.macro_input)
      keyslen = keyDisplayLength(c);
  else
      keyslen = 1;

  if( buf.cur_len+keyslen < (80 - promptlen) )
  {
      buf.buffer[buf.cur_pos]   = c;
      buf.buffer[++buf.cur_pos] = '\0';
      buf.cur_len += keyslen;
	      
      if( !pref.local_mode && show_input )
      {
	  if (c != (char) IAC)
	      if (buf.macro_input)
		  uWrite( keyDisplayString (c), keyslen );
	      else
		  uWrite ( &c, 1 );
	  else		     
	      uWrite ( telnet_double_iac, 2 );
      }
  } else {
    // buffer's full; beep to the user. Only one line at a time allowed!
	      c=(char) 7;
	      uWrite( (char *) (&c), 1 );
	      if( pref.local_mode && show_input)
		  RedisplayEntryLine();				
  }

  if (user_fd == -1)
      return -1;
  else
      return 0;
}


/** used by ManageInputNewChar: try to handle this control char. (includes ESC)
 *  Return false if it wasn't handled, true if it was; also may set user_fd=-1
 *  and retval = -1.
 *
 *  'retval' is ManageInputNewChar's retval; this is because
 *  some places in here need to set it to 1 (indicate CR)
 */
bool SLForumUser::ManageInputNewCtrlChar
	(char c, int &retval, bool admuserescape)
{

    switch(c)
    {
    case 21: {		  // jer: if ^U, erase line. (see ^K)

	    if (!show_input)
	      ClearBuffer();
	    else
	      VisuallyClearBuffer();

	    if (admuserescape)
	    {
		// re-put the cmd char
		buf.buffer[0] = pref.cmd_char;
		buf.cur_pos = 1;
		buf.cur_len = 1;
		if (show_input)
		  uWrite(&pref.cmd_char, 1);
	    }
	    break;
        }

    case 23: {		// jer: if ^W, erase word.  980910.0930

	    if (buf.cur_pos > 0)
	    {
	      char cc = buf.buffer[buf.cur_pos - 1];
	      char fc = cc;  /* 'first' char; for '...' erasing
			      * (a line of several nonalphanum chars,
			      *  such as ----- or ...., will be
			      *  erased as a single word, instead of
			      *  one at a time.)
			      */
	      if ((cc == ' ') && (buf.cur_pos > 1))
	      {
		fc = buf.buffer[buf.cur_pos - 2];
		cc = fc;  /* If we're just to the right of a space,
			   * we want to erase the space _and_ the
			   * word to its left... this ensures we
			   * hit ^W thrice to erase 3 words.
			   */
	      }
	      while ((buf.cur_pos > 0) && 
		     ( ( cc == fc ) ||
		       (isalnum(fc) && isalnum(cc))))
	      {
		  --buf.cur_pos;
		  int slen;
		  if (buf.macro_input)
		      slen = keyDisplayLength(buf.buffer[buf.cur_pos]);
		  else
		      slen = 1;
		  buf.cur_len -= slen;

		  if (show_input) 
		      for ( ; slen > 0; --slen)
			  uWrite (backspace, bs_len);

		  cc = buf.buffer[buf.cur_pos - 1];
	      }
	      buf.buffer[buf.cur_pos] = '\0';
	      if ( pref.local_mode && show_input )
		  RedisplayEntryLine();
	    }

	    if (admuserescape && (buf.cur_pos==0))
	    {
		// re-put the cmd char if we killed whole line
		buf.buffer[0] = pref.cmd_char;
		buf.cur_pos=1;  buf.cur_len=1;
		if (show_input)
		    uWrite(&pref.cmd_char, 1);
	    }
	    break;
       }
	  
  // if ^D twice, logout.
  // if ^D once, note current time and print a message
  // about hitting it twice to logout...
  case 4:
      {
	      if (substate_tracks_dblchar &&
		  ((state_d_char == (char) 4)) &&
		  ((time(NULL) - substate) < 3))
	      {
		buf.buffer[0] = '\004';
		retval = 1;  /* signal "hit enter key" */		
	      } else {
		Send ("\t-- hit ^D again within 1 second to log out. --");
		time (&substate);
		state_d_char = (char) 4;
		substate_tracks_dblchar = true;
	      }
	      break;
      }

  // 19990923.2105: "tab" now makes a more sophisticated check
  // to see if it's a command or not.  For historical comments about
  // this, see old versions' slforumuser.cc at this point in the
  // file; I cleaned out the history of this feature.
	  
  case 9:
  {			// check if we should just expand tab to spaces:
			// is there a cmd char?
      char *cmdchar;
      if ((buf.cur_pos == 0) || (buf.buffer[buf.cur_pos-1] == ' '))
	  // empty buffer, or tabbing right after a space
	  cmdchar = NULL;
      else
      {
	  if (! cmd_char_disabled)
	  {
	      // search to find cmdchar's index in line, if any
	      cmdchar = index(buf.buffer, pref.cmd_char);

	      // or, failing that, maybe alt cmd char
	      if ((cmdchar == NULL) && (pref.cmd_char_alt != '\0'))
	      {
		cmdchar = index(buf.buffer, pref.cmd_char_alt);
		if (cmdchar != NULL)
		  *cmdchar = pref.cmd_char;  // switch it to normal cmd char
	      }
	  } else
	      cmdchar = NULL;
      }

      if (cmdchar != NULL)
      {
	  // check to see if cmd_char ('!') is first nonblank char
	  char *spc;
	  spc = cmdchar - 1;
	  while ((spc >= buf.buffer) && (*spc == ' '))
	      --spc;
	  
	  if ((spc >= buf.buffer) && (*spc != ' ')) {
	      cmdchar = NULL;    // non-space char to left of cmdchar
	  }
      }
      
      if (cmdchar == NULL)
      {
	  expand_one_tab();
      } else {
	  
	  // if tab, try to complete a partial match for usernames.
	  // (or command names, at 19990126.2100)
	  // error if none, list if more than one.

	  char partial[18];  // leave room for a space at end
	  int pstlen = 17;
	  // first, find it:
	  entry_extract_partial (partial, pstlen);
	  if (pstlen != 0)
	  {
	      int i = 0;
	      int nfound = 0;

	      // either a cmdname or not:
	      if (partial[0] == pref.cmd_char)
	      {
		  if ((partial[1] == ' ') || (partial[1] == '\0'))
		  {
		      // just plain '!' expands to default
		      // command, which is usually '!to' (19990130.1615)
		      sprintf (partial, "%c%s", pref.cmd_char, DFLT_CMD_XPAN);
		      nfound = 1;
		  } else {
		      i = find_cmd_index (partial + 1, true);
		      if (i == -1)
			  Send ("\t-- no unique completion --");
		      else {
			  partial[0] = pref.cmd_char;
			  strcpy (partial+1, usercmds[i].name);
			  nfound = 1;
		      }
		  }
	      } else { 	// not cmd, so assume username.
		  // count the possibilities:
		  SLForumUser *u, *lastu = NULL;
		  const char *fyrst = NULL;

		  // find all names but us?
		  const SLForumUser *exceptfor =
		      (pref.tab_expand_incl_self ? NULL : this);

		  while ( (u = room->GetUserIncremental
			   (partial, i, true, exceptfor))
			  != NULL )
		  {
		      if (fyrst == NULL)
		      {
			  fyrst = u->GetName();
			  nfound = 1;
		      } else {	// check for multiple matches
			  if (strcmp (fyrst, u->GetName())) // w/ diff username
			      ++nfound;
		      }
		      lastu = u;
		      if (nfound > 1)
			  break;
		  }  // end while

		  if (nfound == 0)
		  {
		    Send ("\t-- no possible completions --");
		    expand_one_tab();
		    RedisplayEntryLine();
		    
		  } else if (nfound > 1)
		  {
		    ClearEntryLine();
		    uWrite ("\t-- multiple matches: ");
		    int i = 0;
		    const char *lasnam = NULL, *curnam;
		    // print possibilities:

		    while ( (u = room->GetUserIncremental
			     (partial, i, true))
			    != NULL ) 
		    {
		      curnam = u->GetName();
		      if ((lasnam == NULL) || (strcmp(curnam, lasnam))) {
			uWrite ("\t");
			uWrite ( u->GetName() );
		      }
		      lasnam = curnam;
		    }
		    uWrite (" --\r\n");
		    RedisplayEntryLine();

		  } else {
		    
		    // one single match, yay
		    strcpy (partial, lastu->GetName() );

		  }  // end if-whatever-nfound-is
		}  // end if-cmdname-or-name-choice

		if (nfound == 1)
		{	// fill in the completion.
		  strcat (partial, " ");
		  int plen;     // # chars in partial
		  int psiz;     // display length of partial
		  int pstsiz;   // display length of what's already filled in
		  psiz = stringDisplayLength(partial, &plen);
		  pstsiz = stringDisplayLength(buf.buffer+buf.cur_pos-pstlen);

		  if ( (buf.cur_len - pstsiz + psiz) > (79 - promptlen) )
		  {
		    Send ("\t-- too close to end of line --");
		    RedisplayEntryLine();
		  } else if (pstlen != plen)
		  {
		    // 'backspace' over the partial
		    buf.cur_pos -= pstlen;
		    buf.cur_len -= pstsiz;
		    // append the full
		    strcpy (buf.buffer + buf.cur_pos, partial );
		    buf.cur_pos += plen;
		    buf.cur_len += psiz;
		    // fill in the completion onscreen
		    if (buf.macro_input)
			for (char *parchar = partial+pstlen;
			     *parchar != '\0';
			     ++parchar)
			    uWrite (keyDisplayString(*parchar));
		    else
			uWrite ( partial+pstlen , psiz-pstsiz );
		  }
		}
	      } else { // end if-pstlen!=0 (if-is-completion)
			// it isn't a completion, so expand tab to spaces.
		expand_one_tab();
	      }
      }
      break;
  }  // tab (9)


  case 8:   // if it is backspace or delete then do backspace sequence.
  case 0x7f:
      {
	      back_one_tab();
	      break;
      }

	  
  case 11:	// ^K, erase line to yankbuffer (see ^U)
      {
	      if (buf.cur_pos > 0) {
		  strcpy (yankbuf.buffer, buf.buffer);
		  yankbuf.cur_pos = buf.cur_pos;
		  yankbuf.cur_len = buf.cur_len;
		  yankbuf.macro_input = buf.macro_input;
		  for (int tabi = (TABMASK_BUFSIZE - 1); tabi >= 0; --tabi)
		      yankbuf.tabmask[tabi] = buf.tabmask[tabi];
	      }
	      if (show_input)
		  VisuallyClearBuffer();
	      else
		  ClearBuffer();

	    if (admuserescape)
	    {
		// re-put the cmd char
		buf.buffer[0] = pref.cmd_char;
		buf.cur_pos=1;  buf.cur_len=1;
		if (show_input)
		    uWrite(&pref.cmd_char, 1);
	    }
	    break;
      }

  case 25:	// ^Y, retrieve line from yankbuffer
      {
	      pasteIntoBuffer
		  (yankbuf.buffer, yankbuf.cur_pos, yankbuf.cur_len,
		   yankbuf.macro_input, yankbuf.tabmask);
	      break;
      }

  case 14:	// ^N, retrieve line from lastpubline (note tab mask not used)
      {
	      const char *lastpub;
	      lastpub = room->getLastPublicMsg();
	      pasteIntoBuffer (lastpub, strlen(lastpub),
			       stringDisplayLength(lastpub), buf.macro_input);
	      break;
      }

  case 16:  // ^P, retrieve last sent line
      {
	      pasteIntoBuffer (lastbuf.buffer, strlen(lastbuf.buffer),
			       stringDisplayLength(lastbuf.buffer),
			       buf.macro_input, lastbuf.tabmask);
	      break;
      }

  case 12:  // ^L, clear screen
      {
	      ClearEntryLine();
	      SLForumCmd::c_clear (room, this, 0, NULL);
	      RedisplayEntryLine();

	      // and now make sure From: attribution is printed before
	      // the next message...
	      resetFrom();

	      // and we're done.
	      break;
      }

  case 15:  // ^O, command char on/off
      {
	      cmd_char_disabled = ! cmd_char_disabled;
	      if (cmd_char_disabled)
	      {
		if (hintbits & HINT_CCDISAB)
		  Send ("Command character disabled", 0, false);
		else {
		  Send
		   ("Command character disabled, hit Ctrl-O again to reenable",
		    0, false);
		  hintbits |= HINT_CCDISAB;
		}
	      } else {
		Send
		  ("Command character reenabled", 0, false);
	      }
	      break;
      }

  case 20:  // ^T, rot13 (v pna'g oryvrir v'z nqqvat guvf...)
      {
	slfparam slp;
	slp.kind   = p_is_str  ;
	slp.strval = buf.buffer;
	running_cmd = true;
	SLForumCmd::c_rot13(room, this, 1, &slp);
	running_cmd = false;
	RedisplayEntryLine();
	break;
      }

#ifdef ALLOW_MACROS

  case 17:  // ^Q, quote, but only in a certain state
      {
	  if ((state == STATE_GET_LINE)
	      && (admin_substate == ss_macro_addedit_readline))
	  {
	      setAdmSubstate(ss_macro_addedit_ctrlq);
	  }
	  break;
      }

  case 5:  // ^E, expand macro
      {
	  if (state != STATE_GET_LINE)
	      return false;
	  if ((admin_substate < ss_macro_FIRST)  // except when in macro
	      || (admin_substate > ss_macro_LAST))  // states, ^E expands macro
	  {
	      if (! cmd_char_disabled)
		  setAdmSubstate(ss_macro_expand_readkey);
	      else
		  Send
		    ("Cannot expand macros while the command char is disabled",
		     0, false );
	  } else {
	      // macro input.  just put it in the buffer
	      buf.buffer[buf.cur_pos] = (char) 5;
	      ++buf.cur_pos;
	      buf.buffer[buf.cur_pos] = '\0';
	      const char* ctrle = keyDisplayString ( (char) 5 );
	      int ctrlelen = keyDisplayLength ( (char) 5 );
	      uWrite ( ctrle, ctrlelen );
	      buf.cur_len += ctrlelen;

	      // and quote next char
	      if (admin_substate != ss_macro_addedit_ctrlq)
		  setAdmSubstate(ss_macro_addedit_ctrlq, true);
	  }
	  break;
      }

#endif  /* def(ALLOW_MACROS) */


  case 6:  // ^F, reflow mode on/off
      {
	      reflow_mode = ! reflow_mode;
	      if (reflow_mode)
	      {
		      lastkey_was_cr = false;  // re-set this flag
		      if (hintbits & HINT_REFLOW)
			      Send ("Re-flow mode ON", 0, false);
		else {
		  Send
			  ("Re-flow mode is now ON.  Hit enter twice, or exit reflow mode via",
			   0, false);
		  Send
			  ("Ctrl-F, to end a paragraph (and send its incomplete last line.)",
			   0, false);
		  hintbits |= HINT_REFLOW;
		}
	      } else {
		      if (buf.cur_pos>0) {
			      _sendOut (buf.buffer, buf.tabmask);
			      VisuallyClearBuffer();
		      }
		      Send ("Re-flow mode off", 0, false);
	      }
	      break;
      }


  case 27:     // esc
      {
	      time_t now = time(NULL);
	      if ( ( ! substate_tracks_dblchar )
		   || (state_d_char != (char)27)
		   || ((now - substate) > 2) )
	      {
	        substate = now;
		state_d_char = (char) 27;
		substate_tracks_dblchar = true;
	      } else {
		slfparam flargs[1];
		flargs[0].kind = p_is_user;
		flargs[0].u = SLF_USER_ALL;

		if ( (n_talking_to != TALKING_TO_ALL) || ! show_input )
			ClearBuffer();
		else
			VisuallyClearBuffer();  // already talking to all!

		SLForumCmd::c_to (room, this, 1, flargs);  // send to all
	      }
	      break;
      }
      
  default:
    return false;  // not an slforum ctrl char

  }  // endsw

  if (user_fd == -1)
      retval = -1;

  return true;   // handled.  all is well.

}  // ManageInputNewCtrlChar



/** Return whatever the usercommand returns,
 *  after parsing the command line.
 *
 *  Return values from HandleUserCommand:
 *  -1 == user has disconnected.  Check user_fd before each "return"
 *	in HandleUserCommand if you edit this method!
 *   0 == redisplay input line, continue.
 *
 *  Return values from user command procedures to HandleUserCommand
 *  are a little different.  They must provide for the possibility
 *  of the command being entered because the line wrapped, like a
 *  !to <user> <message> might, or even a !listen add <word> <word> ...
 *  command;  If the user hasn't disconnected, the user command should
 *  return the # parameters to copy from the old command line to the
 *  new command line (e.g., !to <user> would return 1, which would
 *  copy the first parameter <user> to the new command line, and leave
 *  out the second <message> parameter, since that's what filled the line
 *  in the first place.
 *
 *  -1 == user has disconnected.
 *   0 == fill buffer with !cmdname only, redisplay input line, continue.
 *   n == fill buffer with !cmdname and first n params,
 *	redisplay input line, continue.
 * SLForumCmd::RVAL_ALWAYS_CLEAR
 *	== clear buffer entirely, don't put !cmdname there.
 * SLForumCmd::RVAL_RETAIN_BUFFER
 *	== keep buffer content set by the command function.
 *	   If the cmd has wrapped, buffer will be !cmdname followed by
 *	   the buffer content.
 *
 *  If the line was _not_ wrapped, the buffer is totally cleared
 *  unless return val is RVAL_RETAIN_BUFFER; -1 will still be returned if
 *  the user command returns -1.
 *
 *  Also, in any command that does something public (set an away msg, kick a
 *  guest, etc.), the command must reset the user's idle time by assigning
 *	me->lastkey = me->lastping;
 *  if and only if the action succeeded (e.g., if they had a syntax error in
 *  a parameter, don't reset it) because a user's idle time is now based solely
 *  on the time of their last public action.
 */
int	SLForumUser::HandleUserCommand (bool was_wrapped)
{ 
  slfparam parms[SLF_MAX_ARGC];
  memset (parms, 0, sizeof(parms));

  /* find end of command name string */
  char *cmdend = buf.buffer+1;   // +1 to skip the esc char ('!') for commands.
  while ( (*cmdend != '\0') && (*cmdend != ' ') )
    ++cmdend;
  if (*cmdend == ' ')
    *cmdend = '\0';
  else
    cmdend = NULL;  /* no need to un-null it later */

  /* b-search to find command's SLForumCmd method */
  int idx = find_cmd_index (buf.buffer+1, false);
  if (idx == -1) {
    SLForumCmd::c_help (room, this, 0, NULL);
    if (user_fd == -1)
      return -1;
    else {
      ClearBuffer();
      // if it's admin mode, set that prompt right
      if (admin_substate != ss_nonadmin)
      {
	buf.buffer[0] = pref.cmd_char;
	buf.buffer[1] = '\0';
	buf.cur_pos = 1;  buf.cur_len = 1;
      }
      return 0;
    }
  }

  /* un-delimit cmd name and pass to method */
  if (cmdend != NULL)
    *cmdend = ' ';

  int found_params = 0;

  {
    /* parse rest of arguments (19990529.1800) */
    register char *c;
    const char* argtyp = usercmds[idx].parmspec;
    bool cmdend_was_space;
    bool repeatable;

    int i;

    for (i = SLF_MAX_ARGC - 1; i >= 0; --i)
    {
      parms[i].kind   = p_is_unused;
      parms[i].strval = NULL;   // zero out any past param data
    }

    i = 0;

    c = cmdend;
    if (c != NULL)
      while (*c == ' ')
	++c;	// skip past whitespace

    while ((c != NULL) && (*c != '\0')
	   && (argtyp != NULL) && (*argtyp != '\0')
	   && (i < SLF_MAX_ARGC)) {

      // find end of the arg on input line
      cmdend = c; ++cmdend;
      while ((*cmdend != '\0') && (*cmdend != ' '))
	++cmdend;

      // temporarily mark it as \0 for parsing's sake
      // (actually, this is permanent unless they want entire-rest-of-line)
      cmdend_was_space = (*cmdend != '\0');
      if (cmdend_was_space)
	*cmdend = '\0';

      // now parse the arg

      if (*argtyp == '?')
	++argtyp;	// skip "optional" mark
	      // TODO TODO TODO what about multiple optionals?
	      // especially of diff. types?

      repeatable = (*(argtyp+1) == '+');
      if (repeatable && (*(argtyp+2) != '\0')) {
	room->Announce ("Internal cmd spec error: only the last param can be repeatble for");
	room->Announce (usercmds[idx].name);
	return 0;
      }

      switch (*argtyp) {
      case 'V':
	// void, end of args
	argtyp = NULL;
	break;
      case 'C':
	// parse arg name
	{
	  if ((*c == pref.cmd_char) && (*(c+1) != '\0'))
	    ++c;    // skip cmd char if user put it in
	  int cidx = find_cmd_index (c, false);
	  if (cidx == -1) {
	    Send ("No command by that name was found.", 0, false);
	    if (user_fd == -1)
	      return -1;
	    else
	      return 0;
	  }
	  parms[i].kind = p_is_cmd;	  
	  parms[i].cmd = &(usercmds[cidx]);	 
	  ++found_params;
	}
	break;
      case 'A':
      case 'U':
	// parse user name
	parms[i].kind = p_is_user;
	parms[i].u = room->GetUserByName (c);
	if (parms[i].u == NULL) {
	  if (( (*argtyp == 'A') && (0 == strcasecmp ("all", c))))
	    parms[i].u = SLF_USER_ALL;
	  else {
	    Send ("No user by that name was found.", 0, false);
	    if (user_fd == -1)
	      return -1;
	    else
	      return 0;
	  }
	}
	++found_params;
	break;
	
      case '{':
	// TODO TODO TODO parse specific string, one of several alternatives
	while ((*argtyp != '}') && (*argtyp != '\0'))
	  ++argtyp;
	// for now, just don't break, pretend we want a single word
      case 'W':
	// just a word.
	parms[i].kind = p_is_str;
	parms[i].strval = c;
	++found_params;
	break;
      case 'I':
	// integer
	{
		// use strtol instead of atoi to detect errors
	    char *endp;
	    parms[i].kind = p_is_int;
	    parms[i].intval = (int) strtol(c, &endp, 10);
	    if (*endp != '\0')
	    {
		Send ("Parameter must be a number.", 0, false);
		if (user_fd == -1)
		    return -1;
		else
		    return 0;
	    }
	}
	++found_params;
#ifdef EGCS_ATOI_BUG_WORKAROUND
	/* that's right, the value won't take if it's not atoi'd and printed
	 * out... bizarre. egcs-1.1.2-12 rh package. Fixed by 1.1.2-24. (rh6.1)
	 */
	room->logprintf ("L2155: parms[%d] = %d\n", i, atoi(c));
#endif
	break;
      case '*':
	// want data to end of line
	if (cmdend_was_space)
	  *cmdend = ' ';  // restore it
	parms[i].kind   = p_is_str;
	parms[i].strval = c;
	++found_params;
	argtyp = NULL;
	break;
      case '\0':
	argtyp = NULL;
	break;
      default:
	room->Announce("Internal Error: incorrect arg spec given in cmd table for");
	room->Announce(usercmds[idx].name);
	break;
      }  // endsw

      if (! cmdend_was_space)
	argtyp = NULL;	// this really end of cmd string, so stop parsing
      else if ((argtyp != NULL) && (*argtyp == '\0'))
	argtyp = NULL;

      if (argtyp != NULL) {
	// move along to next param on user input line
	c = cmdend;
	if (cmdend_was_space)
	  ++c;  // skip past it, since it's now a null

	while (*c == ' ')  // eat whitespace
	  ++c;
	if (*c == '\0')    // nothin' else on cmdline!
	  argtyp = NULL;
	else {
	  ++i;  // move to next param index in array of args
	  if (! repeatable) {
	    ++argtyp;  // move to next param in argtyp spec
	    if ((*argtyp == '\0') || (i == SLF_MAX_ARGC)) {
	      argtyp = NULL;  // no more params.
	    }
	  }  // if it _is_ repeatable, it must be last param to avoid
	     // any ambiguity in type, which makes things simpler. :-)
	}
      }
    }  // end parsing loop (one iteration per param)
  }  // end {} section for declaring vars and parsing args

  // now we do the command, and if the line wrapped, do
  // the right thing with the params on the new input line.
  {
    int rval = (usercmds[idx].meth) (room, this, found_params, parms);

    if ( (! was_wrapped) || (rval < 0) ||
	 (rval == SLForumCmd::RVAL_ALWAYS_CLEAR))
    {
      if (rval != SLForumCmd::RVAL_RETAIN_BUFFER)
      {
	ClearBuffer();
	if (admin_substate != ss_nonadmin)
	  promptlen = admpromptlen;   // since we're going escmode->adminmode
      }
      if ((user_fd == -1) || (rval < 0))
	return -1;
      else
	return 0;
    }

    // build a new buffer, then copy it over the old one.
    {
      char newbuf[2 * USERBUFSIZ];  // large size is Just In Case

      // now the args.
      const char* argtyp = usercmds[idx].parmspec;

      // first, the command... (and a space)
      sprintf(newbuf, "%c%s ", pref.cmd_char, usercmds[idx].name);

      for (int ii = 0;
	   ((ii < rval) && (argtyp != NULL) && ((*argtyp) != '\0'));
	   ++ii, (argtyp != NULL ?
		  ++argtyp /* side-effect: moveto next char in param desc fld */
		  : argtyp /* do nothing */ ) ) {

	if (*argtyp == '?')
	  ++argtyp;

	if (ii >= found_params)
	  argtyp = "";  // no params left, time to stop

	switch(*argtyp) {
	case 'V':
	  // void, end of args
	  argtyp = NULL;
	  break;
	case 'C':
	  // a cmd name
	  if (parms[ii].cmd == NULL)
	    break;
	  strcat (newbuf, parms[ii].cmd->name);
	  break;
	case 'A':
	case 'U':
	  // a username
	  if (parms[ii].u == NULL)
	    break;
	  if (parms[ii].u == SLF_USER_ALL)
	    strcat (newbuf, "all");
	  else
	    strcat (newbuf, (parms[ii].u)->GetName());
	  break;
	case '{':
	  // TODO TODO parse specific string, one of several alternatives
	  while ((*argtyp != '}') && (*argtyp != '\0'))
	    ++argtyp;
	  // for now, just don't break, pretend we want a single word
	case 'W':
	  // just a word.
	  if (parms[ii].strval == NULL)
	    break;
	  strcat (newbuf, parms[ii].strval);
	  break;
	case 'I':
	  // integer
	  {
	    char intstr[32];
	    sprintf (intstr, "%d", parms[ii].intval);
	    strcat (newbuf, intstr);
	  }
	  break;
	case '*':
	  // data to end of line. Another string.
	  if (parms[ii].strval == NULL)
	    break;
	  strcat (newbuf, parms[ii].strval);
	  argtyp = NULL;
	  break;
	case '\0':
	  argtyp = NULL;
	  break;
	}  // endsw

	strcat (newbuf, " ");

      }  // end for

      // copy it over the old one.
      strncpy (buf.buffer, newbuf, USERBUFSIZ-1);
      buf.cur_len = stringDisplayLength(buf.buffer, &buf.cur_pos);
    }
  }
  if (user_fd == -1)
    return -1;
  else
    return 0;
}


/* for login: set up listen list, load prefs, and maybe make admin.
 * Just as things about entering the room go in AcceptAndWelcomeUser,
 * things about logging in a real user go here.
 */
void SLForumUser::setupRealUserInfo (struct slf_userinfo *ppw)
{
  if (is_guest)
    return;

  // set our name
  strcpy (name, ppw->u.pw_name);

  // are we real?  (if so, our uid is not zero, since root can't log in)
  // virtusers always have uid 0 to mark them in their passwd struct.
  is_virt = (ppw->u.pw_uid == (uid_t) 0);

  // are we admin?
  is_admin = false;

  // user->admin check (The userid which started SLF, if at least
  //	SLF_MIN_REAL_USERID, is always an admin.)  20000714.2100
  {
    uid_t procuid = getuid();
    if ((! is_virt) &&
	(procuid >= SLF_MIN_REAL_USERID) &&
	(procuid == ppw->u.pw_uid))
      is_admin = true;
  }

  // group->admin check
  if (! is_admin)
  {
      gid_t admgid = room->getAdminGid();
      if (ppw->u.pw_gid == admgid)
	  is_admin = true;       // it's their primary group
      else
      {
	  // not their primary group, but they may be in it
	  struct group *grent;
	  grent = getgrgid(admgid);
	  if (grent != NULL)
	  {
	      const char** grmems = (const char**) grent->gr_mem;
	      for (int i = 0; grmems[i] != NULL; ++i)
	      {
		  if (strcmp(name, grmems[i]) == 0)
		  {
		      is_admin = true;
		      break;
		  }
	      }  // end for-each-member
	  }  // end if-grent-not-null
      }  // end grent check
  }  // end admin check
  

  // load prefs (which may include a listen list that we don't want to
  // overwrite by adding to the listen list here)
  loadPrefs();       // load prefs. w00t!  -- dschin 19991116.0200
}



/** This is the workhorse of !to and !to-multiple.
 * Have this user send a message out; depending on
 * privacy fields, this will either go to all users,
 * or a specific user.  If the !to'd user logs out,
 * the message won't be sent, and further messages
 * will be broadcast to all.
 * jer 980701.1500
 * 19990113.1412: added !to-multiple support
 * [see changelog for many other changes]
 * 20000320.1100: added ALL_MESSAGES_TIMESTAMP;
 *	it's much less annoying to add it here than inside the bottom-level
 *	Send method.
 */
void SLForumUser::_sendOut (const char *sendthis, int* tabmask /* =NULL */ )
{    
    const char *sendtext;  // = sendthis, unless sendthis contains IAC chars
    char st_buf[USERBUFSIZ * 2];  // this is used if IAC found

#ifdef ALL_MESSAGES_TIMESTAMP
  char         tstampBuf[64];
  const struct tm *ptime;
  time_t       now  = time(NULL);

  ptime = localtime(&now);
  if (ptime == NULL) {
    room->Announce("Assert failed in _sendOut: ptime is NULL");
    sprintf (tstampBuf, "At unknown:time:");
  } else {
    sprintf (tstampBuf, "At %d:%02d:", ptime->tm_hour, ptime->tm_min);
  }
#endif

  /* check for IAC (telnet command char) and double any of those to esc them */
  if (NULL == (strchr(sendthis, IAC)))
      sendtext = sendthis;
  else
  {
      int i=-1, j=-1;   // will be 0 (start of string) soon.
      do {
	  ++i;  ++j;
	  st_buf[i] = sendthis[j];
	  if (st_buf[i] == (char) IAC) {  ++i;  st_buf[i] = (char) IAC;  }
      } while (st_buf[i] != '\0');

      sendtext = st_buf;
  }

  if (to_bits == (userfield_t)0)  // public
  {

      if (sendtext[0] != '\0')
      {
	  strncpy (lastbuf.buffer, sendtext, USERBUFSIZ-1);  // 20000321.1330
	  if (tabmask != NULL)
	      for (int tabi = (TABMASK_BUFSIZE - 1); tabi >= 0; --tabi)	
		  lastbuf.tabmask[tabi] = tabmask[tabi];
	  else
	      for (int tabi = (TABMASK_BUFSIZE - 1); tabi >= 0; --tabi)	
		  lastbuf.tabmask[tabi] = 0;
      }

      lastkey = lastping;		// reset idle

#ifdef ALL_MESSAGES_TIMESTAMP
      room->Announce
	  ( tstampBuf );
#endif
      room->BroadCast
	  ( name, sendtext, MSG_PUBLIC | (is_guest ? MSG_GUEST : 0) );

      if ((pref.ignore_mask & MSG_PUBLIC) ||
	  (is_guest && (pref.ignore_mask & MSG_GUEST)))
      {
#ifdef ALL_MESSAGES_TIMESTAMP
	  Send (tstampBuf, 0, false);
#endif
	  Send (0, name, NULL, sendtext); // so we see our own text if we're
					  // ignoring the world (19990211.0200)
      }

  } else {    // to_bits is not empty: whispering

    userfield_t tf = to_bits;
    SLForumUser *u_to = this;  // stop compiler warn/err about poss. uninit.
    int i;
    bool did_local = false;    // sent to self yet?

    // send to !to-ers, stop when all bits in tf are 0
    for ( i = 0; tf ; tf = tf >> 1, ++i )
    {
      if (i >= MAX_CHAT_USERS)
      {
	      room->logprintf ("L3651: _sendOut: MAX_CHAT_USERS %d but i %d -- tf is 0x%llx\n",
			       MAX_CHAT_USERS, i, (unsigned long long) tf);
	      break;
      }
      if (0 == (tf & (userfield_t) 1 ))
      {
	              // if LSB (bit on the right) isn't set,
	continue;     // move to next bit
      }

      u_to = room->GetUserByIndex(i);
      if (u_to != NULL) {

	// first, check whisperban list, we might be squelched
	if (u_to->CheckBanned(this))
	  continue;        // move to next bit

#ifdef ALL_MESSAGES_TIMESTAMP
        u_to->Send (tstampBuf, 0, false);
#endif
	u_to->Send ( (is_guest ? MSG_GUEST : 0) , name, u_to->GetName(),
		     sendtext, talktomany_fieldwid);

	if (u_to == this)
	  did_local = true;   // self was among to'ers.

      } else {
	// to'd user is no longer connected, we didn't know; update status.

	char buf[USERBUFSIZ+12];  buf[USERBUFSIZ+11] = '\0';
	u_to = room->GetUserByIndex(i, true);
	if (u_to != NULL) {
	  snprintf
	    (buf, USERBUFSIZ+11, "%s has left.", u_to->GetName() );
	  Send ( buf, 0, true );
	}
	talkingTo (i, false);
      }
    }  // end for-each-talkingto

    if ((to_bits) && ! did_local)  // to_bits!=0 check is in case they left
    {                              // and no more bits are set; if we aren't
                                   // talking to anyone anymore, don't echo
                                   // the line no one heard.
      // local display: echo to self to remember that we
      // were talking to someone... :-)
      ClearEntryLine();
#ifdef ALL_MESSAGES_TIMESTAMP
      Send (tstampBuf, 0, false);
#endif
      if (n_talking_to == TALKING_TO_ONE)
	Send (0, name, u_to->GetName(), sendtext);
      else
	Send (0, name, "many", sendtext, talktomany_fieldwid);
    }
  }
}



// jer 19990126.2100
/** Return the index of the matching command in the usercmds table above,
 *  or -1 if none.  *buff should NOT contain the initial command escape
 *  character (usually '!'), but should start with the start of the actual
 *  command name.
 *
 *  Hack: 19991031.0210: two command chars in a row (usually !!)
 *  will return c_toall_idx.  We can't hardcode it into the usercmd
 *  table anymore, because the user can change the command char.
 *  It's still in there for help purposes, and because find_cmd_index
 *  must return its index. <Grin>
 */
int SLForumUser::find_cmd_index (const char *buff, bool complete_partial) const
{
  int idx0 = 0;
  int idxn = NUM_USERCMDS - 1;
  int cmpres=1, idx;

  if ( (buff[0] == pref.cmd_char)
       && ( (buff[1] == ' ') || (buff[1] == '\0')))
    return c_toall_idx;				// !! - jer 19991031.0210

  size_t partiallen = (size_t) 0;
  if (complete_partial)
      partiallen = strlen (buff);  // used many times below

  for ( idx = (idx0 + idxn) >> 1 ;
	(cmpres != 0) && (idx0 <= idxn);
	idx = (idx0 + idxn) >> 1 )
  {
    cmpres = strcasecmp ( usercmds[idx].name, buff );  // 19990717.2250;
						// changed strcmp to strcasecmp
    if (cmpres > 0) {
	idxn = idx - 1;   // binary search left
    } else if (cmpres < 0) {
	idx0 = idx + 1;   // binary search right
    }
  }
  if (cmpres == 0)
    return idx;
  else {
	// before we move on (looking left or right), check this and
	// the next cmd for partial match.
    idx = idxn + 1;
    if (complete_partial &&
	(idx < NUM_USERCMDS) &&
	(idx0 > idxn) &&
	(0 == strncasecmp (usercmds[ idx ].name, buff, partiallen)) &&
	( idx == (NUM_USERCMDS-1) ||
	  ((0 != strncasecmp (usercmds[idx+1].name, buff, partiallen)))))
      return idx;
    else
      return -1;
  }
}



////////////////////////////////////////
// connection mgmt
//
////////////////////////////////////////


/* write an entry for logout of the user to wtmp file fname
 * (calls passwd.c's writelogoutentry)
 * jer 980701.1300
 */
void SLForumUser::writeLogoutEntry (const char *fname) {
  // This is mostly a wrapper to the plain-C function
  // of that name in passwd.c
  //
  writelogoutentry (&uentry, fname);
}

/* Reset fields to disconnected state; used by
 * constructor and ManageUser().
 * jer 980701.1500
 */
void SLForumUser::resetFieldsToDiscon (bool manualDiscon /* =false */ )
{
  if (user_fd != -1)
  {
    close (user_fd);
    user_fd=-1;
  }

  if (state != STATE_NO_CONNECTION)  // this check prevents incr in constructor
      ++(room->empty_slots);  // incr since not taking up a slot anymore

  time (&loginat);	// logout time set -- jer 981005.0955
  ClearBuffer();
  sendMessagesToNone();
  sendMessagesFromNone();
  setState (STATE_NO_CONNECTION);
  show_input = true;
  is_away    = false;
  is_guest   = false;
  is_admin   = false;
  is_virt    = false;
  running_cmd= false;
  running_macro   = false;
  pref.local_mode = false;
  buf.macro_input = false;
  buf.tel_bin_in  = false;  // initial NVT state is not telnet binary mode
  buf.tel_bin_out = false;
  buf.tel_bin_in_hadit  = false;
  buf.tel_bin_out_hadit = false;
  admin_substate = ss_nonadmin;
  admin_prev_substate = ss_nonadmin;
  substate  = 0;
  statedata = NULL;
  statedat2 = NULL;
  all_keys_reset_idle = true;   // except for in-room, all keys reset idle.
				// so we set it to true, though the user prefs
  				// will probably set it to false when they get
				// into the room.

  pref.wants_true_idle     = false;  // this is the default setting (for privacy),
				// if they don't have a prefs file.
  pref.tab_expand_incl_self = false;
  pref.rot10_digits_too = true;	// pref for c_rot13
  pref.rot_offset = 13;		// pref for c_rot13
  pref.jetlag    = 0;	        // pref for timezone offset from server
  pref.dice_n_sides = 6;        // pref for # sides on dice
  pref.showaidle = true;        // pref for showing AWAY:idle messages
  pref.show_recap = true;       // pref for showing recap at login.
				// Although recap may be ifdef'd away,
				// the pref save/load code always sees it,
				// in case this user's file has the pref.

  hintbits   = 0;
  promptlen  = 0;
  talktomany_fieldwid = 0;	// just in case
  pref.ignore_mask  = 0;
  user_can_lock = unknown;

  memset ( &loginfromip, 0, sizeof(loginfromip));  // clear IP info

  *away       = '\0';
  *lastmfrom  = '\0';
  *timer_message ='\0';
  *yankbuf.buffer = '\0';
  yankbuf.cur_pos = 0;  yankbuf.cur_len = 0;  yankbuf.macro_input = false;
  *lastbuf.buffer = '\0';
  *admprompt  = '\0';
  lastm_rpt   = 0;
  lastkey     = loginat;
  lastping    = loginat;
  blanklines  = 0;
  pref.cmd_char    = SLF_CMD_ESC_CHAR;
  cmd_char_disabled = false;
  pref.cmd_char_alt = '\0';
  is_locked   = false;
  reflow_mode = false;

  setAutoMsgchk (AUTO_MSGCHK_INTERVAL);  // just in case
  auto_msgchk_lastnew_is_valid = false;

  setAutoAway(AUTO_AWAY_TIME);

  pref.idle_here_threshold = SLF_IDLE_HERE_THRESHOLD;
	// MaTTeD add 20000129.1745 for timer
  IsTimerSet = false;
  timer_expire_hour = -1;


  if (manualDiscon)
    substate = 1;
  else
    substate = 0;

  pref.maxAwayIdle=0;	// 19990922.1145 dschin
  pref.maxTopicIdle=0;	// 19990922.1145 dschin

  if (pref.listenlist != NULL) {
    llist_free (pref.listenlist);
    pref.listenlist = NULL;
  }

  if (pref.wblist != NULL) {
    llist_free (pref.wblist);
    pref.wblist = NULL;
  }

#ifdef ALLOW_MACROS
  macro_next_level = 0;  // not running a macro :)

  if (pref.macrolist != NULL) {
    llist_free (pref.macrolist);
    pref.macrolist = NULL;
  }
#endif  /* def(ALLOW_MACROS) */

  /* setup the logout_at time string -- jer 981005.0955 */
  ctstr[0] = '\0';
  {
    char *sys_ctbuf = ctime (&loginat);
    if (sys_ctbuf == NULL) {
      char erbuf[128];
      sprintf (erbuf, "assert failed: ctime: sys_ctbuf NULL for %s", name);
      room->Announce(erbuf);
    }else
      strncpy (ctstr, sys_ctbuf+11, 5);  // +11 skips day of week, date.
    ctstr[5] = '\0';  // be null-terminated, have only hh:mm
  }


  /* clear out the lines/sec brake's memory   19990922.2100 */
  lines_idx = 0;
  lines_sum = 0;
  lastline = time(NULL) - ((time_t) MAX_LINES_SECS_FOR_AVG);
  for (int i = 0; i < MAX_LINES_SECS_FOR_AVG; ++i)
    lines_per_sec[i] = 0;
}


/* Check for login timeout... if not logged-in
 * yet, this will disconnect them if they're too slow.
 * jer 980703.2345
 * 980903.1200: announce to room when they're timed out.
 *
 * 19990702.1900: renamed from kickIftimeout to pingAndCheckTimeout;
 *	added functions to send something to idle users
 *	to see if they're still connected.
 *
 * 20000510.0708: now returns true if still here, false if disconnected
 */
void SLForumUser::pingAndCheckTimeout() {
  time_t now = time(NULL);
  if (    (state != STATE_NO_CONNECTION)
       && (state != STATE_GET_LINE)
       && ( (now - loginat) > CONNECT_TIMEOUT ))
  {
    // first, announce timeout (so we know who's _trying_ to log in)
    {	  	// 980903.1200
      char s[80];
/*      char *gtime;      // 990427.0015 dschin- log the time they time out
      time_t gknock = time(NULL);
      gtime=ctime(&gknock);
      if (gtime == NULL) {
	room->Announce
	  ("user:L2625: ctime: assert failed in 'pingAndCheckTimeout'");
	gtime=(char *) "??? ??? ?? ??:??:??";  // gcc2.95 wants cast from const
      } else
	gtime[16]= '\0';
*/
      sprintf (s, "During login, '%s' has timed out at \201.", name);
      room->Announce (s);
    }

    // now do the kick
    kick("timeout");
    return;
  }

  if (! IsUserAccepted())
    return;

  if ( ((int) now - lastping) >= PING_INTERVAL_SEC ) {
    // send them a space,backspace to see if they're still connected.
    // (uWrite sets user_fd to -1 if not)
    if (uWrite (" \010", 2) != -1)
      lastping = now;	// still here :)
  }
}


/** Just kick 'em out.
 *  Set state to STATE_KICKED; on next
 *  call to ManageUser(), close the
 *  socket, clean up, and return -1.
 *  (modded 980709.2300; it used to
 *   directly close, now it sets state.)
 *  jer 980704.1420
 */
void SLForumUser::kick(const char *reason) {
  room->logprintf("%s: disconnected %s (from %s)\n",
		  reason, name,
#ifdef SOLARIS_UTMP
		  dnsfrom
#else
		  uentry.ut_host
#endif
		  );
  writeLogoutEntry(wtmpfname);
  resetFieldsToDiscon();
}



/* Set a user as logged-in, and make the appropriate beeps.
 * This is called in 2 places:
 * - from HandleGetPw() after the right pw is given
 * - from HandleUserCommand() when a guest has been invited
 * It assumes that a couple appropriate fields
 * (such as name and uentry) are properly set,
 * and sets the rest (lastkey, login_at, etc.)
 * jer 980703.2345
 * 981005.0955 - added timestamp to announcement.
 */
void SLForumUser::acceptAndWelcomeUser() {
  char s[128];

  all_keys_reset_idle = pref.wants_true_idle; // in-room, only public msgs reset
			// idle, by default; wants_true_idle is the user pref.

  setState (STATE_GET_LINE);

  time (&loginat);		// login time set
  lastkey = lastping = loginat; // idle time set

#if USE_RECAP
  time_t room_last_public = room->get_recap_lasttime();
#endif

  /* setup the login_at time string */
  {  
      ctstr[0] = '\0';
      char *sys_ctbuf = ctime (&loginat);
      if (sys_ctbuf == NULL) {
	  char erbuf[128];
	  sprintf (erbuf, "L3850 assert failed: sys_ctbuf NULL for %s", name);
	  room->Announce(erbuf);
      } else
	  strncpy (ctstr, sys_ctbuf+4, 12);  // +4 skips day of week.
      ctstr[12] = '\0';  // lop off ":ss year" at end
  }

  strcpy (s, name );
  strcat (s, " has arrived, from " );
  strncat (s,
#ifdef SOLARIS_UTMP
	   dnsfrom, (DNSFROMSIZ-1)
#else
	   uentry.ut_host, UT_HOSTSIZE
#endif
	   );
  strcat (s, " at \201.");

  n_talking_to = TALKING_TO_ONE;
  to_bits = (userfield_t) 0;
  sendMessagesTo(NULL);  // this also sets prompt up properly,
			// if n_talking_to isn't TALKING_TO_ALL on entry.
			// (which we conveniently assure, right above here)

  writeloginentry (&uentry, name, wtmpfname);
  room->Announce( MSG_MOVES, s, true );  // "userx has arrived, from..."
#if USE_RECAP
  int idx_announce_ignoreline = room->get_recap_lastindex();
#endif

  if (pref.auto_msgchk_minutes > 0)
    auto_msgchk_next = time(NULL) + 10;  // at least 10 secs from now
			// so it doesn't get lost among the help msgs.

  uWrite( welcome );
  uWriteSubst( commands );
  rndCommand();
  sprintf (s, "   Type %cset command X to change the command-prefix char to X (/ for example)", pref.cmd_char);
  Send (s);   // no preceding To:
  ClearBuffer();
  room->UpdateUserCount (NULL, true);
  room->who_sort_is_valid = false;  // trigger re-sort
  room->ListUsers( this );
  room->who_sort_is_valid = false;  // mark as invalid

  hintbits = 0;  // since ListUsers will set a hintbit if anyone's away
  
  // warn if ignoring public
  if (pref.ignore_mask & MSG_PUBLIC)
  {
    Send ("NOTE: You are currently ignoring public.", 0, true);
    Send ("Type \177unignore public to hear it again.", 0, false);
  }


#if USE_USERCOUNT_FILE
  {
    time_t now = time (NULL);
    room->UpdateUserCountFile (now);
  }
#endif


  // show the re-cap, if any
#if USE_RECAP

  if (pref.show_recap)
    room->SendRecapToUser (this, room_last_public, idx_announce_ignoreline);

#endif  /* USE_RECAP */
  
  // setup of up listen list is done elsewhere.  Thus, user is ready.
  // room->logprintf ( "done.\n" );
}



////////////////////////////////////////
// getters and setters
//
////////////////////////////////////////


bool SLForumUser::isAdmin() const { return is_admin; }


/** Mostly for room::AnnounceHint.
 *  jer added 980709.1700
 */
int SLForumUser::getHint(int setbitfield) const {
  return (hintbits & setbitfield);
}


/** Mostly so room::AnnounceHint can set so a hint
 *  is only sent once.
 *  jer added 980709.1700
 */
void SLForumUser::setHint(int setbitfield) {
  hintbits |= setbitfield;
}



/** Set the admin prompt to newprompt, and redisplay input line with
 *  that prompt.  Call setAdmPrompt(NULL) to no longer display the
 *  admin prompt (the prompt will revert back to the user's
 *  slforumuser::prompt buffer.)  The user's 
 *  non-admin buffer will also be displayed if buf[0] == cmd_char.
 *
 *  For convenience, the prompt character SLF_PROMPT_CHAR is substituted
 *  if newprompt ends with ">" or "> " but the current SLF_PROMPT_CHAR is
 *  something different.
 *
 *  Note that as a sideeffect, calling setAdmPrompt clears the buffer
 *  before changing the prompt, unless clearbuffer=false.
 */
void SLForumUser::setAdmPrompt
	(const char *newprompt, bool clearbuffer /* =true */ )
{
  if (clearbuffer) ClearBuffer();

  // set the length right... (it's either the admin prompt
  // length, or user prompt length)

  if (newprompt != NULL) {
    if (admprompt[0] == '\0') {	// displaying real->displaying admin prompt:
      realpromptlen = promptlen;   // save length of user prompt
    }
    admpromptlen = strlen(newprompt);
  } else {
    if (admprompt[0] != '\0')	// displaying admin->displaying real prompt:
      promptlen = realpromptlen;	// restore length of user prompt
  }

  // and set the prompt
  // (code from ::setPrompt)

  if (newprompt == NULL)
    admprompt[0] = '\0';
  else {

    if (admpromptlen < 12) {
      admpromptlen = 12;  	// for proper line wrap in other routines

      sprintf (admprompt, "%12s", newprompt);	// so it aligns like the rest
						// of the lines on the screen
    } else {
      strncpy (admprompt, newprompt, USERBUFSIZ-1);
      if (admpromptlen > (USERBUFSIZ-1))
	admpromptlen = USERBUFSIZ - 1;
    }

#if (SLF_PROMPT_CHAR != '>')
    if (admprompt[admpromptlen-1] == '>')
    {
      admprompt[admpromptlen-1] = SLF_PROMPT_CHAR;
    } else if ((admprompt[admpromptlen-2] == '>') &&
	       (admprompt[admpromptlen-1] == ' '))
    {
      admprompt[admpromptlen-2] = SLF_PROMPT_CHAR;
    }
#endif

    promptlen = admpromptlen;
  }

  if (! running_cmd) {
    RedisplayEntryLine();
  }
}


/** Regenerate the username> or username to othername> prompt
 *  for STATE_GET_LINE, and redisplay the entry line.
 *
 *  Unless always_do_it=true, it may not set the prompt, assuming
 *  the prompt is already set to the right value.  (This would be
 *  the proper assumption if this was called right after adding or
 *  removing a to'd user.)
 *
 *  If your routine has nuked the prompt and you need it regenerated,
 *  set always_do_it to true.
 */
void SLForumUser::setPromptFromTo(bool always_do_it /* =false */ )
{
  if (state != STATE_GET_LINE)
    return;

  /* first, save current n_talking_to value */
  n_talk_to_t last_talking_to = n_talking_to;

  /* now set n_talking_to:  Go through the bits and see if we
   * have 0, 1, or more unique usernames to talk to.
   */
  if (to_bits == (userfield_t) 0) {
    n_talking_to = TALKING_TO_ALL;
    if ((n_talking_to == last_talking_to) && (! always_do_it))
      return;
    else {
      char prmptbuf[USERNAMESIZ+4];
      sprintf (prmptbuf, "%s%c ", name, SLF_PROMPT_CHAR);  // "joe> "
      setPrompt (prmptbuf);
      /* Prompt is set.  Now, the user
       * _was_ talking to someone else,
       * so clear buffer to prevent faux pas of public
       * echo of a private message.
       * If it was a command, though, don't clear it.
       */
      if ((last_talking_to != TALKING_TO_ALL) && (buf.buffer[0] != pref.cmd_char))
	ClearBuffer();
    }

  } else {

    // it's either _ONE or _MANY
    int bitno;
    const char *bitnam = NULL;
    int namlen = strlen(name);	// so all !to-ers look at same set of names
				// for finding the longest one.
    int dbg_found_idx=-1;  // for debug: the bit number for the user we "found"
             // TODO TODO TODO when the bug is elimintated, get rid of this var
	     // the to (null) bug that is

  // The cast ((userfield_t) 1) _before_ shift left is needed to ensure proper
  // operation of 64-bit userfield_t on 32-bit platforms.  (20020728.1015)

    for (bitno = 0; bitno < MAX_CHAT_USERS; ++bitno) {
      if (to_bits & ( ((userfield_t) 1) << bitno)) {
	// found one.
	int nlen;
	SLForumUser *uu;
	if ((bitnam == NULL) || (*bitnam == '\0')) {
	  // it's the first one.
	  uu = room->GetUserByIndex(bitno);
	  if (uu != NULL) {
	    n_talking_to = TALKING_TO_ONE;
	    dbg_found_idx = bitno;
	    bitnam = uu->GetName();
	    if ((bitnam != NULL) && (*bitnam != '\0')) {
	      nlen = strlen(bitnam);
	      if (nlen > namlen)
		namlen = nlen;
	    }
	  }

	} else {

	  // at least one more.
	  uu = room->GetUserByIndex(bitno);
	  if (uu != NULL) {
	    const char *nnam = uu->GetName();
	    if ((nnam != NULL) && (*nnam != '\0')
		&& strcmp (nnam, bitnam)) {
	      // it's a different name.
	      n_talking_to = TALKING_TO_MANY;
	      nlen = strlen(nnam);
	      if (nlen > namlen)
		namlen = nlen;
	    }
	  }
	}

      }
    }  // end for-each-bit

    /* we might return here.
     * (The only thing left to do is to set the
     *  prompt and the fieldwid.)
     *
     *  We return here, with prompt and
     *  fieldwid unchanged, iff:
     *  - We aren't definitely setting (always_do_it == false)
     *  - n_taking_to unchanged (still to many or to one,
     *		so the prompt is set already)
     *  - we're not talking to one (thus, talking to many)
     *  - no user to whom we're talking has a name longer than ours
     */
    if ((! always_do_it) &&
	(n_talking_to == last_talking_to) &&
	(n_talking_to != TALKING_TO_ONE) &&
	(talktomany_fieldwid == namlen*2+4 ))
      return;

    /* Not returning; set prompt. */

    if (n_talking_to == TALKING_TO_ONE)
    {
      char fromtobuf[USERPROMPTSIZ];
      fromtobuf[USERPROMPTSIZ-1] = '\0';
      if (bitnam == NULL)
      {
	  room->logprintf
	      (" L3875 ASSERT FAILED: bitnam is NULL check log tell jer");
	  room->logprintf
	 ("(while setting up talkingTo for user %s (index %d) to index %d)\n", 
	  name, my_idx_in_room, dbg_found_idx);
	  room->debug_PrintSlots();
	  bitnam = "??";
      }
      snprintf (fromtobuf, USERPROMPTSIZ-1, "%s to %s%c ",
	name, bitnam, SLF_PROMPT_CHAR);		  // "joe to bob> "
      setPrompt (fromtobuf);
      talktomany_fieldwid = strlen(prompt) - 2;   // take 2 off for "> "
    } else {
      char *endc;
      int fwid;

      talktomany_fieldwid = namlen * 2 + 4;

      snprintf (prompt, USERPROMPTSIZ-1, "%s to many", name);
      // pad out that "many" to length of longest name
      // and then add the "> " to the end

      fwid = strlen(prompt);
      endc = prompt + fwid;
      for (int nidx = fwid; nidx < talktomany_fieldwid; ++nidx) {
	*endc = ' ';  // pad one space
	++endc;
      }

      // and add the "> "
      *endc = SLF_PROMPT_CHAR;  ++endc;
      *endc = ' ';  ++endc;
      *endc = '\0';
    }
  }

  promptlen = strlen(prompt);

  if (! running_cmd) {
    RedisplayEntryLine();
  }
}


/** This is so we always know the length of the prompt
 *  string, saving many many calls to strlen().
 *  jer added 980709.1715
 */
void SLForumUser::setPrompt (const char *newprompt) {
  promptlen = strlen(newprompt);
  if (promptlen < 12) {
    promptlen = 12;  	// for proper line wrap in other routines

    sprintf (prompt, "%12s", newprompt);  // so it aligns like the rest
					  // of the lines on the screen
					  // (19990612.2330)
  } else {
    strncpy (prompt, newprompt, USERPROMPTSIZ-1);
  }
}


void SLForumUser::setAutoMsgchk(int minutes)
{
  pref.auto_msgchk_minutes = minutes;
  if (minutes > 0)
  {
    time_t now = time(NULL);	// time it for sometime _by_ the right minute
				// but not by the minute before, without
				// looking up the current minutes and seconds.
    auto_msgchk_next = now + (time_t) ( 1 + (60*(minutes-1)) );
  } else {
    auto_msgchk_next = 0;   // no need to really set it;
		// loop checks pref.auto_msgchk_minutes != 0. (LookForTimers)
  }
}



void SLForumUser::setAutoAway(int minutes)
{
  pref.auto_away_seconds = 60 * minutes;
  if ((minutes > 0) && ! is_away)
  {
    // are they that idle?  if so, set 'em away
    time_t now = time(NULL);
    if (pref.auto_away_seconds <= idleTime(now))
    {
      slfparam fl;
      fl.kind   = p_is_str;
      fl.strval = (char *) TOO_IDLE_AWAY_MSG;  // const char *, gcc 2.95, bleh.
      SLForumCmd::c_away(room, this, 1, &fl);  // !away idle
    }
  }
}


/** Change state in a well-defined manner.
 *  jer added 981005.1015
 */
void SLForumUser::setState (State newstate) {
  setState (newstate, false);
}

/** Change state and optionally keep substate...
 *  jer added 981006.1350
 *  (Always clears substate_tracks_dblchar)
 */
void SLForumUser::setState (State newstate, bool rememberSubstate) {
  if (state != newstate) {
    prevstate = state;
    state = newstate;
  }
  if (! rememberSubstate ) {
    substate  = 0;
    statedata = NULL;
    waittil   = (time_t) 0;
  }

  if (is_guest || (state != STATE_JER_GETPW))
    show_input = true;
  else
    show_input = false;

  substate_tracks_dblchar = false;
}


/** Util: change a name to all lowercase, and complain if
 *      any chars outside ascii 33-126 are used (return null).
 *	This assumes a valid connection (we're validating the
 *	name during connect), so the error message is sent
 *	to the user_fd.
 *
 *	If this _is_ valid, it makes a second pass and
 *	converts any uppercase chars to lowercase,
 *	in place; thus, the buffer pointed at by *tryname
 *	must be writeable.
 *
 * -- jer added 980805.2045
 */
char *SLForumUser::validateAndDowncaseName (char *tryname) {
  char c, *pc;

  if ((tryname == NULL) || (*tryname == '\0'))
	return NULL;

  /*-- pass 1: validate --*/
  for ( pc = tryname;
	*pc != '\0';
	pc++ ) {
    c = *pc;
    if (c == ' ') {
	Send ("You can't have a space in your name.");
	return NULL;
    }
    if ( c > 126) {
	Send ("You must use only 7-bit characters in your name.");
	return NULL;
    }
  }

  /*-- pass 2: convert upcase to downcase --*/
  for ( pc = tryname;
	*pc != '\0';
	pc++ ) {
    c = *pc;
    if ((c >= 'A') && (c <= 'Z'))
      *pc = tolower(c);	// to downcase!
    else
      switch (c) {
	case ':':
	case ';':
		*pc = '_';
		break;
	case '@':
		*pc = 'a';
		break;
    }
  }
  return tryname;
}



////////////////////////////////////////
// whisper list
//
////////////////////////////////////////



/* Set the recipient of messages sent by this user;
 * if NULL, broadcast messages.
 * If usr isn't logged in, this call is ignored.
 * This procedure also sets the prompt properly
 * to reflect the new setting.
 * Also, messages are automatically sent to other SLForumUsers
 * in the room with the same username.
 * jer added 980701.1500
 * 19990113.1412: changed to use to_bits and from_bits, not msgsTo
 */
void SLForumUser::sendMessagesTo (SLForumUser *usr)
{
  if ((usr != NULL) && (! usr->IsUserAccepted() ))
    return;

  sendMessagesToNone();
  sendMessagesAlso(usr);
}


// jer 19990113.1412
/** Send messages to this user _in addition to_ others being sent to. */
void SLForumUser::sendMessagesAlso (SLForumUser *usr)
{
    if (usr == NULL)    return;
    bool  non_away = false;
    const char *toname = usr->GetName();
    int   toidx = 0;

    if (usr == this) non_away = true;  // don't print "%s is away" for yourself

    while ( (usr = room->GetUserIncremental (toname, toidx, false))
	    != NULL )
    {
	talkingTo(toidx - 1, true);
	usr->talkingFrom (my_idx_in_room, true);
	if (! usr->is_away)
	    non_away = true;
    }

    if (! non_away)
    {
	char unbuf[64];
	snprintf(unbuf, 63, "Note: %s is away.", toname);
	Send(unbuf, 0, false);
    }
}



/** Notify all users we're sending messages to that we won't be anymore.
 *  See talkingTo, talkingFrom for dirty bitfield details.
 */
void SLForumUser::sendMessagesToNone () {
  int i;
  SLForumUser *u_to;
  if (n_talking_to == TALKING_TO_ALL)
    return;

  for ( i = 0; to_bits; to_bits = to_bits >> 1, ++i )
  {
    if (i >= MAX_CHAT_USERS)
    {
	    room->logprintf (" L4519: sendMessagesToNone: ASSERT FAILED: i=%d, MAX_CHAT_USERS=%d, to_bits=%llx\n",
			     i, MAX_CHAT_USERS, (unsigned long long) to_bits);
	    break;
    }
    if (0 == (to_bits & (userfield_t) 1))
      continue;
    u_to = room->GetUserByIndex(i);
    if (u_to == NULL)
      continue;
    u_to->talkingFrom (my_idx_in_room, false);
  }  // end for-each-in-to_bits

  n_talking_to = TALKING_TO_ALL;

  // reset the prompt to public.
  to_bits = (userfield_t) 0;  // just to be clear ;)
  setPromptFromTo(true);
  if (! running_cmd) {
    RedisplayEntryLine();
  }
}


/** Tell folks that're talking to us, that they shouldn't :-)
 */
void SLForumUser::sendMessagesFromNone () {
  int i;
  SLForumUser *u_to;

  for ( i = 0; from_bits; from_bits = from_bits >> 1, ++i )
  {
    if (i >= MAX_CHAT_USERS)
    {
	    room->logprintf (" L4551: sendMessagesFromNone: ASSERT FAILED: i=%d, MAX_CHAT_USERS=%d, from_bits=%llx\n",
			   i, MAX_CHAT_USERS, (unsigned long long) from_bits);
	    break;
    }
    if (0 == (from_bits & (userfield_t) 1))
      continue;
    u_to = room->GetUserByIndex(i);
    if (u_to == NULL)
      continue;
    u_to->talkingTo (my_idx_in_room, false);
  }  // end for-each-in-from_bits
}



/** Manipulating the to_bits and from_bits.
 *  This also sets the n_talking_to value, and sets the to/from
 *  prompt appropriately.
 *  stayte is whether to set or clear the bit
 *  (jer 19990113.1412)
 */
void SLForumUser::talkingTo (int unum, bool stayte) {

  if (unum < 0)  return;
  if (unum >= MAX_CHAT_USERS)
  {
	  room->logprintf (" L4562: ASSERT FAILED: MAX_CHAT_USERS=%d, unum=%d\n", MAX_CHAT_USERS, unum);
	  return;
  }
  // The cast ((userfield_t) 1) _before_ shift left is needed to ensure proper
  // operation of 64-bit userfield_t on 32-bit platforms.  (20020728.1015)
  if (stayte)
    to_bits |= ( ((userfield_t) 1) << unum);
  else
    to_bits &= (~ ( ((userfield_t) 1) << unum));

  setPromptFromTo();
}



void SLForumUser::talkingFrom (int unum, bool stayte) {
  if (unum < 0)  return;
  // The cast ((userfield_t) 1) _before_ shift left is needed to ensure proper
  // operation of 64-bit userfield_t on 32-bit platforms.  (20020728.1015)
  if (stayte)
    from_bits |= ( ((userfield_t) 1) << unum);
  else
    from_bits &= (~ ( ((userfield_t) 1) << unum));
}


bool SLForumUser::talkingTo (int unum) const {
  if (unum < 0)  return false;
  // The cast ((userfield_t) 1) _before_ shift left is needed to ensure proper
  // operation of 64-bit userfield_t on 32-bit platforms.  (20020728.1015)
  return (0 != (to_bits & ( ((userfield_t) 1)   << unum)));
}


bool SLForumUser::talkingFrom (int unum) const {
  if (unum < 0)  return false;
  return (0 != (from_bits & ( ((userfield_t) 1) << unum)));
}


// jer 19990113.1412
/** All users talking to me, now also talk to newguy; this is a private
 *  method used so that people talking to duplicate logins will talk to
 *  a newly logging-in person.  newguy_idx is the index in the room's array
 *  of users for the new guy.
 */
void SLForumUser::talkToMeToo (SLForumUser *newguy)
{
  if (newguy == NULL)
    return;

  int i;
  userfield_t ff = from_bits;  /* those talking to me */
  int newidx = newguy->idx_in_room();
  SLForumUser *u_from;

  // Move along the bitmask "ff" until we've seen each nonzero bit.
  // At each point, "i" is the current bit number (== some user index in room)
  // being examined.
  for ( i = 0; ff ; ff = ff >> 1, ++i )
  {
    if (0 == (ff & (userfield_t) 1))
      continue;      // this bit is 0, skip it
    u_from = room->GetUserByIndex(i);
    if (u_from == NULL)
      continue;      // that user has left?
    u_from->talkingTo (newidx, true);
    newguy->talkingFrom (i, true);
  }  // end for-each-in-from_bits
}




////////////////////////////////////////
// misc
//
////////////////////////////////////////

// 19990212.0830
/** Given the name of a command (without the initial '!' that marks it
 *  as a command), return the parameters and one-line description.  If the
 *  command isn't valid, return NULL for both of these.
 */
void SLForumUser::getHelpText (const char *cmdname,
			       const char **cmdparams, const char **cmddesc)
const
{
  getHelpText (cmdname, false, cmdparams, cmddesc);
}


/** getHelpText with optional partial-cmdname completion */
void SLForumUser::getHelpText (const char *cmdname, bool complete_partial,
			       const char **cmdparams, const char **cmddesc)
const
{
  int idx = find_cmd_index (cmdname, complete_partial);
  if (idx == -1) {
    *cmdparams = NULL;  *cmddesc = NULL;
    return;
  }
  *cmdparams = usercmds[idx].parmhelp;  *cmddesc = usercmds[idx].deschelp;

  return;
}


// 19990224.2315 dschin
/** Kludge fix so that the from: line is printed after c_help is called */
void SLForumUser::resetFrom () {
  lastm_rpt=9999;
}




// jer 19990922.2100
/** check if they've reached max lines/sec brake;
 *  return false if so.  If they haven't, increment
 *  counters and return true.
 */
bool SLForumUser::checkMaxLinesBrake() {
  time_t now = time(NULL);
  if ((now - lastline) >= ((time_t) MAX_LINES_SECS_FOR_AVG)) {
    // we're past the lines/sec history buffer length.
    // haven't said anything in a while.  We're good.
    for (int i = MAX_LINES_SECS_FOR_AVG - 1; i >= 0; --i)
      lines_per_sec[i] = 0;

    lines_idx = 0;
    lines_sum = 0;
    lastline = now;

  } else {

    if (now != lastline) {
      // new second.  zero out counters from then til now
      while (lastline < now) {
	++lines_idx;
	if (lines_idx >= MAX_LINES_SECS_FOR_AVG)
	  lines_idx = 0;
	lines_sum -= lines_per_sec[lines_idx];
	lines_per_sec[lines_idx] = 0;
	++lastline;
      }
    }
  }

  ++(lines_per_sec[lines_idx]);
  ++lines_sum;

  if (lines_per_sec[lines_idx] == 1)
    return true;  // allow a min of 1 line/sec

  if ((lines_per_sec[lines_idx] >= MAX_LINES_BURST) ||
      (lines_sum >= (MAX_LINES_SEC * MAX_LINES_SECS_FOR_AVG)))
    return false;  // hit the total, must wait.

  return true;
}




////////////////////////////////////////
// listen list
//
////////////////////////////////////////

// 19990215.1900
/** Manipulate the listening-to list */
void SLForumUser::listenListReset() {
  if (pref.listenlist != NULL) {
    llist_free (pref.listenlist);
    pref.listenlist = NULL;
  }
}


/** strdup()s the word, lowercases it, and adds it
 *  to the list of words.
 */
void SLForumUser::listenListAdd (const char *word) {
  char *wcopy;
  if ((word == NULL) || (*word == '\0'))	return;

  if (*(word+1) == '\0') {
    if (running_cmd)
      Send("\t-- sorry, no 1-letter words can be added to the listen list --");
    return;		// no 1-letter words, sorry (19990623.2330)
  }

  // first, search the list to see if it's already there, ret if not.
  if (pref.listenlist != NULL) {
    LinkedListLink *LL;
    for ( LL = llist_head (pref.listenlist);
	  LL != NULL;
	  LL = llist_next (pref.listenlist, LL) )
      if (0 == strcasecmp (word, (char *)llist_ldata(LL)))
	return;   // it's already in list
  }


  if (pref.listenlist == NULL) {
    pref.listenlist = llist_new(free);
    if (pref.listenlist == NULL) {
      room->Announce ("-- out of mem trying to make listen list --");
      return;
    }
  }

  wcopy = strdup (word);
  if (wcopy == NULL) {
    room->Announce ("-- out of mem in listenListAdd --");
    return;
  } else {
    // lcase it before adding
    register char *c;
    for (c = wcopy; *c != '\0'; ++c)
      *c = tolower(*c);
  }
  llist_add_end (pref.listenlist, wcopy);
}


/** does the line contain anything in the list?
 *  returns a pointer to the occurence, or NULL if none.
 *  Don't use this unless you have to, it's not too efficient,
 *  because ignore-public is not a common case.
 *  On return, 'mend' points to the char after the last char of the
 *  matching keyword, if any was found.
 */
char* SLForumUser::listenListMatch (const char *line, char **mend) const
{
  char *ldup, *pc;
  LinkedListLink *LL;
  if ((line == NULL) || (pref.listenlist == NULL))
    return NULL;
  ldup = strdup(line);
  if (ldup == NULL) {
    room->Announce ("-- out of mem in listenListMatch --");
    return NULL;
  }
  pc = ldup;
  for (pc = ldup; *pc != '\0'; ++pc)
    *pc = tolower(*pc);  // lowercase the input line for case-insens matching

  pc = NULL;
  for ( LL = llist_head (pref.listenlist);
	LL != NULL;
	LL = llist_next (pref.listenlist, LL) ) {
    pc = strstr (ldup, (char *)llist_ldata(LL));
    if (pc != NULL)
      break;
  }

  if (pc != NULL)
    pc = ( pc - ldup ) + (char *) line;  // make pc point within line, not ldup
  free(ldup);
  if ((LL != NULL) && (mend != NULL))
    *mend = pc + strlen ( (char *)llist_ldata(LL) ); // point past end of match
  return pc;
}





////////////////////////////////////////
// whisper ban list
//
////////////////////////////////////////

// 19990215.1900
/** Manipulate the ban-whispering-from list */
void SLForumUser::wbListReset() {
  if (pref.wblist != NULL) {
    llist_free (pref.wblist);
    pref.wblist = NULL;
  }
}


/** strdup()s the word, lowercases it, and adds it
 *  to the list of words.
 */
void SLForumUser::wbListAdd (const char *usrname)
{
  char *wcopy;
  if ((usrname == NULL) || (*usrname == '\0'))	return;


  // first, search the list to see if it's already there, ret if not.
  if (wbListMatch(usrname))
    return;

  if (pref.wblist == NULL) {
    pref.wblist = llist_new(free);
    if (pref.wblist == NULL) {
      room->Announce ("-- out of mem trying to make whisper list --");
      return;
    }
  }

  wcopy = strdup (usrname);
  if (wcopy == NULL) {
    room->Announce ("-- out of mem in wbListAdd --");
    return;
  }

  llist_add_end (pref.wblist, wcopy);
}


/** is the usrname in the whisperban list? */
bool SLForumUser::wbListMatch (const char *usrname) const
{
  if (pref.wblist != NULL) {
    LinkedListLink *LL;
    for ( LL = llist_head (pref.wblist);
	  LL != NULL;
	  LL = llist_next (pref.wblist, LL) )
      if (0 == strcasecmp (usrname, (char *)llist_ldata(LL)))
	return true;   // it's already in list
  }
  return false;
}






////////////////////////////////////////
// prefs
//
////////////////////////////////////////

void SLForumUser::loadPrefs() 
{
  if (is_guest)
    return;

  FILE *fp=NULL;
  char prefBuf[128];  prefBuf[127] = '\0';
  char *p;      // pointer into prefBuf
  int lineno = 1;
  const int MAX_FILE_LINES = (11+LISTENLIST_MAXSIZE+WBLIST_MAXSIZE+255); 
				 /* 11 params currently, plus # macros + blank
				  * lines? This maximum is to prevent denial-
				  * of-service attacks from a really long file,
				  * since nothing else goes on in the Forum
				  * while this is being read in.
				  */
  {
    char fn[PATH_MAX];  fn[PATH_MAX-1] = '\0';
    
    if (slfhomedir != NULL)
    {
      snprintf
	(fn, PATH_MAX-1, PREFSPATH, slfhomedir, PATH_SEP,
	 SLFUSERDIR, PATH_SEP, name);
    	// slfhomedir/userpref/slforum-%s
      fp= fopen(fn,"r");
    }

    if(fp==NULL)
    {
      snprintf (fn, PATH_MAX-1, PREFSPATH_fallback, name);
	// /tmp/.slforum-%s
      fp= fopen(fn,"r");
      if (fp==NULL)
	return;  // doesn't exist?  that's ok
    }

    // room->logprintf("Opened file %s\n",fname);
  }

  setState (STATE_LOAD_PREFS);

  slfparam params[2];

    // file line format: paramname\tparamval\n
  while (! feof(fp))
  {
    ++lineno;
    if (lineno > MAX_FILE_LINES)
    {
      Send ("Prefs file too long... has it been tampered with?", 0, true);
      fclose(fp);
      return;
    }
  // TODO TODO TODO should we move this to its own fxn?

    // read the first char to determine whether it's a macro or not...
    if (0 == fread (prefBuf, sizeof(char), 1, fp ))
    {   // io error
	fclose(fp);
	return;
    }
    
#ifdef ALLOW_MACROS
    if (prefBuf[0] == '$')
    {
	macro_t *newmac = readMacroFromFile (fp);
	if (newmac != NULL)
	{
	    tryAddMacro (newmac);
	}
	else
	{   // io error or out of memory in readMacroFromFile
	    fclose(fp);
	    return;
	}
    }
    else
    {
#endif /* ALLOW_MACROS */
	if ((prefBuf[0] != 13) && (prefBuf[0] != 10))
	{
  fgets(prefBuf+1,126,fp);     // Buffer one line
  // room->logprintf("  read from config file: %s\n",prefBuf);

    p = prefBuf;
    // read in the param name
    while(*p > ' ') {
      // TODO TODO TODO - need to check for invalid fieldnames on the way
      ++p;
    }
    if (*p != '\t') {
      char erbuf[80];
      *p = '\0';
      if (p != prefBuf)	// don't print msg for a blank line
      {
        if (-1 == snprintf
	    (erbuf,79,"No value for parameter %s, ignoring line.",prefBuf))
        {
	erbuf[80] = '\0';
        }
        Send (erbuf, 0, true);
      }
    } else {
      *p = '\0';  // turn the '\t' into a null to terminate paramname
      ++p;        // move past '\t' in buffer
      params[0].kind   = p_is_str;
      params[1].kind   = p_is_str;
      params[0].strval = prefBuf;
      params[1].strval = p;
      while((*p!='\n') && (*p!= '\r') && (*p != '\0')) {
	// rest of line is param
	++p;
      }
      *p = '\0';  // chomp CR/LF

      // room->logprintf("read in: '%s' <- '%s'\n", params[0].strval, params[1].strval);

      if ((params[0].strval[0] == '*') && (params[0].strval[1] == '\0'))
      {
	// '*' == listen word
	params[0].strval = (char *) "add";  // (gcc: cast from const char *)
	SLForumCmd::c_listen (room, this, 2, params);
      } else if ((params[0].strval[0] == '!') && (params[0].strval[1] == '\0'))
      {
	// '!' == whisperbanned user
	params[0].strval = (char *) "add";  // (gcc: cast from const char *)
	SLForumCmd::c_ban (room, this, 2, params);
      }	else {
	// pref
	SLForumCmd::c_zebra (room, this, 2, params);
      }
    }  // end if-param-has-value
	}  // end if-not-CR-or-LF
#ifdef ALLOW_MACROS
    }  // end if-not-macro-marker-char
#endif  /* ALLOW_MACROS */
  }  // end while-not-feof
  
  setState (prevstate);
  fclose(fp);

}



int SLForumUser::savePrefs()
{
  FILE *outp=NULL;
  char fn[PATH_MAX];  fn[PATH_MAX-1] = '\0';

  if(is_guest)
    return 0;

  if (slfhomedir != NULL)
  {
    snprintf(fn, PATH_MAX-1, PREFSPATH, slfhomedir, PATH_SEP,
	     SLFUSERDIR, PATH_SEP, name);
    	// slfhomedir/userpref/slforum-%s
    outp= fopen(fn,"w");
  }

  if(outp==NULL)
  {
    snprintf (fn, PATH_MAX-1, PREFSPATH_fallback, name);
	// /tmp/.slforum-%s
    outp= fopen(fn,"w");
    if (outp==NULL)
      return 0;
  }
  // room->logprintf("Opening %s and saving...\n",fn);

  // For "history"'s sake, these are in order (exc. cmd_char_alt)
  // of when the pref was added to slforum.

  fprintf(outp,"command\t%c\n",   pref.cmd_char);
  if (pref.cmd_char_alt != '\0')
      fprintf(outp,"altcmd\t%c\n",   pref.cmd_char_alt);
  fprintf(outp,"hereidle\t%d\n",  pref.idle_here_threshold);
  fprintf(outp,"autowho\t%d\n",   pref.maxAwayIdle/60);
  fprintf(outp,"trueidle\t%s\n", (pref.wants_true_idle ? "Yes" : "No"));
  fprintf(outp,"local\t%s\n",    (pref.local_mode ? "Yes" : "No"));
  fprintf(outp,"msgchk\t%d\n",   pref.auto_msgchk_minutes);
  fprintf(outp,"tabself\t%s\n",   (pref.tab_expand_incl_self?"Yes":"No"));
  fprintf(outp,"autoaway\t%d\n",   pref.auto_away_seconds/60);
  if (pref.jetlag != 0)
      fprintf(outp,"jetlag\t%d\n",   pref.jetlag);
  fprintf(outp,"_ignoringMask\t%d\n",pref.ignore_mask);
#ifdef ALLOW_TOPICS
  fprintf(outp,"autotopic\t%d\n", pref.maxTopicIdle/60);
#endif
  fprintf(outp,"rot10\t%s\n",   (pref.rot10_digits_too?"Yes":"No"));
  if (pref.rot_offset != 13)
    fprintf(outp,"rotamt\t%d\n", pref.rot_offset );
  if (pref.dice_n_sides != 6)
    fprintf(outp,"dsize\t%d\n", pref.dice_n_sides );
  fprintf(outp,"showaidle\t%s\n",    (pref.showaidle ? "Yes" : "No"));
  if (! pref.show_recap)
    fprintf(outp,"recap\t%s\n", (pref.show_recap ? "Yes" : "No"));

  // save listen list
  if (pref.listenlist != NULL)
  {
    LinkedListLink *LL;
    for ( LL = llist_head (pref.listenlist);
	  LL != NULL;
	  LL = llist_next (pref.listenlist, LL) )
      fprintf (outp, "*\t%s\n", (char *)llist_ldata(LL));
  }

  // save whisperban list
  if (pref.wblist != NULL)
  {
    LinkedListLink *LL;
    for ( LL = llist_head (pref.wblist);
	  LL != NULL;
	  LL = llist_next (pref.wblist, LL) )
      fprintf (outp, "!\t%s\n", (char *)llist_ldata(LL));
  }

#ifdef ALLOW_MACROS
  // save macros
  if (pref.macrolist != NULL)
  {
    LinkedListLink *LL;
    for ( LL = llist_head (pref.macrolist);
	  LL != NULL;
	  LL = llist_next (pref.macrolist, LL) )
      writeMacroToFile (outp, (macro_t *) llist_ldata(LL));
  }
#endif  /* def(ALLOW_MACROS) */

  fclose(outp);
  return 1;
}




////////////////////////////////////////
// telnet commands
//
////////////////////////////////////////

/** Got IAC, waiting for WILL/WONT/etc. (rfc854 telnet negotiations.) */
void SLForumUser::awaitTelCmd ()
{
  substate = (substate << 8) | state;
  state = STATE_TEL_CMD;
}


int SLForumUser::HandleTelCmd()
{
  char c;
  int rval = slfgetch(&c);
  if (rval<1)
    return rval;
  rval = 0;
  
  switch ((int) c)
  {
  case WILL:
  case WONT:
  case DO:
  case DONT:
    substate = (substate << 8) | c;
    state = STATE_TEL_OPT;
    break;

  case IAC:   /* doubled IAC == ascii 255 */
      if ((substate & 0xff) == ((int) STATE_GET_LINE))
	  rval = ManageInputNewChar(c, false);
      /* NO BREAK - FALL THROUGH to restore state */

  default:
    // ignore it (since rval==0); revert to prev state in any case.
    state = (enum SLForumUser::State) (substate & 0xff);
    substate = (substate >> 8);
  }
  return rval;
}


/* from RFC854:
   In summary, WILL XXX is sent, by either party, to indicate that
   party's desire (offer) to begin performing option XXX, DO XXX and
   DON'T XXX being its positive and negative acknowledgments; similarly,
   DO XXX is sent to indicate a desire (request) that the other party
   (i.e., the recipient of the DO) begin performing option XXX, WILL XXX
   and WON'T XXX being the positive and negative acknowledgments.  Since
   the NVT is what is left when no options are enabled, the DON'T and
   WON'T responses are guaranteed to leave the connection in a state
   which both ends can handle.  Thus, all hosts may implement their
   TELNET processes to be totally unaware of options that are not
   supported, simply returning a rejection to (i.e., refusing) any
   option request that cannot be understood.
*/
int SLForumUser::HandleTelOpt()
{
  char c;
  int rval = slfgetch(&c);
  if (rval<1)
    return rval;

  // reply properly:
  char willwont = (char) (substate & 0xff);

// buf.tel_bin
  if (c == TELOPT_BINARY)
  {
      willwont = negotiate_telopt_binary (willwont);
  } else
  {
      if (c == TELOPT_ECHO)
      {
	  switch (willwont)
	  {
	  case DO:   case DONT:  willwont = WILL;  break;
	  case WILL: case WONT:  willwont = DO;  break;
	  } 
      } else
	  willwont = 0;  

      /* all other option requests will be turned down. 
       * commented out -- still broken unfortunately

      switch (willwont)
      {
      case DO:   case DONT:  willwont = WONT;  break;
      case WILL: case WONT:  willwont = DONT;  break;
      } */
  }

  if (willwont != 0)
  {
      char response[3] = { (char) IAC, willwont, c };
      uWrite (response, 3);
  }

  // restore normal state/substate:
  // extract state from substate
  state = (enum SLForumUser::State) ((substate >> 8) & 0xff);
  // restore substate
  substate = (substate >> 16);
  return 0;
}


int SLForumUser::negotiate_telopt_binary (int willwont)
{
    /* try to enable telnet binary for 8-bit chars. */

    switch (willwont)
    {
    case WILL:
	if (buf.tel_bin_in)
	{ 
	    /* ignore it, since it's already set (see RFC854,
	     * consideration 3b)
	     */
	    willwont = 0;
	} else
	{
	    if (buf.tel_bin_in_hadit)
	    {
		/* ok, they're renegotiating in mid-connection */
		willwont = DO;  /* so affirm their offer */
	    } else {
		/* they're responding to our initial 'DO' request;
		 * dont reply
		 */
		willwont = 0;
	    }
	    /* either way, note that we now have 8-bit coming in */
	    buf.tel_bin_in = true;
	    buf.tel_bin_in_hadit = true;
	}
	break;
	
    case WONT:
	if (buf.tel_bin_in)
	{
	    buf.tel_bin_in = false;
	    willwont = DONT;
	} else
	    willwont = 0;
	break;
	
    case DO:
	if (buf.tel_bin_out)
	{ 
	    /* ignore it, since it's already set */
	    willwont = 0;
	} else
	{
	    if (buf.tel_bin_out_hadit)
	    {
		/* ok, they're renegotiating in mid-connection */
		willwont = WILL;  /* so affirm their offer */
	    } else {
		/* they're responding to our initial 'WILL' offer;
		 * dont reply
		 */
		willwont = 0;
	    }
	    /* either way, note that we now have 8-bit going out */
	    buf.tel_bin_out = true;
	    buf.tel_bin_out_hadit = true;
	}
	break;
	
    case DONT:
	if (buf.tel_bin_out)
	{
	    buf.tel_bin_out = false;
	    willwont = WONT;
	} else
	    willwont = 0;
	break;
	
    }  /* endsw */

    return willwont;
}



////////////////////////////////////////
// lock, unlock
//
////////////////////////////////////////

void SLForumUser::lock_me() /*public*/
{
#ifndef ALLOW_LOCKOUT
  return;
#endif
  if (is_locked)
    return;

  // check the file every time, in case their status changes

  user_can_lock = room->checkLockUserFile(this);
  if (user_can_lock != yes)
  {
    Send ("You do not have permission to lock.", 0, false);
    return;
  }

  is_locked = true;
  show_input = false;
  RedisplayEntryLine();
}

/** This is a private method. */
void SLForumUser::try_unlock_me() /*private*/
{
  if (is_guest)
    return;	/* can't unlock */

  struct slf_userinfo *ppw;
  ppw = validate(name, buf.buffer, longfrom);
  ClearBuffer();
  memset(buf.buffer, 0, sizeof(buf.buffer));
  if (ppw==NULL)
    return;

  is_locked = false;
  show_input = true;
  RedisplayEntryLine();
}


slf_tristate SLForumUser::userCanLock() const {
  return user_can_lock;
}



void SLForumUser::printLoginDetails (SLForumUser *uto) const
{
    char buf[USERBUFSIZ];  buf[USERBUFSIZ-1] = '\0';
    char idlebuf[20];
    time_t now = time(NULL);

    writeIdleBuf (idlebuf, now);
    if (idlebuf[0] == '\0') { strncpy (idlebuf, "active", 8); }

    snprintf (buf, sizeof(buf)-1, "-- %s is %s --", GetName(), idlebuf);
    uto->Send (buf, 0, false);

    const char *gueststr = (is_guest ? " (Guest)" : "");

    {
	char logintimebuf[25];
	uto->writeTime (logintimebuf, sizeof(logintimebuf), loginTime(), 2);
	snprintf (buf, sizeof(buf)-1, "Here since %s%s",
		  logintimebuf, gueststr);
    }
    uto->Send (buf, 0, false);

    snprintf (buf, sizeof(buf)-1, "Login from %s %s",
	      inet_ntoa(loginfromip), loginFrom());
    uto->Send (buf, 0, false);

    if (is_away) {
	const char *awaymsg = getAwayMessage();
	if (awaymsg == NULL) awaymsg = "";
	if (awaymsg[0] != '\0') {
	    snprintf (buf, sizeof(buf)-1, "AWAY: %s", awaymsg);
	    uto->Send (buf, 0, false);
	}
    }
}


/** Write this user's idle/away status to an "idle buffer" for output */
void SLForumUser::writeIdleBuf
    (char idlebuf[20], time_t now, int *u_idletime /* =NULL */ )
    const
{
	const char *status = "-unset-";  // this should never show up

	/* 19991012.0100 dschin
	 * added the status part, reflecting awayness vs. idleness.
	 * added the status param to the idlebufs below.
	 */

	time_t tnow = now;  // local ref. for idleTime()
	int idletime;
  	
	idletime = (idleTime(tnow) + 30) / 60;
	if (u_idletime != NULL) *u_idletime = idletime;  // return idle time
	// (for room::ListUsers' benefit)

	if (idletime > 0) {
	    if (isUserAway()) {
		status = "AWAY";
	    } else {
		status = "idle";
	    }

	    if (idletime >= 180)
		if (idletime >= 60*96)
		    sprintf (idlebuf, "%s %3d dy", status, (idletime / (60*24)));
		else
		    sprintf (idlebuf, "%s %3d hr", status, (idletime / 60));
	    else
		sprintf (idlebuf, "%s %3d min", status, idletime);
	} 
	else   /* no idle time */
	{
	    if (isUserAway()) 
		strcpy  (idlebuf, "AWAY");
	    else
		idlebuf[0] = '\0';  // no idle, so blank.
	}
}





////////////////////////////////////////
// macros
//
////////////////////////////////////////


#ifdef ALLOW_MACROS


extern "C" {
  void free_macro_list_item(void *macro_struct_to_free)
  {
    if ( macro_val (macro_struct_to_free) != NULL)
      free ( (void*) macro_val (macro_struct_to_free) );
    free (macro_struct_to_free);
  }


}  // end "extern C"



LinkedListLink* SLForumUser::tryFindMacro
	(char mc, LinkedListLink **LLprev /* =NULL */ ) const
{
    // linear search.  crude but effective.  Consider:
    // - ordered linked lists are conceptually simple.
    // - users don't need to use macros often enough to make it a critical path
    // - users' macro lists aren't long enough to justify another strategy

    if (LLprev != NULL)
	*LLprev = NULL;  // set the pointer that LLprev points to, if any

    if (pref.macrolist == NULL)
	return NULL;

    LinkedListLink* LL = llist_head (pref.macrolist);
    void *mcdata;
    while (LL != NULL)
    {
	mcdata = llist_ldata(LL);
	if (macro_key(mcdata) == mc)
	    return LL;         // this is it
	if (macro_key(mcdata) > mc)
	    return NULL;       // ordered list; thus, we've passed where
	                       // it would've been

	// ok, move to next link.

	if (LLprev != NULL)
	    *LLprev = LL;      // remember previous, if asked to

	LL = llist_next (pref.macrolist, LL); // try next link
    }
    return NULL;  // fell off end of the list
}


/** ret true if worked, false if already had one there.  In the macro value,
 *  use MACRO_RS to separate lines, and MACRO_SUB for the cmd char.
 *  (These are declared near the top of slforumuser.cc)
 *  newmac->val should be malloc'd, not a pointer to a static array that won't
 *  contain the same thing later.
 */
bool SLForumUser::tryAddMacro(macro_t *newmac)
{
  LinkedListLink *LL = NULL;  // we'll add after this one (if null, list front)

    // first, search the list to see if it's already there
    if (NULL != tryFindMacro(newmac->key, &LL))
	return false;         // and return if it is

    // now we'll add it.  But first, see if we need to make the macro list

    if (pref.macrolist == NULL)
    {
	pref.macrolist = llist_new(free_macro_list_item);
	LL = NULL;
	if (pref.macrolist == NULL) {
	    room->Announce ("-- out of mem trying to make macro list --");
	    return false;
	}
    }

    if (-1 != llist_add_after (pref.macrolist, LL, newmac))  // add our new one
	return true;
    else
	return false;  // out of memory
}


bool SLForumUser::tryDelMacro(char mc)
{
  LinkedListLink *LL = NULL;  // we'll add after this one (if null, list front)

  // first, search the list to see if it's already there
  if (NULL == tryFindMacro(mc, &LL))
      return false;

  void *idata;
  // try to del it
  if (LL == NULL)
      idata = llist_remove_start (pref.macrolist);
  else
      idata = llist_remove_after (pref.macrolist, LL);

  if (idata == NULL)
      return false;

  free_macro_list_item(idata);
  return true;
}


/** returns 0 for ok, -1 for discon, 1 for macro error.
 *  "toplevel" will be true for calling from the user, false for
 *  a recursive call (^E inside a macro)
 */
int SLForumUser::tryExpandMacro (char mc, bool toplevel)
{
    // try to find it in the user's macro list.
    //    For backwards-compat, don't reject esc or backspace until
    //    after we've seen if the user has a macro on those keys.
    //    (Cannot create or edit such a macro slf1.9.7-devel14 or later)
    LinkedListLink* LL = tryFindMacro (mc);
    if (LL == NULL)
    {
        if ((mc == 27) || (mc == 8)) // esc or backspace -- 20060610.1445
	{
	    Send("Macro expand cancelled.", 0, false);
	} else {
	    char macroerr[64];
	    snprintf (macroerr, 64, "No macro is defined for ^E %s",
		      keyDisplayString(mc, true));
	    Send (macroerr, 0, false);
	}
	if (user_fd != -1)
	    return 1;
	else
	    return -1;
    }

    const macro_t* mac = (const macro_t*) llist_ldata(LL);
    if (macro_editing(mac))
    {
	Send ("The macro exists but is being edited.");
	if (user_fd != -1)
	    return 1;
	else
	    return -1;
    }

    // if it's there, run it... but first some setup:

    // check recursion depth
    if (macro_next_level >= MAX_MACRO_CALL_LEVEL)
    {
	char macroerr[64];
	snprintf
	    (macroerr, 64, "Recursing too deep (%d levels) to expand ^E %s",
	     MAX_MACRO_CALL_LEVEL, keyDisplayString(mc, true));
	Send(macroerr, 0, false);
	if (user_fd != -1)
	    return 1;
	else
	    return -1;
    }

    // check for reentrancy
    for (int i = macro_next_level-1; i >= 0; --i)
    {
	if (macrostack[i] == mc)
	{
	    char macroerr[64];
	    snprintf (macroerr, 64, "Cannot recursively expand ^E %s",
		      keyDisplayString(mc, true));
	    Send (macroerr, 0, false);
	    if (user_fd != -1)
		return 1;
	    else
		return -1;
	}
    }

    int retval;

    // make the entry in the stack, and move the stack pointer
    macrostack[macro_next_level] = mc;
    ++macro_next_level;

    if (toplevel)  // need to display current buffer first
	uWrite (buf.buffer);

    if (toplevel)  // set the expansion flag
	running_macro = true;

    // expand the macro
    retval = macro_really_expand (mac);
    if (user_fd == -1)
	retval = -1;

    if (toplevel)  // unset the expansion flag
	running_macro = false;

    // move the stack pointer back :)
    --macro_next_level;
    macrostack[macro_next_level] = '\0';

    // and return w/ status.

    return retval;
}


/** returns 0 for ok, -1 for discon, 1 for macro error */
int SLForumUser::macro_really_expand ( const macro_t* mac )
{
    if (mac == NULL)
	return 0;  // just in case

    // OK, we run this very simply...
    // step through the macro and put its chars in the buffer, except
    // for MACRO_SUB and MACRO_RS and the macro char ^E.
    // Then we check hit_enter_afterwards.

    char *c;
    int retval = 0;

    for (c = (char*) mac->val; *c != '\0'; ++c)
    {
	switch(*c)
	{
	case MACRO_SUB:
	    retval = ManageChangedInputBuffer(ManageInputNewChar(pref.cmd_char));
	    break;
	case MACRO_RS:
	    retval = ManageChangedInputBuffer(ManageInputNewChar('\r'));
	    break;
	case (char) 5:  // ^E
	    ++c;
	    retval = tryExpandMacro(*c, false);
	    break;
	default:
	    retval = ManageChangedInputBuffer(ManageInputNewChar(*c));
	}
	if (retval != 0)  // managechangedinputbuffer rets 0 or -1
	    return retval;  // tryexpand macro also may ret 1 for macro failure
    }  // end for

    if (mac->hit_enter_afterwards)
    {
	retval = ManageChangedInputBuffer(ManageInputNewChar('\r'));
    }

    return retval;

}  // macro_really_expand


/** Append this line to the macro currently being written; along the way,
 *  transform the cmd char to MACRO_SUB, etc.
 */
void SLForumUser::macro_append_line()
{
    // macro's size so far
    size_t sofar = ((char *)statedat2) - ((char*) macro_val(statedata));

    if ((sofar + buf.cur_pos + 3) > MAX_MACRO_LENGTH)  // assumes past 1st line
    {
	char erbuf[80];
	snprintf(erbuf,80,"Macro too long; you have room for %d more chars.",
		 MAX_MACRO_LENGTH - sofar - 3 );
	Send(erbuf, 0, false);
    } else {

	// first, xform cmd char to MACRO_SUB char at start of line
	{
	    char *uc;

	    // skip whitespace and any ^U's
	    for( uc = buf.buffer; ((*uc == ' ') || (*uc == ((char)21))); ++uc )
		{}
	    // look for cmd or alt char
	    if (*uc == pref.cmd_char)
		*uc = MACRO_SUB;
	    else if ((pref.cmd_char_alt != '\0') && (*uc == pref.cmd_char_alt))
		*uc = MACRO_SUB;
	}

	// now append it to the macro
	{
	    // This char** points to statedat2, which contains
	    // a char* to a further buffer.  The chicanery of making
	    // another pointer to it, rather than directly manipulating
	    // statedat2, is made necessary due to gcc "ANSI C++" warnings.

	    char **sd = (char **) (&statedat2);

	    if (sofar > 0)  // needs newline
	    {
		**sd = (char) MACRO_RS;
		++(*sd);
	    }
	    memcpy (statedat2, buf.buffer, buf.cur_pos);
	    *sd += buf.cur_pos;
	}
    }
}


void SLForumUser::listCurrentMacros()
{
    if ((pref.macrolist == NULL) || (llist_length(pref.macrolist) == 0))
    {
	Send("No macros currently defined.", 0, false);
	return;
    }

    Send("----- Macro Listing -----", 0, false);
    Send("Key:    Value:", 0, false);
    char llbuf[USERBUFSIZ];  llbuf[USERBUFSIZ-1] = '\0';
    
    LinkedListLink* LL = llist_head (pref.macrolist);
    void *ldata;
    size_t maxlen;              // space left on this (output) line
    char *dc;  const char *sc;  // dest, source char
    bool trailing_whitespace = false;

    while (LL != NULL)
    {	
	ldata = llist_ldata(LL);
	snprintf (llbuf, USERBUFSIZ-1, "%-7s ",
		  keyDisplayString(macro_key(ldata), true));
	
	dc = llbuf + strlen(llbuf);
	sc = macro_val(ldata);
	maxlen = 78 - promptlen - strlen(llbuf);

	const char *nextc;
	size_t clen;

	while (*sc != '\0')
	{
	    if (*sc == ' ')
	    {
		if (! trailing_whitespace)
		{
		    // check from here til end of line (or of macro) --
		    // if there's nothing but whitespace til then, we should
		    // show the space chars.
		    const char *wsc;
		    for ( wsc = sc;
			  *wsc == ' ';
			  ++wsc)
		    {}
		    trailing_whitespace =
			((*wsc == '\0') || (*wsc == MACRO_RS));
		
		}
		if (trailing_whitespace)
		    nextc = keyDisplayString(' ', true);
		else
		    nextc = " ";  // just show it as a space char,
				// there's non-whitespace ahead.
	    } else
	    {
		if (*sc == (char) IAC)
		    nextc = telnet_double_iac;
		else if (*sc == MACRO_SUB)
		    nextc = keyDisplayString(pref.cmd_char);
		else		    
		    nextc = keyDisplayString(*sc);
	    }

	    clen = strlen(nextc);

	    if (clen <= maxlen)
	    {
		memcpy(dc, nextc, clen);
		dc += clen;
		maxlen -= clen;
		if (*sc == MACRO_RS)
		    maxlen = 0;    // force newline after MACRO_RS
	    }
	    if (clen > maxlen)
	    {
		// end of line reached.
		// Send what we have so far.
		*dc = '\0';
		Send ((char*)llbuf, 0, false);

		// and build a new one.
		strcpy (llbuf, "        ");  // 8 spaces to line up
		dc = llbuf + 8;
		if ((*sc != MACRO_RS) || (maxlen>0))
		{
		    memcpy(dc, nextc, clen);
		    dc += clen;
		} else
		    clen = 0;  // since we put that before the newline

		maxlen = 78 - promptlen - clen - 8;
	    }
	    ++sc;
	}  // end while

	if (macro_crlf(ldata))
	{
	    nextc = keyDisplayString(MACRO_RS);
	    clen  = strlen(nextc);
	    memcpy(dc, nextc, clen);
	    dc += clen;
	}

        *dc = '\0';

	Send ((char*)llbuf, 0, false);  // print out the macro val
	
	LL = llist_next (pref.macrolist, LL); // next link
    }
}


void SLForumUser::clearCurrentMacros()
{
    if ((pref.macrolist == NULL) || (llist_length(pref.macrolist) == 0))
    {
	// done :)
    } else {
	llist_free (pref.macrolist);
	pref.macrolist = NULL;
    }
    Send ( "Macro list cleared.", 0, false );
}




#endif /* def(ALLOW_MACROS) */


/* opposite of keyDisplayString... this routine is fairly strict,
 * and assumes its input is the output of keyDisplayString.
 * (It's meant to be used in loading macros from the prefs file)
 * Note that <CR> returns MACRO_RS, which is correct for
 * separating lines within a macro, but if you're reading in the key for
 * the macro and the macro is ^E <CR>, you'll need to check for MACRO_RS
 * and change that to (char)13 .
 * Any unrecognized keystring will be returned as '\0' .
 */
char SLForumUser::keystringChar(const char* keyster) const
{
    register char ret;

    ret = keyster[0];
    switch (ret)
    {
    case '^':
	if (keyster[1] != '\0')
	{
	    ret = (keyster[1] + (char) 1) - 'A';
	    if (ret > 31)
		ret = '\0';
	}
	break;

    case '\\':
	// This if-statement DEPENDS on short-circuit boolean eval.
        if ((keyster[1] != '0') ||
	    (keyster[2] == '\0') || (keyster[3] == '\0') ||
	    (keyster[4] == '\0') || (keyster[5] != '\0'))
	    ret = '\0';
	else
	    ret = (((keyster[2] - '0') & 07) << 6)
		| (((keyster[3] - '0') & 07) << 3)
		| (((keyster[4] - '0') & 07));
	break;

    case '<':
	switch (keyster[1])
	{
	case '\0': ret = '<';   // just a plain left-angle-bracket
	    break;
	case 't':  ret = '\t';  // only <tab> starts with t
	    break;
	case 'N':  ret = '\0';  // only <NUL> starts with N
	    break;
	case 'l':  ret = '<';   // only <lt> starts with l
	    break;
	case 's':  ret = ' ';   // only <sp> starts with s
	    break;
	case '^':  ret = '^';   // only <^> is ^
	    break;
	case '\\': ret = '\\';  // <only <\> is backslash
	    break;
	case 'c':  ret = MACRO_SUB;  // only <cmd> starts with lowercase c
	    break;
	case 'C':  ret = MACRO_RS;   // <CR> as line sep within a macro
	    break;
	case 'e':  ret = (char) 27;  // only <esc> starts with e
	    break;
	default:   ret = '\0';   // who knows
	}
	break;

    default:
	{}   // just keep it as is
    }  /* endsw */

    return ret;
}


/* If you change this function, change keyDisplayLength and keystringChar,
 * and if you change the length of the longest <oneofthese> change the
 * declarations around "char minibuf" in readMacroFromFile.
 */
const char* SLForumUser::keyDisplayString
	(char mc, bool even_space /* =false */ )
{                              
    switch (mc)
    {
    case (char)13:
    case MACRO_RS:
	    return "<CR>";
    case MACRO_SUB: return "<cmd>";  // the command character, usually '!'
    case (char) 9:  return "<tab>";
    case (char)27:  return "<esc>";
    case (char) 0:  return "<NUL>";
    case '<':       return "<lt>";
    case '^':       return "<^>";
    case '\\':      return "<\\>";
    case ' ':
	if (even_space)
	    return "<sp>";  /* usually not used */

    }

    if ((( ((int)mc) >= 32) && (((int) mc) <= 126))
	|| (((int) mc) == 128)
	|| (((int) mc) >= (128+32))
	    )
    {
	macroNameBuf[0] = mc;	 /* normal chars          */
	macroNameBuf[1] = '\0';
    } else if (mc < 33) {
	macroNameBuf[0] = '^';   /* ctrl chars ^A-^Z, etc */
	macroNameBuf[1] = (mc + 'A' - (char) 1);
	macroNameBuf[2] = '\0';
    } else {
	macroNameBuf[0] = '\\';  /* octal chars \0177 etc */
	macroNameBuf[1] = '0';
	macroNameBuf[2] = (((mc >> 6) & 07) + '0');
	macroNameBuf[3] = (((mc >> 3) & 07) + '0');
	macroNameBuf[4] = ((mc & 07) + '0');
	macroNameBuf[5] = '\0';
    }
    return macroNameBuf;
}  /* end keyDisplayString */


size_t SLForumUser::keyDisplayLength(char mc, bool spaces_too /* =false */ )
{
    switch (mc)
    {
    case ' ':
	if (!spaces_too)
	    return 1;
    case (char)13:
    case MACRO_RS:
    case (char) 9:
    case (char)27:
    case '<':
    case (char) 0:
	return strlen (keyDisplayString(mc,spaces_too));  /* anything else */
    default:
	if (((mc >= (char) 32) && (mc <= (unsigned char) 126))
	    || ( ((unsigned char) mc) == 128)
	    || ( ((unsigned char) mc) >= (unsigned char) (128+32))
		)
	    return 1;        /* normal chars */
	else if (mc < 32)
	    return 2;        /* ctrl chars   */
	else
	    return 4;        /* octal-codes  */
    }  /* endsw */
}  /* keyDisplayLength */


size_t SLForumUser::stringDisplayLength
	(const char *txt, int *strlenvar /* =NULL */ ,
	 bool spaces_too /* =false */ )
{
    size_t strlenn = (size_t) 0;
    size_t len = (size_t) 0;
    char mc;

    if (! buf.macro_input)
    {
	len = strlenn = strlen(txt);
    } else {
	for ( mc = *txt; mc != '\0'; ++txt, mc=*txt, ++strlenn )
	    len += keyDisplayLength(mc, spaces_too);
    }

    if (strlenvar != NULL)
	*strlenvar = strlenn;

    return len;
}  /* end stringDisplayLength */


#ifdef ALLOW_MACROS

  void SLForumUser::writeMacroToFile (FILE *f, macro_t *mac)
  {
      // macro key , and 1 or 0 for hit-enter-afterwards setting
      fprintf (f, "$\t%s %c ",
	       keyDisplayString (macro_key(mac), true),
	       ( macro_crlf(mac) ? '1' : '0' ) );

      // macro value
      for (const char *c = (const char *) macro_val(mac); *c != '\0'; ++c)
	  fprintf (f, keyDisplayString(*c));      

      // and finally, end of line
      fprintf (f, "\n");
  }


  /** Assumes the initial '$' has been read, signifying a macro.
   *  Will read up to and including newline, and return a macro.
   *  If an IO error occurs or out of memory, returns NULL.
   */
  macro_t *SLForumUser::readMacroFromFile (FILE *f)
  {
      bool ioerror = false;
      macro_t *mac;
      mac = (macro_t *) malloc (sizeof(macro_t));
      if (mac != NULL)
      {
	  macro_editing(mac) = false;  // initialize _that_ member

	  // now for the rest...

	  char macbuf[MAX_MACRO_LENGTH];
	  char *bufpos;  int remain;

	  // first read the tab after '$'
	  if (0 == fread (macbuf, sizeof(char), 1, f))
	  {
	      // io error
	      free (mac);
	      return NULL;
	  }

	  // now, read the keystring for the macro's key til ' '
	  remain = MAX_MACRO_LENGTH - 1;
	  bufpos = macbuf;
	  while (remain > 0)
	  {
	      if (0 == fread (bufpos, sizeof(char), 1, f))
	      {
		  ioerror = true;
		  break;
	      }
	      if ((*bufpos == ' ') || (*bufpos < (char) 32))
		  break;
	      ++bufpos;  --remain;
	  }
	  *bufpos = '\0';  // properly terminate the string

	  // now translate the keystring to an ascii value
	  // and store that as the macro key
	  macro_key(mac) = keystringChar (macbuf);

	  // done with key.  Now read 1 or 0 for macro_crlf
	  if (0 == fread (macbuf, sizeof(char), 1, f))
	  {
	      // io error
	      free (mac);
	      return NULL;
	  }
	  macro_crlf(mac) = (macbuf[0] == '1');

	  // now read the space after macro_crlf
	  if (0 == fread (macbuf, sizeof(char), 1, f))
	  {
	      // io error
	      free (mac);
	      return NULL;
	  }
	  
	  // Now read the macro value til EOLN into macbuf
	  remain = MAX_MACRO_LENGTH - 1;
	  bufpos = macbuf;
	  while ((remain > 0) && ! ioerror)
	  {
	      // inner loop to read into keystringbuf
	      // (yes, this is hairy) so we can decode it
	      // (a keystring) to the proper ASCII value
	      // and add _that_ to the macro value (in macbuf).
	      char keystringbuf[8];  // 8 is length + 1 of the longest
	                             // "<keystring>" from keyDisplayString.
	      char *ks;  int ksremain;
	      ksremain = (sizeof(keystringbuf)/sizeof(char)) - 1;
	      ks = keystringbuf;

	      // read the first char, which will tell us how many more
	      // we need to read.
	      if (0 == fread (ks, sizeof(char), 1, f))
	      {
		  ioerror = true;
		  break;
	      }

	      // now we'll know what to do...
	      // determine how many more chars to read.
	      // (This code is sensitive to keyDisplayString's
	      //  output format.)
	      switch (*ks)
	      {
	      case (char) 13:
	      case (char) 10:  /* EOLN */
		  ksremain = 0;
		  break;
	      case '^':        /* ctrl chars ^A-^Z, etc */
		  ksremain = 1;  break;
	      case '\\':       /* octal chars \0177 etc */
		  ksremain = 4;  break;
	      case '<':        /* <something> */
		  /* don't fall through to default */; break;
	      default:         /* anything else */
		  ksremain = 0;
	      }

	      if (*ks < (char) 32)
	      {
		  // ok, end of line.  Make it a zero-length string.
		  *ks = '\0';  remain = 0;  // signal the end
	      }
	      else if (*ks != '<')
	      {
		  // normal reading. # chars determined above.

		  ++ks;  // so we read into the next char in buffer

		  while (ksremain > 0)
		  {
		      if (0 == fread (ks, sizeof(char), 1, f))
		      {
			  ioerror = true;
			  break;
		      }
		      if (*ks < (char) 32)  // eoln
			  break;
		      ++ks;  --ksremain;
		  }
		  *ks = '\0';  // properly terminate the string

	      } else {
		  // reading a keystring inside <angle brackets>;
		  // keep going til we hit the right bracket '>'

		  ++ks;  // so we read into the next char in buffer

		  while (ksremain > 0)
		  {
		      if (0 == fread (ks, sizeof(char), 1, f))
		      {
			  ioerror = true;
			  break;
		      }
		      if (*ks < (char) 32)
			  break;
		      if (*ks == '>')
		      {
			  ++ks;  // so we don't put a \0 where our '>' is
			  break;
		      }
		      ++ks;  --ksremain;
		  }
		  *ks = '\0';  // properly terminate the string
	      }

	      if  ((! ioerror) && (keystringbuf[0] != '\0'))
	      {
		  // now translate the keystring to an ascii value
		  // and put that in our ongoing macro value buffer
		  *bufpos = keystringChar (keystringbuf);

		  // and move on.
		  ++bufpos;  --remain;
	      }
	  }  // wend
	  *bufpos = '\0';  // properly terminate the macro value string

	  if (ioerror)
	  {
	      free (mac);
	      return NULL;
	  }

	  // make a copy of the string, since the buffer is local...
	  {
	      const char *macval;
	      macval = strdup (macbuf);
	      if (macval == NULL)
	      {
		  free (mac);
		  return NULL;
	      }

	      macro_val(mac) = macval;
	  }
      } // end if (mac ! null)

      return mac;

  }  /* readMacroFromFile */

#endif /* def ALLOW_MACROS */


/** Write time to a buffer, taking heed of this user's timezone offset.
 *  "incldate" 0=time; 1=time,date; 2=time,date,year.
 */
void SLForumUser::writeTime
  (char *buf, size_t bufsize, time_t whicht, int incldate /* =0 */ )
	const
{
    whicht += (pref.jetlag * 60 * 60);  // allow for timezone offset
    _writetime (buf, bufsize, whicht, incldate);
}



/** Expand the time tokens \201 and \202 to the _expandBuf temp buffer.
 *  Uses user's timezone.  idx should be the first \201 or \202 seen
 *  within srcp, but can be NULL to search here for the first.
 *  \201 = HH:MM, \202 = DD MMM HH:MM.
 *  Returns _expandBuf, OR, if no \201 or \202 found (idx NULL) returns srcp.
 *  Used by recap feature and ::Send.
 */
char* SLForumUser::expandTimeTokens
	(time_t now, const char *srcp, const char *idx)
{
  if (idx == NULL)
  {
    idx = index(srcp, '\201');
    if (idx==NULL)
    {
      idx = index(srcp, '\202');
      if (idx == NULL)
	return (char *) srcp;  // discard const in case caller wants to chg it
    }
  }

    /* This code shared with room::logprintf; if you change it here,
     * change it there.
     */

	char *destp = _expandBuf;
	size_t destleft = USERBUFSIZ-1;
	char *enddest = destp + destleft - 1;
	while ((idx != NULL) && (destleft > 1) && (*srcp != '\0'))
	{
		// first copy everything up to but not incl the \201 or \202
		memcpy (destp, srcp, (idx - srcp));
		// and update pointers
		destp += (idx - srcp);  destleft -= (idx - srcp);
		srcp += (idx - srcp);
		// now write the timestamp
		writeTime (destp, destleft, now, (*idx == '\202') ? 1 : 0);
		// and update dest pointer
		if (*idx == '\201') {
		    destp += 5;  destleft -= 5;
		} else {
		    destp += 12;  destleft -= 12;
		}
		++srcp;

		// and re-null-terminate dest
		if (destp <= enddest)
		    *destp = '\0';
		else {
		    *enddest = '\0';
		    destleft = 0;
		}

		// now look for the next \201 or \202 if any
		{
		    ++idx;
		    char *id201 = index (idx, '\201');
		    char *id202 = index (idx, '\202');
		    if (id201 == NULL)
			if (id202 == NULL)
			    idx = NULL;
			else
			    idx = id202;
		    else
			if (id202 == NULL)
			    idx = id201;
			else
			    if (id201 < id202)
				idx = id201;
			    else
				idx = id202;
		}
	}  // end while
	    
	if (destleft > 0) {
	  destp[destleft] = '\0';  // defensive final nullterm
	  strncpy (destp, srcp, destleft);
	} else
	  *enddest = '\0';         // normal final nullterm

	return _expandBuf;

  /* shared timestamp expand code ends */
}



/** Announce an hourly, daily, or yearly chime.
 *  Contents of 'ptime' used when pref.jetlag == 0.
 *  Otherwise, 'now' is used to recalc after timezone offset,
 *  and ptime contents are ignored, ptime structure is used
 *  as a temporary workspace instead.
 */
void SLForumUser::hourlyChime (time_t now, const struct tm *ptime, int msgflag)
{
    if (pref.jetlag != 0)
    {
	now += (60 * 60 * pref.jetlag);  // adjust for jetlag
	ptime = localtime(&now);  // get hours, mins, etc
	if (ptime == NULL) {
	    room->Announce ("Assert failed in CheckHourChime: USER:L6036: ptime is NULL");
	    return;
	}
    }

   char s[64];
   //    sprintf(s, "Dong!  The time is now %d:00", ptime->tm_hour);
   if (ptime->tm_hour != 0)
     sprintf (s, "The time is now %d:00.", ptime->tm_hour);
   else
     if ((ptime->tm_mday != 1) || (ptime->tm_mon != 0))
	strftime (s, 63, "The time is now midnight, start of %A, %d %B.", ptime);
     else
	strftime (s, 63, "The time is now midnight!  Happy New Year %Y!", ptime);

   Send ( msgflag, s, false );

   if (pref.jetlag != 0)   // set it back to what other users expect ;)
   {                      // since "ptime" is probly a static area.
	now -= (60 * 60 * pref.jetlag);  // adjust for jetlag
	ptime = localtime(&now);  // get hours, mins, etc
	if (ptime == NULL) {
	    room->Announce ("Assert failed in CheckHourChime: USER:L6043: ptime is NULL");
	    return;
	}
    }
}


////////////////////////////////////////
// debug
//
////////////////////////////////////////

void SLForumUser::debug_printSlotStatus() const
{
  room->logprintf ("# Slot %02d: user_fd=%d, state=%d, name='%s' subs=0x%x cur_pos=%d cur_len=%d macro_input=%d waitt=%ld promptlen=%d prompt='%s'\n",
		   my_idx_in_room, user_fd, state, name, substate,
		   buf.cur_pos, buf.cur_len, buf.macro_input, waittil,
		   promptlen, prompt);
}



////////////////////////////////////////
// end class SLForumUser
//
////////////////////////////////////////

// -- commented out 20020704.1210 after authlocal fixed
#if 0
// appends to /tmp/slf-log-authfail.txt
static void dumpAuthlFailLogInfo
	(unsigned long loip,
	 unsigned int uport, unsigned int dport)
{
	fprintf (stderr, "L6280: got here\n");

	int fd = open ("/tmp/slf-log-authfail.txt",
		       O_WRONLY | O_CREAT | O_APPEND);
	if (fd == -1)
	{
		perror ("could not open slf logfile");
		return;
	}

	char buf[512]; buf[512] = '\0';
	time_t now = time(NULL);
	int charcount = 
		snprintf
		(buf, 511, "\nL6275: authlocal(localip=%lu, uport=%u (0x%x), dport=%u) fail.\nTime: %s/proc/net/tcp follows.\n",
		 loip, uport, uport, dport,
		 ctime (&now) );
	write (fd, buf, charcount);

	fprintf (stderr, "L6288: got here\n");

	slf_send_file ( fd, "/proc/net/tcp" );
	fflush(stderr);
	fprintf (stderr, "L6292: got here\n");

	write (fd, "/proc/net/tcp ends.\n\n\n\n", 21);

	close (fd);

}
#endif      // 20020704.1210 -- commented out, authlocal has been fixed


/* write a file to socket fd (jer);
 * on error, returns -1 and sets perror.  if fd = -1,
 * returns 0.
 */
/* commented out 20020704.1210 after authlocal fixed -- not currently used */
#if 0
static int slf_send_file ( int fd, const char *fname )
{
  FILE *diskf;
  int done=0;

  if (fd == -1)
    return 0;
  diskf = fopen (fname, "r");
  if (diskf == NULL)
    return -1;

  /* jer 981023.0320.... since we have evidence we may be
   * hanging in here (at strchr()), set up so we exit this
   * routine, no matter what, after 5 seconds... this
   * kludges around a potentially infinite loop.
   */

  {
  char bf[2048];
  char *b;
  int sz;

  int todo;
  int si = 0;  /* size to write; 0 for all */
  int cnt; /* how many bytes actually wrote */
  char *cp;

  while (! feof(diskf))
  {
  bf[2047] = '\0';
  sz = fread (bf, 1, 2047, diskf );
  b  = bf;

  todo=sz;
  done=0;


  if (todo == 0)
    lpwrite ( fd, "\r\nlpcat: todo is 0\r\n", 20);

  while( (todo>0) )
    {
      /* strip out ctrl chars (19990623.2335)
       * Make 'em blanks instead.
       * This code is elsewhere too; grep for the timestamp.
       */
      cp = b;
      {
	int ticker;
	for (ticker = (si == 0 ? sz : si); ticker > 0; ++cp, --ticker)
	{
	  register int cv = (*cp);
	  if ( ((cv < 32) || ( (cv > 128 ) && (cv < (128+32))) || (cv==IAC) )
	       && (cv != '\n') && (cv != '\r') )
	    *cp = ' ';
	}
      }



      cnt=write( fd, b, sz );  /* everything */

      /* 981023.0320: commented out check gainst EINTR */

      if( cnt<0 /* && errno!=EINTR */ && errno!=EAGAIN)
	{
	  done=-1;	 /* error! */
	  break;
	}
		
      if( cnt>0 )
	{
	  todo-=cnt;
	  done+=cnt;
	  b+=cnt;
	}		
    }  // while todo > 0
  }  // while ! eof
  }  // lexical block
  fclose (diskf);
  return done;
}  // slf_send_file ends


#endif  // 20020704.1210 - comment out slf_send_file, not currently used


