#include <pwd.h>     /* for passwd struct */
#include "world.h"
#include <sys/stat.h>	/* for struct stat, stat() */
#include <dirent.h>	/* for dirent stuff :-) (enumerating banners) */
#include "slforum.h"

/*
    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
*/

/* set in main() */
extern const char *wtmpfname;

/* if set in main, base dir for slf-related files
 *	---- files: ----
 *	banners/ dir
 *	virtusersdb.*
 *	slforum-<username> prefs
 *	issue.banner
 *	lockout-users
 *	wtmp, if not /var/log/wtmp
 */
const char *slfhomedir = NULL;


//------------------------------------------------------

SLForumRoom::SLForumRoom( int _port, FILE *_log_file  /* =NULL */ )
{
	port=_port;	
	log_file=_log_file;
	room_startup_time = time(NULL);

	for( int i=0; i<MAX_CHAT_USERS; ++i )
	{
		user[i] = new SLForumUser( this, i );
	}
	server_fd=-1;	
	lastpublicfrom[0] = '\0';
	lastpublicfrom[USERNAMESIZ-1] = '\0';
	lastpublicmsg [0] = '\0';
	lastpublicmsg [USERBUFSIZ-1]  = '\0';
	who_sort_is_valid = false;
	auto_accept_guests = (0 != AUTO_ACCEPT_GUESTS);
	print_guest_howto_message = (0 != GUEST_HOWTO_MESSAGE);

	// 19991111.1840: for max limits
	empty_slots = MAX_CHAT_USERS;

	// 19990212.1630: added banners support
	bannersdir = NULL;
	bannerflist = NULL;
	bannerflbuf = NULL;
	nbanners = 0;
	lastbanneridx = -1;
	bannerpause = (time_t) 60;
	lockoutfile_chktime_is_valid = false;

        //MaTTeD - 19990731.2245 - Topic stuff
	topic.tname  [0] = '\0';  topic.tname  [USERBUFSIZ-1] = '\0';
	topic.tsetter[0] = '\0';  topic.tsetter[USERNAMESIZ-1] = '\0';
	topic.ttime = room_startup_time;
	topic.tcomments = NULL;

	for (int i = 0; i < NUM_REMEMBERED_TOPICS; ++i) {
	  prevtopics[i].tname[0] = '\0';
	  prevtopics[i].tcomments = NULL;
	}

	// jer added runtime pref 20020404.1224
#ifdef ALLOW_TOPICS
#if 0 == TOPICS_ON_AT_START
	room_topics_on = false;
#else
	room_topics_on = true;
#endif  // topics on at start?
#else
	room_topics_on = false;
#endif  // defined (ALLOW_TOPICS)?
	lastTopicSetterTime = time(NULL);  // just to give it _something_

	// for virtuser support
	admin_gid = SLF_ADMIN_GID;
	users_gid = (admin_gid+1);  /* anything but admin_gid */
	slf_virtuser_set_admin_gid (admin_gid);

	// for virtuser db
	virtuserdbfname[0] = '\0';
	slf_virtuser_set_dbfname(NULL);

#if USE_VIRTUSERS

	if (slfhomedir != NULL) {
	  if (-1 == snprintf(virtuserdbfname, PATH_MAX-5, "%s%c%s",
			     slfhomedir, PATH_SEP, SLF_VIRTUSER_DB))
	    {
	      fprintf(stderr, "Warning: couldn't set virtuser database file to %s in %s: path too long\n", SLF_VIRTUSER_DB, slfhomedir);
	    }
	  char *endp = &(virtuserdbfname[strlen(virtuserdbfname)]);
	  endp[0] = '.';
	  endp[1] = 'd';
	  endp[2] = 'b';
	  endp[3] = '\0';
	  FILE *f = slf_safe_fopen("virtuser db", virtuserdbfname, "ab+");
	  if (f == NULL) {
		  // failed to open or to create.
	    virtuserdbfname[0] = '\0';
	    slf_virtuser_set_dbfname(NULL);  // indicates don't use feature.
	  }
	  else {
		  // successful open or create, good news.
	    fclose(f);
	    slf_virtuser_set_dbfname(virtuserdbfname);
	    // Now, if we just created it, we must delete it -- ndbm isn't fond
	    // of zero-byte files (dbm_open returns EFTYPE).
	    {
	      struct stat st;
	      if ((0 == stat (virtuserdbfname, &st))
		  && (0 == st.st_size))
	      {
		 unlink (virtuserdbfname);  // del it, dbm_open will recreate
		 // with the format it wants.
	      }
	    }
	  }  // success?
	  endp[0] = '\0';
	}
	if (virtuserdbfname[0] == '\0') {
	  FILE *f = NULL;
	  char *endp = NULL;
	  if (-1 == snprintf(virtuserdbfname, PATH_MAX-5, "%s%c%s",
			     SLF_VIRTUSER_DIR, PATH_SEP, SLF_VIRTUSER_DB))
	    {
	      fprintf(stderr, "Warning: couldn't set virtuser database file to %s in %s: path too long\n", SLF_VIRTUSER_DB, SLF_VIRTUSER_DIR);

	      
	    } else {
	      endp = &(virtuserdbfname[strlen(virtuserdbfname)]);
	      endp[0] = '.';
	      endp[1] = 'd';
	      endp[2] = 'b';
	      endp[3] = '\0';
	      f = slf_safe_fopen("virtuser db", virtuserdbfname, "ab+");
	    }
	  if (f == NULL) {
	    fprintf(stderr, "Virtual users will not be available.\n");
	    slf_virtuser_set_dbfname(NULL);  // indicate don't use feature.
	    virtuserdbfname[0] = '\0';
	  } else {
	    fclose(f);
	    endp[0] = '\0';
	    slf_virtuser_set_dbfname(virtuserdbfname);
	  }
	}
#endif  /* USE_VIRTUSERS */

	// recap feature... 20060528.1140
#if USE_RECAP
	recap_last = -1;
	recap_wrapped = false;
	recap_lasttime = room_startup_time;
#endif	/* USE_RECAP */


	// usercount file feature.. 20060601.0700
	user_count_total = 0;
	user_count_here = 0;
	user_count_idle_threshold = SLF_IDLE_HERE_THRESHOLD;
	user_count_updated = room_startup_time;

#if USE_USERCOUNT_FILE
	user_count_file_updated = room_startup_time - USERCOUNT_UPDATE_SECONDS;
	if (slfhomedir != NULL) {
	  if (-1 == snprintf(user_count_file_fullpath, PATH_MAX-1, "%s%c%s-%d",
			     slfhomedir, PATH_SEP, "usercount", port))
	    {
	      fprintf(stderr, "Warning: couldn't set usercount file to %s-%d in %s: path too long\n", "usercount", port, slfhomedir);
	    }
	  FILE *f = slf_safe_fopen
	    ("usercount file", user_count_file_fullpath, "w+");
	  if (f == NULL) {
		  // failed to open or to create.
	    user_count_file_fullpath[0] = '\0';  // indicates don't use.
	  }
	  else {
		  // successful open or create, good news.
	    fclose(f);
	    
	    UpdateUserCountFile(room_startup_time);

	  }  // success?
	}

#endif  /* USE_USERCOUNT_FILE */


}  // end SLForumRoom constructor




/** jer separated this out from destructor to clean
 *  a mess up... system wanted to free what I'd already
 *  freed.  -- jer 19990522.0250
 */
void SLForumRoom::room_cleanup()
{
	// jer added this part 980809.0900
	for (int i=0; i<MAX_CHAT_USERS; ++i )
	{
	  if (user[i] == NULL)
	    continue;

	  if (user[i]->IsUserAccepted())
	    user[i]->writeLogoutEntry(wtmpfname);

	  delete user[i];
	  user[i] = NULL;
	}


	if (bannerflist != NULL) {
	  free (bannerflist);
	  bannerflist = NULL;
	}
	if (bannerflbuf != NULL){
	  free (bannerflbuf);
	  bannerflbuf = NULL;
	}

	// this was always here...
	if( server_fd!=-1 )
	{
		close( server_fd );	
	}
}


SLForumRoom::~SLForumRoom()
{
  room_cleanup();
}


/* for use by SIGHUP reloader; don't abuse this. */
void SLForumRoom::SetLogFile(FILE *newlog)
{
  log_file = newlog;
}


void	 SLForumRoom::BindSocket()
{
	int er, len;
	server_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (server_fd == -1) {
	  const char *es = strerror(errno);
	  fprintf( stderr, "Could not make socket on port %d: %s\r\n", port, es );
	  exit(1);
	}
  	len = sizeof(server_address);

  	memset ((char*) &server_address, '\0', len);
  	server_address.sin_family = AF_INET;
  	server_address.sin_addr.s_addr = htonl(INADDR_ANY);
  	server_address.sin_port = htons(port);
	
	{
	  // This little bit of code lets us exit cleanly without
	  // tying up the port we bound to.  Without this, trying to
	  // re-bind to the port could result in "port in use" errors
	  // for a few minutes.
	  // thanks apache sources (main/http_main.c)

	  // note that setsockopt normally wants a const void *;
	  // but solaris wants a const char *, and linux doesn't
	  // complain, so that's that.  -- jer

	  int one = 1;
	  if ((er = setsockopt
	       (server_fd, SOL_SOCKET, SO_REUSEADDR,
		(const char *) &one, sizeof(int))) == -1) {
	    perror ("couldn't set SO_REUSEADDR, but that's not the end of the world");
	    errno = 0;
	  }

	  // set nodelay (disable nagle's algorithm)
	  one = 1;
	  int tcp_proto_num = -1;
	  {
	    struct protoent *e;
	    e = getprotobyname("tcp");
	    if (e != NULL)
	      tcp_proto_num = e->p_proto;
	    else
	      tcp_proto_num = 6;
	  }
	  if ((er = setsockopt
	       (server_fd, tcp_proto_num, TCP_NODELAY,
		(const char *) &one, sizeof(int))) == -1) {
	    perror ("couldn't set TCP_NODELAY, but that's not the end of the world");
	    errno = 0;
	  }
	}

	if ((er = bind(server_fd, (struct sockaddr *)&server_address, len)) != 0)
	{
	  const char *es = strerror(errno);
	  fprintf( stderr, "Error binding to port %d: %s\r\n", port, es );
	  exit(1);
	}

	listen(server_fd, MAX_CHAT_USERS);
}

	
/** FD_SET is for synchronous I/O multiplexing
 *  using the select(2) system call.
 *  FD_SET adds fd to a set.
 *  select() watches a set for avail for input, output and exceptions.
 */
int	 SLForumRoom::Make_fdset( fd_set *r, fd_set *w, fd_set *e )
{
	int num=1;

	FD_SET( server_fd, r );
	FD_SET( server_fd, e );
	
	for( int i=0; i<MAX_CHAT_USERS; ++i )
	{
		int fd=user[i]->GetFD();
		
		if( fd!=-1 )
		{
			FD_SET( fd, r  );
			++num;
		}
	}
	
	return num;
}


int SLForumRoom::ManageRoom( fd_set *r, fd_set *w, fd_set *e )
{
	if( FD_ISSET( server_fd, r ) )
	{
		FindSlotForNewUser();
	}
	
	for( int i=0; i<MAX_CHAT_USERS; ++i )
	{
		int fd=user[i]->GetFD();
		
		if( fd!=-1 )
		{
			bool has_left = false;

			if( FD_ISSET( fd, r ) )
			{
			  // process keypress from this user,
			  // and check if they disconnected.
			  // (if so, we announce it later)
			  //
			  if( ! user[i]->ManageUser() )
			    has_left = true;
			}

			/* 980914.0850: added test for IsUserAccepted()
			 *	to fix a bug in which logging in,
			 *	giving name, discon on pw told the
			 *	room that <arbitrary name> has left.
			 * 981005.0955: added timestamp
			 */
			if(( has_left )  && ( user[i]->WasUserAccepted() ))
			{
			  char buf[48];  const char* leavemsg;
			  // 19990522.1515: distinguish tween
			  //	manual and unplanned logout

			  if (user[i]->WasUserManualDiscon())
			    leavemsg = "%s has left at \201.";
			  else
			    leavemsg = "%s has lost connection at \201.";

				sprintf( buf, leavemsg,
					 user[i]->GetName(),
					 user[i]->loginTimeStr() );
				user[i]->writeLogoutEntry(wtmpfname);
				Announce( SLForumUser::MSG_MOVES, buf );

				UpdateUserCount (NULL, true);
#if USE_USERCOUNT_FILE
				{
				  time_t now = time (NULL);
				  UpdateUserCountFile (now);
				}
#endif
			}
		}

	}	
	return 0;
}


void	SLForumRoom::FindSlotForNewUser()
{
  int i;

  for( i=0; i<MAX_CHAT_USERS; ++i ) {  // look for a slot for the new entry
    if ( user[i]->isUserConnected() == false )
      {
	user[i]->ManageUser(); 	// nobody here, let's use this slot
	return;
      } else if (user[i]->CheckForMissedDiscon())
      {
	user[i]->ManageUser(); 	// someone was here but lost, use this slot
	return;
      }
  }	
  if (i == MAX_CHAT_USERS) {
    // no more room.. print message and close connection.
    // connection-init stuff is from slforumuser::HandleNoConnection.
    struct sockaddr_in user_address;
    socket_arg3_t len = (socket_arg3_t) sizeof(user_address);
    int user_fd = accept
      ( GetServerFD(), (struct sockaddr *)&user_address, &len );
    if (user_fd == -1)
      return;
    lpwrite (user_fd, telnet_init, strlen(telnet_init));
    lpwrite (user_fd, telnet_local_off, strlen(telnet_local_off));
    if (-1 != lpwrite (user_fd, ROOM_FULL_TXT, strlen(ROOM_FULL_TXT)))
      close (user_fd);
  }
}


/** Broadcast to all, with no hint flags. */
void	SLForumRoom::BroadCast( const char *from, const char *c,
				int msgdescflags)
{
  _BroadCast (from, c, msgdescflags, false, 0);
}



/** back end private method for implementing BroadCast variants.
 *  Note that anything with a hintbitset will ignore the "from"
 *  and "msgdescflags" values.
 *
 *  Otherwise, if "from" is NULL, the message is sent out as-is with
 *  no forther formatting, starting at the left edge of the screen.
 *
 *  Otherwise it formats as usual.
 */
void	SLForumRoom::_BroadCast( const char *from, const char *msg,
		    int msgdescflags,
		    bool wants_beep, int hintbitset )
{
  time_t now = time(NULL);

  if (msg == NULL)
    return;

  // 19990615.1800 - reject blank lines
  //	if the user sending the blank line didn't
  //	sent the previous line...
  //	If it's _not_ blank, set who it's from.
  if (from != NULL)
  {
    bool new_fromuser = strncmp (from, lastpublicfrom, USERNAMESIZ-1);

    if (new_fromuser)
      if (*msg != '\0')
	// non-blank line, from a new user.
	// note for next time.
	strncpy (lastpublicfrom, from, USERNAMESIZ-1);
      else
	// blank line from a new user.  Return.
	return;
  }
  // 19990615.1800 mod ends here..

  if  ( (0 == (msgdescflags & SLForumUser::MSG_NOLOG)) 
	&& (0 == hintbitset) )
  {
      if ((log_file != NULL) || log_stdout_too)
      {
	  if (from != NULL)
	      logprintf( "%10s: %s\n", from, msg );
	  else
	      logprintf( "%s\n",msg );
      }
  }

  if ((msg[0] != '\0') && (0 == (msgdescflags & SLForumUser::MSG_SYS))
      && (0 == hintbitset))
    strncpy (lastpublicmsg, msg, USERBUFSIZ-1);    // 20000321.1330

  for( int i=0; i<MAX_CHAT_USERS; ++i )
    {
      if( user[i]->IsUserAccepted() ) {
	if (hintbitset)
	  user[i]->Send (msg, hintbitset, wants_beep, msgdescflags); // sys msg
	else if (wants_beep)
	  user[i]->Send (msgdescflags, msg, wants_beep); // sys msg
	else
	  user[i]->Send (msgdescflags, from, NULL, msg);

	user[i]->lastping = now;
      } else
	user[i]->CheckForMissedDiscon();
    }  // next user


#if USE_RECAP

  if ((0 == (msgdescflags & SLForumUser::MSG_NOLOG))
      && (0 == hintbitset))
  {
    if (0 == (msgdescflags & SLForumUser::MSG_SYS))
      recap_lasttime = now;

    ++recap_last;
    if (recap_last >= RECAP_LINES)
    {
      recap_last = 0;  recap_wrapped = true;
    }
    recap_desc [recap_last] = msgdescflags;
    if (from != NULL)
      snprintf( recap [recap_last], USERBUFSIZ-1, "%10s: %s", from, msg );
    else
      strncpy( recap [recap_last], msg, USERBUFSIZ-1 );
    recap [recap_last][USERBUFSIZ-1] = '\0';
    recap_time [recap_last] = now;

    // For recap_timetype, CHIME needs special treatment, we don't set it here.
    {
      if ((NULL == index(msg, '\201')) && (NULL == index(msg, '\202')))
	recap_timetype [recap_last] = RECAP_TIMETYPE_NONE;
      else
	recap_timetype [recap_last] = RECAP_TIMETYPE_STAMP;
    }
  }

#endif	/* USE_RECAP */


}



/** send a timestamp.  jer added 19991031.0512
 */
void SLForumRoom::AnnounceTimestamp
  (time_t now, int msgdescflags, bool wants_beep,
   const char *reason /* =NULL */ )
{
  char buf[USERBUFSIZ];  buf[USERBUFSIZ-1] = '\0';
  char *bufp;

  if (reason==NULL)
      bufp = (char*) "At \201:";
  else {
      bufp = buf;
      snprintf (buf, USERBUFSIZ-1, "%s: at \201:", reason);
  }

  Announce (msgdescflags, bufp, wants_beep);
}


void	SLForumRoom::Announce( const char *msg )
{
	Announce (msg, 0, false);
}


/** Broadcast from SLFORUM (system message)
 *  to all who don't match hint flags;
 *  set the flags so they don't recieve this
 *  broadcast again.  (Set hintbitset to 0 to
 *  broadcast to all.)
 *  jer 980709.1700
 */
void	SLForumRoom::Announce( const char *c, int hintsetmask, bool wants_beep)
{
  // pretend it's not public, so it can't be blocked;
  // anything with hintbitset or wants_beep is a system message.
  _BroadCast (SYSMSG_FROM_USERNAME, c, 0, wants_beep, hintsetmask );
}


// jer 19990607.1600
/** Announce something from system to room of a certain message type;
 *  the msgdescflags should not contain MSG_PUBLIC, in case that's
 *  being ignored, unless that is desired.  Optional beep.
 */
void	SLForumRoom::Announce (int msgdescflags, const char *c,
			       bool wants_beep  /* =false */ )
{
  _BroadCast (SYSMSG_FROM_USERNAME, c,
	      msgdescflags | SLForumUser::MSG_SYS, wants_beep, 0);
}



// int SLForumRoom::who_unidx[MAX_CHAT_USERS];
// jer 19990522.0100: sorted list for !who.
// declared in slforumroom.h, here as just a reminder.


// jer 19991029.1445 - moved into separate function from ListUsers.
// jer 20000227.0200 - can now sort by alarm time instead of idle time
//	(for ListPubTimers)
void SLForumRoom::sort_unidx (bool by_alarm_time /* =false */ )
{
  if (who_sort_is_valid)
    return;

  // 19990522.0100
  // sort the list.  Set up the unindex array that shows which
  // user number is at which index, and whenever a data element
  // is moved, move the element at same index in the who_unidx array.
  int i, key, posn;
  int idletime[MAX_CHAT_USERS];

  int now_hr=0, now_min=0;  // for by_alarm_time
  time_t now;		// for both alarm and idle
  now = time(NULL);

  if (by_alarm_time) {
    const struct tm *ptime;
    ptime = localtime(&now);
    if (ptime == NULL)
      by_alarm_time = false;  // can't do it, so sort by something else logical
    else {
      now_hr  = ptime->tm_hour;
      now_min = ptime->tm_min;
    }
  }

  for (i=0; i<MAX_CHAT_USERS; ++i) {
    who_unidx [i] = i;
    if (! by_alarm_time)
      idletime[i] = user[i]->idleTime(now);
    else {
      int h, m;
      h = user[i]->timer_expire_hour;
      m = user[i]->timer_expire_min;
      idletime[i] = h * 60 + m;
      if ((h < now_hr) || ((h == now_hr) && (m <= now_min)))
	idletime[i] += 60*24;  // it's tomorrow
    }
  }
  
  // code from http://www.iscs.nus.edu.sg/~p37273/ic101/content/ch6p11.html
  // (result of search on hotbot for 'insertion sort', was in java)
  for (i = 1; i < MAX_CHAT_USERS; ++i)
    {
      key = idletime[i];
      posn = i;
      
      //shift larger values to the right
      while (posn > 0 && idletime[posn - 1] > key)
	{
	  idletime[posn] = idletime[posn - 1];
	  who_unidx [posn] = who_unidx[posn - 1];
	  posn--;
	}
      idletime[posn] = key;
      who_unidx [posn] = i;
    }

  who_sort_is_valid = true;
}



/** TODO accessors, documentation */
void	SLForumRoom::UpdateUserCount (const SLForumUser *u, bool do_recalc)
{
  time_t now = time(NULL);
  int idletime;

  if ((! do_recalc) && ((now - user_count_updated) < USERCOUNT_UPDATE_SECONDS))
    return;    // hasn't been long enough

  if (u != NULL)
    idletime = u->pref.idle_here_threshold;
  else
    idletime = SLF_IDLE_HERE_THRESHOLD;

  int nu = 0, notshown = 0;
  for (int i=0; i<MAX_CHAT_USERS; ++i )
  {
    if ( user[i]->IsUserAccepted() )
    {
      nu++;
      if (((user[i]->idleTime(now)+30)/60) > idletime )
	++notshown;
    }
  }

  user_count_total = nu;
  user_count_here = nu - notshown;
  user_count_idle_threshold = idletime;
  user_count_updated = now;

}



#if USE_USERCOUNT_FILE

void	SLForumRoom::UpdateUserCountFile (time_t now)
{
  if (user_count_file_fullpath[0] == '\0')
  {
    // IO error in room constructor, so not using this feature.
    user_count_file_updated = now;
    return;
  }

  // recalc if needed
  UpdateUserCount
    (NULL, (user_count_idle_threshold != SLF_IDLE_HERE_THRESHOLD));

  // write to the file

  FILE *f = slf_safe_fopen
    ("usercount file", user_count_file_fullpath, "w+");
  if (f == NULL) {
    // failed to open or to create.
    user_count_file_fullpath[0] = '\0';  // indicates don't use.
  }
  else {
    // successful open or create, good news.

    int notshown = user_count_total - user_count_here;
    char buf[128];

    // -- BEGIN SHARE dupl'd code with ::ListUsers, ::UpdateUserCountFile --

    if (notshown == 0)
      sprintf (buf,
	       "%d %s (%d %s empty)",
		     user_count_total, 
		     (user_count_total != 1 ? "people" : "person"),
		     MAX_CHAT_USERS - user_count_total,
		     (user_count_total != (MAX_CHAT_USERS-1)
		      ? "slots" : "slot" ));
	  else
	    sprintf (buf,
		     "%d %s (%d %s empty; %d more %s are too idle)",
		     (user_count_total-notshown),	// 20000430.0400
		     ((user_count_total-notshown) != 1 ? "people" : "person"),
		     MAX_CHAT_USERS - user_count_total,
		     (user_count_total != (MAX_CHAT_USERS-1)
		      ? "slots" : "slot"),
		     notshown,
		     (notshown != 1 ? "people" : "person" ));

    // -- END SHARE dupl'd code with ::ListUsers, ::UpdateUserCountFile --

    fprintf (f, "%s\n", buf);
    fclose(f);

  }  // success?

  // All done.
  user_count_file_updated = now;
}

#endif  /* USE_USERCOUNT_FILE */




/** revised 980702.1100 by jer
 *  to list on multiple lines, include login/idle times and login-from IP.
 *
 *  If you call this, make sure to set room->who_sort_is_valid = false
 *  before and after calling it, so it sorts the list by idle time.
 *  This is a hack: we have commands that call ListUsers, and those that
 *  call ListAways, and those that call both, and they both need to be
 *  sorted, but I don't want it sorted twice for those that call both.
 *  Thus the who_sort_is_valid status variable.
 *
 *  19990522.0100: sorts by idle time.
 *  19991012.0100: dschin: separate into 2 functions (users and aways)
 *  19991029.2040: added exclude_aways option (with user's idle_here_thr.)
 *  19991029.2345: changed type to bool; returns true if any users are away
 */
bool	SLForumRoom::ListUsers
	( SLForumUser *u, bool exclude_idleones /* =false */ )
{
	int    i;
	char   buf[128], idlebuf[20], logintimebuf[20];
	SLForumUser *user_i;
	time_t lintime;
	int    idletime;
	bool   had_away_users = false;
	time_t now = time(NULL);

	sort_unidx();	// sort by idle time

	UpdateUserCount(u, true);  // how many here, how many idle

	{
	  int notshown;
	  if (exclude_idleones)
	    notshown = user_count_total - user_count_here;
	  else
	    notshown = 0;

    // -- BEGIN SHARE dupl'd code with ::ListUsers, ::UpdateUserCountFile --

	  if (notshown == 0)
	    sprintf (buf,
		     "%d %s here: (%d %s empty)",
		     user_count_total, 
		     (user_count_total != 1 ? "people" : "person"),
		     MAX_CHAT_USERS - user_count_total,
		     (user_count_total != (MAX_CHAT_USERS-1)
		      ? "slots" : "slot" ));
	  else
	    sprintf (buf,
		     "%d %s here: (%d %s empty; %d more %s too idle, not shown)",
		     (user_count_total-notshown),	// 20000430.0400
		     ((user_count_total-notshown) != 1 ? "people" : "person"),
		     MAX_CHAT_USERS - user_count_total,
		     (user_count_total != (MAX_CHAT_USERS-1)
		      ? "slots" : "slot"),
		     notshown,
		     (notshown != 1 ? "people" : "person" ));
	}
    // -- END SHARE dupl'd code with ::ListUsers, ::UpdateUserCountFile --
	u->Send ( buf, 0, false );

	for( i=0; i<MAX_CHAT_USERS; ++i )
	{
	  user_i = user[who_unidx[i]];
	  if( user_i->IsUserAround() )
	    {
	      lintime = user_i->loginTime();
	      if (user_i->isUserAway()) had_away_users = true;
	      user_i->writeIdleBuf (idlebuf, now, &idletime);

	      if (exclude_idleones &&
		  (idletime > u->pref.idle_here_threshold))
		  continue;    // next user!

	      if (u->pref.jetlag==0)
		  sprintf
		      (buf,
		       "%-9s%c%-12s (login %12s from %s)",
		       user_i->GetName(),
		       (user_i->isUserGuest() ? 'G' : ' '),
		       idlebuf,
		       user_i->loginTimeStr(),
		       user_i->loginFrom16() );
	      else
	      {
		  u->writeTime (logintimebuf, sizeof(logintimebuf),
				user_i->loginTime(), 1);
		  sprintf
		      (buf,
		       "%-9s%c%-12s (login %12s from %s)",
		       user_i->GetName(),
		       (user_i->isUserGuest() ? 'G' : ' '),
		       idlebuf,
		       logintimebuf,
		       user_i->loginFrom16() );
	      }
	      u->Send
		( SLForumUser::MSG_SYS,
		 ( user_i->IsUserAccepted() ? "USERS" : "(g)WAITING" ),
		  NULL, buf );
	    }
	}

	if (had_away_users) {
	  u->Send
	    ("Users are away. Type \177users or \177showaway to see the away messages.", u->HINT_SHOWAWAY, false);
	}
	return had_away_users;
}  // ListUsers


/** Just list the away messages.  19991012.0100 dschin
 *  19991029.2345: changed type to bool; returns true if away users. -jer
 *  19991108.2245: added with_header, which prints a header line above
 *	the list if true; this is because we sometimes call ListUsers
 *	and then ListAways, and sometimes we just call ListAways.
 *	We want a header displayed if we do that, and just the list of
 *	away users if we don't. (Since ListUsers already prints a header.)
 */
bool SLForumRoom::ListAways
	(SLForumUser *u, bool with_header /* =false */ )
{

  bool displayed_any = false;
  bool had_some_aidlehidden = false;
  bool suppress_idle_awaymsg = ! (u->pref.showaidle);

	sort_unidx();

	for ( int i=0; i<MAX_CHAT_USERS; i++ ) {
          SLForumUser* user_i = user[who_unidx[i]];
	  if ( user_i->isUserAway())
	  {
	    // decide whether to show:
	    if (suppress_idle_awaymsg && user_i->isAwayFromIdle())
	    {
	      had_some_aidlehidden = true;
	      continue;    // skip this one, they're autoawayed for idleness
	    }

	    // show this user:
	    if (with_header && ! displayed_any)
	      u->Send ("The following people are away:", 0, false);

	    u->Send
	      ( SLForumUser::MSG_SYS, AWAY_FROM_USERNAME, NULL,
		user_i->getAwayMessage() );
	    displayed_any = true;
	  }
	}

	if (! displayed_any)
	{
	  if (had_some_aidlehidden)
	    u->Send ("Some users are away due to idle (not shown).", 0, false);
	  else
	    u->Send ("No users are away.", 0, false);
	}

	return displayed_any;

}  // ListAways


bool SLForumRoom::ListPubTimers(SLForumUser *to)	// jer 20000227.0200
{
  bool displayed_any = false;
  const SLForumUser *u;
  int jetlag, jetlag_hr;

  sort_unidx(true);    // sort by alarm times
  jetlag = to->pref.jetlag;  if (jetlag < 0) jetlag += 24;

  for (int i=0; i<MAX_CHAT_USERS; ++i )
    {
      u = user[who_unidx[i]];
      if ( u->IsUserAccepted() && u->IsTimerSet && u->IsTimerPublic)
	{
	  char buf[128];
	  if (! displayed_any)
	    displayed_any = true;
	  jetlag_hr = (u->timer_expire_hour + jetlag) % 24;

	  sprintf (buf, "Public timer from %s, for %d:%02d%c", u->GetName(),
		   jetlag_hr, u->timer_expire_min,
		   ((u->timer_message[0] != '\0') ? ':' : '.'));
	  to->Send (buf, 0, false);
	  if (u->timer_message[0] != '\0')
	    to->Send (u->timer_message, 0, false);
	}
    }
  
  return displayed_any;
}


/* Get a connected user, by username; if no one by that name
 * is connected, return NULL.  If more than one user by that name
 * is connected, they should be the same person!  Which of the user
 * objects is returned will be undefined; If you need to check for
 * multiple occurences, use GetUserIncremental instead.
 * The search is case-insensitive.
 * -- jer added 980701.1530
 * jer 980805.2150: modded to use GetUserIncremental.
 */
SLForumUser* SLForumRoom::GetUserByName (const char *nam) const
{
  int i = 0;
  return GetUserIncremental (nam, i, false);
}


/* For looking for more than one user logged in under the
 * same (case-insensitive) username.
 * The search will begin at index i; if no user by that name
 * is found, NULL is returned.  For a successful match, i
 * equals the 'index' _after_ that at which the user was found; thus,
 * to search again, the next search will start at the correct
 * place.  The variable i must be writeable.  It should start at 0.
 *
 * If exceptfor isn't NULL, that user (or another login w/ same name)
 * won't be found by the search.  This is good for not making yourself
 * the target of a command. (e.g., !to)
 *
 * -- jer added 980805.2150
 * -- and modded for partial matching 981207.0015
 * -- and added exceptfor on 19991021.1800
 * -- and included STATE_FIND_DUPL as "valid" on 20010202.1045
 *	by changing IsUserAround().
 */
SLForumUser* SLForumRoom::GetUserIncremental
	(const char *nam, int &i, bool allowPartial,
	 const SLForumUser *exceptfor /* =NULL */ )
const
{
  if (nam == NULL)
    return NULL;
  SLForumUser *u;
  const char *excname=NULL;  // except-for user's name, if any
  size_t partln = 0;
  if (allowPartial)
    partln = strlen (nam);
  if (exceptfor != NULL)
    excname = exceptfor->GetName();

  for ( ; i<MAX_CHAT_USERS; ++i ) {
    u = user[i];
    if ( u->IsUserAround()
	 && ( allowPartial
	      ? ( strncasecmp (nam, u->GetName(), partln) == 0 )
	      : ( strcasecmp (nam, u->GetName()) == 0 ))
	 && ((exceptfor == NULL) || strcasecmp(excname, u->GetName())) ) {
      ++i;
      return u;	  // ++i to set to next index after this user,
		  // in case of incremental search.
    }
  }
  return NULL;
}


SLForumUser* SLForumRoom::GetUserByIndex
	(int idx, bool evenIfNotAccepted /* =false */ )
  const
{
  if (( idx < 0 ) || ( idx >= MAX_CHAT_USERS ))
    return NULL;
  SLForumUser* u = user[idx];
  if ( evenIfNotAccepted ||
       ((u != NULL) && (u->IsUserAround())))
    return u;
  return NULL;
}


int SLForumRoom::IndexOf (SLForumUser *u) const
{
  if (u == NULL)	return -1;
  for ( int i = 0; i < MAX_CHAT_USERS; ++i )
    if ( u == user[i] )
      return i;

  return -1;
}


static char _logFmtBuf[255];

/* j added 980703.1300
 */
void SLForumRoom::logprintf(const char *fmt, ...) const
{
  // from linux vsprintf manpage example //
    if ((log_file == NULL) && ! log_stdout_too)
    return;

    // Run through fmt as printf would

  char buf[256];
  va_list ap;
  va_start (ap, fmt);
  vsnprintf(buf, 255, fmt, ap);  buf[255] = '\0';
  va_end (ap);

  // Now  check for and expand \201 and \202
    // (code from user::Send)
  /* should we expand a timestamp?  This code is shared with 
   * user::Send and user::expandTimeTokens; if you change it here,
   * change it there.
   */
  char *outbuf = buf;
    {
	bool wants_copy = false;
	char *idx;
	idx = index(outbuf, '\201');
	if (idx==NULL)
	{
	    idx = index(outbuf, '\202');
	    if (idx != NULL) wants_copy = true;
	} else
	    wants_copy = true;

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

	    // copy from 'outbuf' to '_logFmtBuf' and then re-point
	    // 'outbuf' AT '_logFmtBuf'.  \201 = HH:MM, \202 = DD MMM HH:MM.
	    const char *srcp  = outbuf;
	    char *destp = _logFmtBuf;
	    size_t destleft = sizeof(_logFmtBuf)-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

	    // done.  we'll actually print out _logFmtBuf, so point "outbuf"
	    // at it.
	    outbuf = _logFmtBuf;
	}
    }  /* timestamp expansion code ends */


  if (log_stdout_too)
  {
      puts (outbuf);  fflush (stdout);
  }
  if (log_file != NULL)
  {
      fputs (outbuf, log_file);  fflush (log_file);
  }
}




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

int SLForumRoom::GetServerFD() const
{
	return server_fd;	
}

int SLForumRoom::getServerPort() const { return port; }

const struct sockaddr_in *SLForumRoom::GetServerAddress() const
{
	return &server_address;
}

bool SLForumRoom::getAutoAccept() const
{
    return auto_accept_guests;
}

void SLForumRoom::setAutoAccept (bool should_auto_accept_guests)
{
    auto_accept_guests = should_auto_accept_guests;
}

bool SLForumRoom::getPrintGuestHowtoMsg() const
{
    return print_guest_howto_message;
}

void SLForumRoom::setPrintGuestHowtoMsg (bool should_print_guest_howto_msg)
{
    print_guest_howto_message = should_print_guest_howto_msg;
}

void SLForumRoom::setLogStdoutToo(bool doit)
{
    log_stdout_too = doit;
}

const char* SLForumRoom::getLastPublicMsg() const { return lastpublicmsg; }


/** no setter: setting adminGid sets both */
gid_t SLForumRoom::getUsersGid() const { return users_gid; }

gid_t SLForumRoom::getAdminGid() const { return admin_gid; }

void SLForumRoom::setAdminGid(gid_t newgid)
{
    if (newgid == admin_gid)
	return;

    admin_gid = newgid;
    if (users_gid == admin_gid)
	users_gid = admin_gid + 1;

    slf_virtuser_set_admin_gid (admin_gid);
}



////////////////////////////////////////
// banners
//
////////////////////////////////////////
// j added banner funcs on 19990212.1630

const char * SLForumRoom::getBannersDir() const { return bannersdir; }


/** rets 0 on success, or -1 (and sets errno) on fail.
 *  There should be NO trailing slash on *d.
 */
int SLForumRoom::setBannersDir (const char *d)
{
  if (d == bannersdir)
    return 0;
  if (d != NULL) {
    struct stat ds;
    if (-1 == stat (d, &ds)) {
      fprintf (stderr, "Couldn't set banners dir to '%s':\n", d);
      perror ("setting banners dir");
      return -1;
    }
    if (! S_ISDIR(ds.st_mode)) {
      fprintf (stderr, "'%s' isn't a directory.\n", d);
      return -1;
    }
    bdir_mtime = ds.st_mtime - 1; // force update on refreshBannersList()
  }
  lastbanner = time(NULL) - bannerpause;
  lastRndShuffle = lastbanner - 8000;
  if (bannerflist != NULL) {
    free (bannerflist);  bannerflist = NULL;
  }
  if (bannerflbuf != NULL) {
    free (bannerflbuf);  bannerflbuf = NULL;
  }
  nbanners = 0;
  bannersdir = d;
  if (d != NULL)
    return refreshBannersList();
  else
    return 0;
}


/** rebuild the list of available banner files.
 *  Return 0 on success, or -1 (and sets errno) on fail.
 */
int SLForumRoom::refreshBannersList()
{
  if (bannersdir == NULL)
    return 0;

  {
    struct stat ds;
    if (-1 == stat (bannersdir, &ds))
      return -1;

    if (ds.st_mtime == bdir_mtime)
      return 0;  // no need to change

    bdir_mtime = ds.st_mtime;  // update mtime
  }

  // free old copy
  if (bannerflist != NULL) {
    free (bannerflist);  bannerflist = NULL;
  }
  if (bannerflbuf != NULL) {
    free (bannerflbuf);  bannerflbuf = NULL;
  }

  // check # entries and allocate space
  nbanners = 0;
  DIR *d;
  struct dirent *de;
  size_t bdirnam_L = strlen(bannersdir);
  {
    size_t nchars = 0;
    d = opendir (bannersdir);
    if (d == NULL)
      return -1;
    while ( (de = readdir (d)) != NULL ) {
      if (de->d_name[0] == '.')
	continue;
      ++nbanners;
      nchars += strlen (de->d_name) + bdirnam_L + 2;
      // one char for path separator, one for terminating zero
    }
    if (nbanners == 0) {
      closedir(d);
      nbanners = 0;
      return 0;
    }
    bannerflbuf = (char *) malloc (nchars * sizeof(char));
    bannerflist = (const char **) malloc (nbanners * sizeof(const char *));
    if ((bannerflbuf == NULL) || (bannerflist == NULL)) {
      if (bannerflbuf != NULL)  free (bannerflbuf);
      if (bannerflist != NULL)  free (bannerflist);
      closedir(d);
      nbanners = 0;
      errno = ENOMEM;
      bannerflbuf = NULL;
      bannerflist = NULL;
      return -1;
    }
  }

  // read it in
  rewinddir (d);
  {
    char *bname;
    int i;
    size_t L;
    bname = bannerflbuf;
    for ( i = 0; (de = readdir (d)) != NULL; /* ++i */ ) {
      if (de->d_name[0] == '.')
	continue;
      L = strlen (de->d_name);
      ++L;  // include trailing null char
      bannerflist[i] = bname;
      memcpy (bname, bannersdir, bdirnam_L);  bname += bdirnam_L;
      *bname = PATH_SEP;	++bname;
      memcpy (bname, de->d_name, L);
      bname += L;
      ++i;
    }
    closedir(d);
  }
  // yay, done.
  return 0;
}


/** returns number of seconds til can !banner again */
time_t SLForumRoom::getBannersRemainingPause() const
{
  time_t now = time(NULL);
  now -= lastbanner;
  if (now >= bannerpause)
    return (time_t) 0;
  else
    return (bannerpause - now);
}


/** set the required minimum time between two !banner commands;
 *  can't fail, but will ignore negative values. */
void SLForumRoom::setBannersMinpause(time_t pausetime)
{
  if (pausetime < 0)
    return;
  bannerpause = pausetime;
}


/** does a banner, or prints a message that can't yet,
 *  and returns 0 on success, n-secs-remaining on not-time-yet,
 *  or -1 (and sets errno) on fail from sys error.
 *  If banners aren't enabled, slforum will Send a message to
 *  the user about that.
 *  If 'from' is NULL, will return 0 with no action taken.
 */
int SLForumRoom::doBanner (SLForumUser *from)
{
  char *bbuf;

  if (from == NULL)
    return 0;
  if ((nbanners == 0) || (bannersdir == NULL)) {
    from->Send ("No banners are available.", 0, false);
    return 0;
  }

  {
    time_t rema;
    if ((rema = getBannersRemainingPause()) > 0)
      return (int) rema;
  }

  // be sure dir is updated
  if (-1 == refreshBannersList())
    return -1;
  if (nbanners == 0) {
    from->Send ("No banners are available.", 0, false);
    return 0;
  }

  // select one
  time_t now = time(NULL);
  const char *theone;
  {
    int banidx;
    if (now - lastRndShuffle > 800) {
      srand ( (unsigned int) now );
      lastRndShuffle = now;
    }
    if (nbanners == 1)
      banidx = 0;
    else
      while (lastbanneridx == (banidx = (rand() % nbanners)))
	;
    theone = bannerflist[ banidx ];
    lastbanner = now;
    lastbanneridx = banidx;
  }

  // load it
  char *bufend;  // the last valid char in the buffer, which is \0 anyway
  {
    FILE *ban;

    bbuf = (char *) malloc (MAX_BANNER_SIZE * sizeof(char));
    if (bbuf == NULL) {
      errno = ENOMEM;
      return -1;
    }

    off_t bfsize;
    {
      struct stat fst;
      if (-1 == stat (theone, &fst)) {
	sprintf (bbuf,"Couldn't stat banner file %s: %s",
		 theone, strerror(errno) );
	Announce (bbuf);
	free (bbuf);
	return -1;
      }
      bfsize = fst.st_size;
    }

    ban = slf_safe_fopen ("banner file", theone, "r");
    if (ban == NULL) {
      sprintf (bbuf,"Couldn't open banner file %s: %s",
	       theone, strerror(errno) );
      Announce (bbuf);
      free (bbuf);
      return -1;
    }

    logprintf("Banner from %s.  Filename: %s\n", from->GetName(), theone);

    if (bfsize > (MAX_BANNER_SIZE - 1))
      bfsize = MAX_BANNER_SIZE - 1;
    size_t res = fread (bbuf, 1, (size_t) bfsize, ban);
    if (((size_t) bfsize) > res)
    {
      sprintf (bbuf,
	       "Error reading from banner file %s (read %ld not %ld): %s",
	       theone, (long) res, (long) bfsize, strerror(errno));
      Announce (bbuf);
      free (bbuf);
      fclose (ban);
      return -1;
    }
    bbuf [bfsize] = '\0';
    fclose(ban);
    bufend = bbuf + bfsize;
  }

  // print it, one line at a time
  int banlines = MAX_BANNER_LINES;  // remaining # lines allowed :)
  char *c, *eoln;
  const char *nam = from->GetName();
  bool fromguest = from->isUserGuest();
  char eolchar;
  c = bbuf;

  while ((c < bufend) && (*c != '\0'))
  {
    eoln = index (c, '\r');
    if (eoln == NULL)
      eoln = index (c, '\n');
    if (eoln == NULL)
      eoln = (char *) memchr (c, 0, MAX_BANNER_SIZE);
    if (eoln == NULL)
      break;

    eolchar = *eoln;
    if (eoln - c > 65)
      *(c + 65) = '\0';  // truncate line to 65 chars
    else
      *eoln = '\0';  // so we don't print past this line yet


    // strip out ctrl chars (19990623.2335); make 'em blanks instead.
    // grep for datestamp to find the other place we have this same code.
    {
      char *cp;
      register int cv;
      for (cp = c; *cp != '\0'; ++cp) {
	cv = (*cp);
	if ( ((cv < 32) || ( (cv > 128 ) && (cv < (128+32))) || (cv == IAC))
	     && (cv != '\n') && (cv != '\r') )
	  *cp = ' ';
      }
    }


    // send it out

    BroadCast (nam, c, (from->MSG_PUBLIC | from->MSG_BANNER | from->MSG_NOLOG
			| ( fromguest ? from->MSG_GUEST : 0) ));
    // move to next line
    c = eoln;  ++eoln;
    if (eoln < bufend) {
      if (((eolchar == '\r') && (*eoln == '\n'))
	  || ((eolchar == '\n') && (*eoln == '\r')))
	c = eoln;
    }
    ++c;
    --banlines;
    if (banlines == 0)
      *c = '\0';    // all done
  }

  // 

  // yay, we're done with the file
  free (bbuf);

  // Tell those ingoring spam that (20010322.1430)
  {
      char buf[70];
      if (-1 == snprintf
	  (buf, 70, "Spam from %s: %s", from->GetName(), theone))
	  buf[69] = '\0';

      for( int i=0; i<MAX_CHAT_USERS; ++i )
	  if( user[i]->IsUserAccepted() )
	      if ((user[i]->getIgnoringMask()) & SLForumUser::MSG_BANNER)
	      {
		  user[i]->Send
		      (SYSMSG_FROM_USERNAME, SLForumUser::MSG_BANNERED,
		       buf, false);
		  user[i]->Send
		      ("Type \177ignore spammed to suppress this message from now on.",
		       SLForumUser::HINT_IGN_BANNERED, false);
		  user[i]->lastping = now;
	      }
  }

  return 0;
}



////////////////////////////////////////
// topics
//
////////////////////////////////////////
//MaTTeD adds 19990731.2245  setTopic, topicTimeCheck, listTopic, resetComments

/** NOTE: This method does NOT enforce the min-time-until-change constraint,
 *  anyone calling this method can change topic at any time.  The !topic cmd
 *  calls topicTimeCheck(u) to check this constraint.
 *
 * 20020802.1830: If current topic setter is same as prev, do not reset the
 *  countdown; this ensures 1 user can only 'own' topic for 2 hours, can't
 *  monopolize it.
 */
void SLForumRoom::setTopic (const char *topicstr, SLForumUser *setBy)
{
#ifndef ALLOW_TOPICS
  return;
#else

  bool topicSetterChanged = true;  // diff than prev topic's setter?
                                   // Must be true if no previous topic.

  if (! room_topics_on) return;  // not currently on

  if ((topic.tname[0] != '\0') || (topic.tcomments != NULL))
  {
    // a topic already exists; remember it.

    // First check whether we have a new topic setter.
    topicSetterChanged = (0 != strcmp (topic.tsetter, setBy->GetName()));

    // Now, go through the previous topics list and make them all
    // more previous.
#if (NUM_REMEMBERED_TOPICS > 0)
    llist_free (prevtopics[NUM_REMEMBERED_TOPICS-1].tcomments);

    for (int i = NUM_REMEMBERED_TOPICS - 1; i >= 1; --i)
      prevtopics[i] = prevtopics[i - 1];
  }
  prevtopics[0] = topic;
  prevtopics[0].tcomments
     = resetComments(); // all comments blanked w/ new topic;
			// returns the old comments as a linked list.
  if (topic.tname[0] == '\0') {
    // "no topic set yet" -- comments but no topic name,
    // if someone comments right after the forum starts up.
    // Set these fields so it looks normal.  ("Dummy topic")
    //
    strncpy (prevtopics[0].tname, TOPIC_NOT_YET_FMT, USERBUFSIZ-1);
    strncpy (prevtopics[0].tsetter, "nobody", USERNAMESIZ-1);
    prevtopics[0].ttime = time(NULL);
  }

#else
  // not remembering prev topics,
  // so free the comment list now.
  llist_free (resetComments());  // yes, it's ok if resetComments returns NULL.
#endif

  strncpy(topic.tname, topicstr, USERBUFSIZ-1);
  strcpy (topic.tsetter, setBy->GetName());
  topic.ttime = time(NULL);
  topic.tcomments = NULL;
  if (topicSetterChanged) {  lastTopicSetterTime = time(NULL);  }
  listTopic (&(topic), NULL, false);
#endif
}


const char *SLForumRoom::getTopic() const { return topic.tname; }
const char *SLForumRoom::getTopicSetter() const { return topic.tsetter; }



bool SLForumRoom::topicsAreOn() const
{
#ifdef ALLOW_TOPICS
  return room_topics_on;
#else
  return false;
#endif
}

void SLForumRoom::setTopicsOnOff (bool turnOn) { room_topics_on = turnOn; }



/** This either allows the user to reset the topic, by returning 0, or tells
 *  them how much time (seconds) remains until they can set it.
 */
int SLForumRoom::topicTimeCheck(const SLForumUser *caller /* =NULL */ ) const
{
  time_t now;

  if (topic.tsetter[0] == '\0')
    return 0;

  if ((caller != NULL) && 
      (0 == strcmp (topic.tsetter, caller->GetName())))
	  return 0;    // they set it in the first place, they can change it

  now = time(NULL);

  if ((now - (time_t) TOPIC_RESET_INTERVAL) >= lastTopicSetterTime)
    return 0;
  else
    return (int) (lastTopicSetterTime + ((time_t)TOPIC_RESET_INTERVAL) - now);
}


/** Lists the topic, and optionally all the comments to that topic;
 *  't' is the topic, NULL for current room topic;
 *  'u' is the user to write this info to; u==NULL to broadcast.
 *  'prevtopnum' is the optional previous topic #, for the !prevtopic listing.
 */
void SLForumRoom::listTopic
    (TopicInfo *t, SLForumUser *u, bool showCommentsToo,
     int prevtopnum /* =0 */ )
{
#ifdef ALLOW_TOPICS  /* entire body of method */

 bool at_least_one_comment = false; //so user can see if no one

 if (t == NULL)
     t = &(topic);   // default topic = current topic


// print the topic info:
 {
    char s[80];  s[79] = '\0';
    char timebuf[20];
    if (u != NULL)
	u->writeTime (timebuf, sizeof(timebuf), t->ttime, 1);
    else    // the \201 is for the timebuf+7 below
	memcpy (timebuf, "\202\0\0\0\0\0\0\201\0", 9);

    time_t tleft;
    int min, sec;
    if (t == &(topic)) {
      // current topic
      if (topic.tname[0] == '\0') {
	// "No topic is set yet."
	snprintf (s, 79, TOPIC_NOT_YET_FMT);
      } else {
	tleft = topicTimeCheck(u);
	if (tleft > 0) {
	  min = (tleft / 60);
	  sec = (tleft % 60);
	  // "topic set at %s by %s.  Can be reset in %d min %d sec."
	  snprintf (s, 79, TOPIC_SET_MSG_FMT,
		    timebuf+7, t->tsetter, min, sec); // just hh:mm time
	} else {
	  // "topic set at %s by %s.  Can be reset any time."
	  snprintf (s, 79, TOPIC_MSG_NOW_FMT,
		    timebuf, t->tsetter);	// mmm dd hh:mm for time
	}
      }
    } else {
      // previous topic, not current topic
      int i = llist_length(t->tcomments);
      // "Old topic #%d set at %s by %s.  (%d %s)" (n comment/comments)
      snprintf (s, 79, TOPIC_PREV_MSG_FMT,
		prevtopnum,
		timebuf, t->tsetter,
		i, (i != 1 ? "comments" : "comment"));
    }
    if (u == NULL) {
      Announce (SLForumUser::MSG_SYS, s);
      // now print out the topic name
      if (t->tname[0] != '\0')
	BroadCast(TOPIC_FROM_USERNAME, t->tname, SLForumUser::MSG_SYS);   
    } else {
      u->Send ( SLForumUser::MSG_SYS, SYSMSG_FROM_USERNAME, NULL, s );
      if (t->tname[0] != '\0')
	u->Send ( SLForumUser::MSG_SYS, TOPIC_FROM_USERNAME, NULL, t->tname );
    }
 }

   if (!showCommentsToo)
     return;

   // Linked list holds comments.
   LinkedListLink *LL;
   for ( LL = llist_head (t->tcomments);  // will be NULL if tcomments==NULL
	 LL != NULL;
	 LL = llist_next (t->tcomments, LL) ) {

     if (u != NULL)
       u->Send ( SLForumUser::MSG_SYS, COMMENT_FROM_USERNAME, NULL,
		 ((TopicComment *) llist_ldata (LL))->comment );
     else
       BroadCast (COMMENT_FROM_USERNAME,
		  ((TopicComment *) llist_ldata (LL))->comment,
		  SLForumUser::MSG_SYS);

     at_least_one_comment = true;
   }

   //   if (!at_least_one_comment)
   //   {
   //     u->Send ( SLForumUser::MSG_SYS, COMMENT_FROM_USERNAME,
   //		    NULL, NO_COMMENT_TXT );
   //   }
#endif
}   


/** When the topic is changed, all the comments are cleared out here,
 *  and the old comments are returned in a linked list of strings, for
 *  use by the list-previous-topic command, or NULL if no one commented.
 *  Please remember to eventually llist_free this linked list.
 */
LinkedList *SLForumRoom::resetComments()
{
   LinkedList   *clist = topic.tcomments;
   topic.tcomments = NULL;
   return clist;
}   
 

/** Add/change the comment on a topic from a certain user;
 *  returns the formatted comment string, suitable for
 *  announcing to the room.  Returns "" if topics off.
 */
const char* SLForumRoom::topicAddComment
    (const char *comment, SLForumUser *setby)
{
#ifndef ALLOW_TOPICS
  return "";
#else
  if (! room_topics_on) return "";

  TopicComment *newc  = NULL;
  const char   *uname = setby->GetName();

  // check current topic list for a comment by this user;
  // we'll overwrite it if it's there
  for (LinkedListLink *LL = llist_head(topic.tcomments);
       LL != NULL;
       LL = llist_next(topic.tcomments, LL)) {

    if ( 0 == strcmp (uname, ((TopicComment *) llist_ldata(LL))->setby)) {
      newc = (TopicComment *) llist_ldata (LL);
      break;
    }
  }

  if (newc == NULL) {
    newc = (TopicComment *) malloc (sizeof(TopicComment));
    if (newc == NULL) {
      const char *warn = "WARNING: Memory Low: couldn't malloc for comment";
      Announce (warn);
      return warn;
    }
    strcpy (newc->setby, uname);

    if (topic.tcomments == NULL)
    {
      topic.tcomments = llist_new(free);
      if (topic.tname[0] == '\0')
      {
	// No topic set yet, but they're commenting.
	// So set up a dummy "topic".
	strcpy (topic.tname, "(No topic set yet)");
      }
    }

    llist_add_end (topic.tcomments, newc);
  }

  {
    char *buf = newc->comment;

    // set up the comment and we're done.
    if (-1 == snprintf
	(buf, TOPICCOMMENTSIZ, "%9s: %s", uname, comment ))
      buf[TOPICCOMMENTSIZ - 1] = '\0';

    return buf;
  }
#endif
} 





////////////////////////////////////////
// timers
//
////////////////////////////////////////

//MaTTeD add 20000126.1730 : Periodic timer search routine (tick...tick...tick)
// jer 20000201.1255: merged w/ LookForTimeouts
// jer 20000523.1600: Now runs every 30 seconds at :00 and :30 .
// jer 20060601.0700: Can write file with user counts (USE_USERCOUNT_FILE)
//
void SLForumRoom::LookForTimers()
{

const struct tm	*ptime;
time_t		now = time(NULL);

 ptime = localtime(&now);  // get hours, mins, etc
 if (ptime == NULL) {
   Announce ("Assert failed in LookForTimers: room:L1260: ptime is NULL");
   return;
 }


 CheckHourChime();


 /* LookForTimeouts():
  * Search for timed-out users; this is a signal handler that
  * is called (TIMEOUT+3) secs after a login starts.
  * If you want to manually check, call this.
  *
  * This code also checks to see that idle
  * users are still connected.
  *
  * jer 980703.2345, 19990702.1900
  */
 for( int i=MAX_CHAT_USERS-1; i>=0; --i )
  {
    SLForumUser* curu = user[i];
    if (! curu->IsUserAround()) {
      continue; // move along, nothing to see here.
    }

    curu->pingAndCheckTimeout();	// this was LookForTimeouts :-)

    if (! curu->IsUserAccepted())
      continue;		// not in the room (STATE_GET_LINE)


    // LookForTimers:

    if (curu->IsTimerSet
	&& (curu->timer_expire_hour == ptime->tm_hour)
	&& (curu->timer_expire_min  == ptime->tm_min ))
    {
	char s[128];   s[127] = '\0';

	curu->IsTimerSet = false;
	snprintf( s, 127, "It is now \201!  %s",
		  /* curu->timer_expire_hour, curu->timer_expire_min, */
		  curu->timer_message);

	if (curu->IsTimerPublic) {
	  char reason[USERNAMESIZ+24];
	  sprintf (reason, "Public timer from %s", curu->GetName());
	  AnnounceTimestamp (now, 0, false, reason);
	  Announce ( s, 0, true );
	} else {
	  curu->Send( TIMER_FROM_USERNAME, 0, s, true );
	}
    }  // end if
  
    // and now msgchk's...
    if ((! curu->isUserGuest())			// not a guest
	&& (0 != curu->pref.auto_msgchk_minutes) // not disabled auto-msgchk
	&& (now >= curu->auto_msgchk_next))	 // time for next auto-msgchk
    {
      time_t newm;
      if (SLForumCmd::i_msgchk_check(curu, newm))
      {
	// they've got new mail.
	// is it _new_ new mail? ;)
	if ((! curu->auto_msgchk_lastnew_is_valid)
	    || (newm != curu->auto_msgchk_lastnew))
	{
	  SLForumCmd::i_msgchk_printmsg(curu, newm, true);  // beep :)
	  curu->auto_msgchk_lastnew_is_valid = true;
	  curu->auto_msgchk_lastnew = newm;
	}
      }

      // either way, schedule next auto-msgchk
      curu->auto_msgchk_next = now + 1 +
	  (60 * (curu->pref.auto_msgchk_minutes - 1));

    }  // end auto-msgchk

    // and now idle
    if ( (! curu->isUserAway())			 // not already away
	 && (0 != curu->pref.auto_away_seconds)) // not disabled auto-away
    {
	    int numSecsIdle = curu->idleTime(now);
	    if (numSecsIdle >= curu->pref.auto_away_seconds)
	    {
		    // time to go idle

		    // remove 'hit any key' substate if we're in there
		    if (curu->getAdmSubstate() == ss_idlesoon_hitanykey)
			    curu->setAdmSubstate (ss_nonadmin);

		    // go idle
		    slfparam fl;
		    fl.kind   = p_is_str;
		    fl.strval = (char *) TOO_IDLE_AWAY_MSG;  // remove const
		    SLForumCmd::c_away(this, curu, 1, &fl);  // !away idle
	    } else if ( ((curu->pref.auto_away_seconds - numSecsIdle)
			<= AUTO_AWAY_WARN_SEC )
			&& (0 == (curu->pref.ignore_mask &
				  SLForumUser::MSG_IDLESOON))
			&& (curu->getAdmSubstate() != ss_idlesoon_hitanykey) )
	    {
		    // warn going idle soon
		    curu->Send
	       ("You will be set \177away soon due to your autoaway setting.",
		0, false);
		    if (curu->getAdmSubstate() == ss_nonadmin)
		    {
			    if (0 == (curu->getHint
				      (SLForumUser::HINT_IDLESOON_HITKEY)))
			    {
				curu->Send
				  ("Or, hit any key to clear your idle time.",
				   0, false);
				curu->Send
				  ("You may type \177ignore idlesoon to disable this feature.",
				   0, false);
				curu->setHint
					(SLForumUser::HINT_IDLESOON_HITKEY);
			    }
			    curu->setAdmSubstate (ss_idlesoon_hitanykey);
		    }
	    }
    }

  }  // end for


  // write the usercount file, if using that.
#if USE_USERCOUNT_FILE
  if (now - user_count_file_updated >= USERCOUNT_UPDATE_SECONDS)
  {
    UpdateUserCountFile(now);
  }
#endif  /* USE_USERCOUNT_FILE */



  // schedule for next run in 30 seconds.
  // Matt's idea. 20000201.1250 impl.

  now   = time(NULL);
  ptime = localtime(&now);
  if (ptime != NULL) {
    // aim for beginning of the next minute, or :30,
    // whichever's closer.

    register int tsec = ptime->tm_sec;
    if (tsec < 30)
      sked_alarm (30 - tsec);  // :30 sec or later
    else if (tsec != 0)
      sked_alarm (60 - tsec);  // :00 sec or later
    else
      sked_alarm (30);  // sometime in middle of this minute;
			// it will then reschedule for :00 in the next one.
			// (merely scheduling once for 60 seconds at :00 might
			// overshoot the minute we want, but scheduling for 59
			// would usually hit the end of the current one.
  } else {
    sked_alarm (30);  // if we can't get localtime, run every 30 secs or so
  }
}


void SLForumRoom::CheckHourChime() {
#ifndef HOURLY_CHIME
	return;
#else

  const struct tm	*ptime;
  time_t		now = time(NULL);
  static int		lasthr = -1; 	// for preventing two in same minute.

 ptime = localtime(&now);  // get hours, mins, etc
 if (ptime == NULL) {
   Announce ("Assert failed in CheckHourChime: ROOM:L1696: ptime is NULL");
   return;
 }

 if ((ptime->tm_min == 00) && (lasthr != ptime->tm_hour))  // hh:00 - ding!
 {
   /** first, check that folks are actually here to hear us */
   int min_idle = 60*60*(1+CHIME_MAX_QUIET_HOURS);
   int useridle;
   for (int i = 0; i<MAX_CHAT_USERS; ++i) {
     useridle = user[i]->idleTime(now);
     if (useridle < min_idle)
       min_idle = useridle;
   }

   int msgflag;
   if (min_idle <= 60*60*CHIME_MAX_QUIET_HOURS)
     msgflag = SLForumUser::MSG_HCHIME;   /* mark as chime for those who !ignore it */
   else
     msgflag = SLForumUser::MSG_HCHIME | SLForumUser::MSG_NOLOG;   /* too quiet in here;
					  * don't bloat the logfile 
					  * with it if nobody's home.
					  * (for usually-idle rooms)
					  */

   /** now, do stuff */
   for (int i = 0; i<MAX_CHAT_USERS; ++i)
       user[i]->hourlyChime (now, ptime, msgflag);

   lasthr = ptime->tm_hour;

   /* write datestamp to log at start of a new day  (20011012.1420) */
   if ((ptime->tm_hour == 0))	// in server's timezone
   {
	   char timebuf[64];  timebuf[63] = '\0';
	   strftime (timebuf, 63, "The time is now midnight, start of %A, %d %B.", ptime);
	   logprintf ("     FORUM: %s\n", timebuf);
   }

   /* write chime to recap-log (20060613.0600) */
#if USE_RECAP
   ++recap_last;
   if (recap_last >= RECAP_LINES)
   {
       recap_last = 0;  recap_wrapped = true;
   }
   recap [recap_last][0] = '\0';  // no literal, just the chime flag
   recap_desc [recap_last] = msgflag;
   recap_time [recap_last] = now;
   recap_timetype [recap_last] = RECAP_TIMETYPE_CHIME;
   recap_lasttime = now;
#endif  /* USE_RECAP */   


   // and for those who don't like chimes...
   // (must use _BroadCast because this has both hintbits and msgdescbits)
   _BroadCast
     (SYSMSG_FROM_USERNAME,
      "Type \177ignore chimes if you don't like hearing the hourly chime.",
      (SLForumUser::MSG_HCHIME|SLForumUser::MSG_SYS),
      false, SLForumUser::HINT_HCHIME);
 }
#endif	/* ifdef HOURLY_CHIME */
}



////////////////////////////////////////
// recap
//
////////////////////////////////////////
// jer 20060528.1140

#if USE_RECAP


time_t SLForumRoom::get_recap_lasttime() const
{
  return recap_lasttime;
}



/** May be -1 if no recap yet */
int SLForumRoom::get_recap_lastindex() const
{
  return recap_last;
}



/** Send the room's recap to this user.
 *
 * line_to_ignore is so they don't see their own "x has arrived" line.
 *	That would be a waste of a line of screen.
 */
void SLForumRoom::SendRecapToUser
  (SLForumUser* u, time_t lasttime_was, int line_to_ignore /* =-1 */ )
{
  bool recapped_anything = false;

  u->Send (RECAP_HEADER, 0, false);
  if ((0 == recap_last) && (! recap_wrapped) && (0 == line_to_ignore))
    u->Send (RECAP_NONE, 0, false);   // only their own "has arrived" said yet
  else if (-1 != recap_last)          // -1 == nothing said yet
  {
    int hours_chimed = 0;    // for quiet once room is idle,
	// except for start-of-day ones.
    bool user_ignores_chimes =
      (0 != (u->getIgnoringMask() & SLForumUser::MSG_HCHIME));
    int user_jetlag = u->pref.jetlag;  // used for start-of-day chime
    int istart;	// index within recap_* arrays

    if (recap_wrapped)
      istart = 1 + recap_last;  // start with oldest line
    else
      istart = 0;  // no wrap, line 0 == oldest line

    /** Loop through recap_* arrays, may start in the middle. */
    int i = istart;
    do
    {
      if (i != line_to_ignore)
      {
	switch (recap_timetype[i])
	{
	case RECAP_TIMETYPE_NONE:
	  u->Send (recap_desc[i], NULL, NULL, recap[i], 0);
	  hours_chimed = 0;    // room isn't idle anymore
	  recapped_anything = true;
	  break;

	case RECAP_TIMETYPE_STAMP:
	  {
	    const char *expan = u->expandTimeTokens
	      (recap_time[i], recap[i], NULL);
	    u->Send (recap_desc[i], NULL, NULL, expan, 0);
	  }
	  hours_chimed = 0;    // room isn't idle anymore
	  recapped_anything = true;
	  break;

	case RECAP_TIMETYPE_CHIME:
	  if (! user_ignores_chimes)
	  {
	    time_t usertime = recap_time[i] + (user_jetlag * 60 * 60);
	    struct tm *ptime;
	    ptime = localtime(&usertime);  // for hour, to check if new day

	    if ((0 == ptime->tm_hour)
		|| (hours_chimed <= CHIME_MAX_QUIET_HOURS))
	    {
	      // Announce hours when room idle, up to CHIME_MAX_QUIET_HOURS,
	      // announce start of a new day always.
	      if (0 != user_jetlag)        // un-adjust it for hourlyChime
		usertime -= (user_jetlag * 60 * 60); 
	      u->hourlyChime (usertime, ptime, SLForumUser::MSG_HCHIME);
	      ++hours_chimed;
	      recapped_anything = true;
	    }
	  }
	  break;

	}  // switch

      }  // if (i != line_to_ignore)

      ++i;
      if (i >= RECAP_LINES)
	i = 0;
      if ((! recap_wrapped) && (i > recap_last))
	break;

    } while (i != istart);  // do {} until we reach start again

    if (! recapped_anything)
      u->Send (RECAP_IGNOREALL, 0, false);
  }

  time_t now = time(NULL);
  int room_idle_min = (now - lasttime_was) / (time_t) 60;

  if (room_idle_min < RECAP_IDLE_WARN_MINUTES)
  {
    u->Send (RECAP_FOOTER, 0, false);	/* room active, not idle */
  } else if (room_idle_min <= 90)
  {
    char msg [USERBUFSIZ];
    snprintf (msg, USERBUFSIZ-1, RECAP_FOOTER_IDLE_WARN_MIN, room_idle_min);
    u->Send (msg, 0, false);	/* room at most 90 minutes idle */
  } else
  {
    char msg [USERBUFSIZ];
    snprintf (msg, USERBUFSIZ-1, RECAP_FOOTER_IDLE_WARN_HR, room_idle_min/60);
    u->Send (msg, 0, false);	/* room is not the life of the party */
  }
}


#endif  /* USE_RECAP */




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





slf_tristate SLForumRoom::checkLockUserFile (const SLForumUser *u)
{
#ifndef ALLOW_LOCKOUT
  return no;

#else

#ifndef LOCKOUT_ONLY_LOCKOUTUSERS
  return yes;
#else

  if (u->isAdmin())
    return yes;  // we trust our admins

  if (slfhomedir == NULL)
    return no;  // can't read the dir or the file

  struct stat lockoutfstat;
  char fnbuf[PATH_MAX];
  fnbuf[PATH_MAX-1]= '\0';
  snprintf(fnbuf, PATH_MAX-1,
	   "%s%c%s", slfhomedir, PATH_SEP, LOCKOUTUSERS_FILE);

  // first: do we need to (re)read the LOCKOUTUSERS_FILE ?
  {
    if (0 != stat (fnbuf, &lockoutfstat))
      return no;   // file not stattable.

    lockoutfile_mtime = lockoutfstat.st_mtime;
  }

  {
    slf_tristate canthey = u->userCanLock();
    if ((canthey != unknown)
	&& (lockoutfile_chktime_is_valid)
	&& (lockoutfile_chktime > lockoutfile_mtime))
      return canthey; // not modded since last check, no need read.
  }

  // ok, we need to re-read it.

  lockoutfile_chktime = time(NULL);
  lockoutfile_chktime_is_valid = true;


  FILE *usrf;
  usrf = slf_safe_fopen  ("user lockout allow file", fnbuf, "r");
  if (usrf==NULL)
    return no;   // file not found, thus they're not in it

  const char *uname = u->GetName();
  char oneline[32];  oneline[0] = '\0';  oneline[31] = '\0';
  int  linel;

  while ( ! feof(usrf)) {
    fgets(oneline, 31, usrf);      
    linel = strlen(oneline);
    if ((linel > 0) &&
	((oneline[linel-1] == (char)13) || (oneline[linel-1] == (char)10)))
    {
      oneline[linel-1] = '\0';
      --linel;
    }
    if ((linel > 0) && (strcmp(uname, oneline)==0))
    {
      fclose(usrf);
      return yes;  // found 'em
    }
  }
  fclose(usrf);
  return no;
#endif  /* ifdef LOCKOUT_ONLY_LOCKOUTUSERS */
#endif /* ifdef ALLOW_LOCKOUT */
}   // checkLockUserFile




void SLForumRoom::printRoomUptime (SLForumUser *u_to)
{
  char dbuf[96];  // way more than needed
  char valbuf[16];  // same

  time_t tdiff = time(NULL);
  struct tm dif;

  // no more datediff : fake it (we changed our output format)
  // by filling in the fields we use.
  tdiff = tdiff - room_startup_time;
  dif.tm_sec  = tdiff % 60;  tdiff = (tdiff - dif.tm_sec ) / 60;
  dif.tm_min  = tdiff % 60;  tdiff = (tdiff - dif.tm_min ) / 60;
  dif.tm_hour = tdiff % 24;  tdiff = (tdiff - dif.tm_hour) / 24;
  dif.tm_mday = tdiff;


#define prut_append(val,sing,plur)	\
  if (val > 0)				\
  {					\
    sprintf(valbuf, " %d %s", val, ((val != 1) ? plur : sing));	\
    strcat(dbuf, valbuf);		\
  }

  strcpy (dbuf, "Room uptime is");
  prut_append(dif.tm_mday, "day",  "days");
  prut_append(dif.tm_hour, "hour", "hours");
  prut_append(dif.tm_min,  "min",  "min");
  if (! (dif.tm_mday || dif.tm_hour))   // keep the clutter down
    prut_append(dif.tm_sec,  "sec",  "sec");

#undef prut_append

  u_to->Send(dbuf, 0, false);

  // fudge the startup time to this user's timezone:
  {
      time_t sttime = room_startup_time + (60 * 60 * u_to->pref.jetlag);
      snprintf(dbuf, 95, "(since %s", ctime(&sttime));
  }
  // chomp the ctime format
  dbuf[31] = ')';
  dbuf[32] = '.';
  dbuf[33] = '\0';

  u_to->Send(dbuf, 0, false);

}



void SLForumRoom::debug_PrintSlots () const
{
	int    i;

	logprintf("######## debug: slots status ########\n");
	logprintf("# empty_slots is %d\n", empty_slots);

	for( i=0; i<MAX_CHAT_USERS; ++i )
	  user[i]->debug_printSlotStatus();

	logprintf("######## debug: status ends  ########\n");

}  // debug_printSlots


////////////////////////////////////////
// end class SLForumRoom
//
////////////////////////////////////////
