
/* chat.c */
/* Andrew Davison (ad@ratree.psu.ac.th) */
/* 12th December 1996 */

/* Use:
     chat [port-number]
   e.g.
     chat
     chat 3001 &

   Before using a particular port number, you should check /etc/services 
   for ports used on your system. You should definitely avoid numbers 
   below 1024 which are reserved by UNIX. A default port number is stored 
   in PORT.

   Make sure you change SU_NAME so that no one can log-in as super-user 
   but you. Also change SU_EMAIL, so you get Chat users e-mail.

   Users can use telnet to connect to Chat at your site and port. 
   e.g.
     telnet catsix.coe.psu.ac.th 3001

   As background information, many of the network related functions 
   contain page references to 'Stevens', which means the text:
      "UNIX Network Programming", W. Richard Stevens,
      Prentice Hall, 1990.
   Naturally, any coding mistakes are mine.

   Chat user commands and their meanings:
      .quit                  : leave Chat
      .who                   : who is in the room with me?
      .go <room>             : go to <room>
      .help [<command>]      : give me help on <command>
      .private [<name>]      : initiate private conversation with
                               <name>, or end it
      .recent                : what were the recent public messages 
                               in the room?
      .email <message>       : send e-mail to the super-user
      .info                  : give information about Chat rooms

   Super-user commands are:
      .shutdown              : boot off all the Chat users (apart from you)
      .users                 : give information on all Chat users
      .warn <user> <message> : warn <user> with <message>
      .close <user>          : boot off <user>
      .ban <user>            : ban this user; actually ban their site

   All commands can be specified with just their first letter 
   (e.g. .q instead of .quit).

   Commands are implemented in functions beginning with the command
   name followed by '_cmd' (e.g. quit_cmd() ).

   General talk starts with anything other than a '.'
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/ioctl.h>       /* ioctl() */
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>        /* inet_ntoa() */
#include <bstring.h>          /* bcopy() */
#include <unistd.h>           /* read(), write(), close() */
#include <time.h>

#define MAX_USERS 20       /* max number of Chat users;
                              cannot exceed FD_SETSIZE used by select() */
#define MAXLEN 120         /* max length of a string */
#define MAX_ROOMS 5        /* max number of Chat rooms */
#define MAX_MSGS 5         /* max number of recent messages stored in a room */

#define PORT 2001          /* default port for server */
                           /* should check /etc/services to avoid clashes */

#define WAIT_NAME 7        /* seconds given to user to type a name */

/* times are approximate due to behaviour of wait_contact() */
#define CLOSE_TIME 600     /* 10 mins of inactivity means a warning */
#define REPRIEVE_TIME 120  /* 2 mins to act before user shutdown */

#define ROOMS "rooms.chat"  /* file holding room descriptions */
#define BANNED "ban.chat"   /* file holding banned sites */
#define LOG "log.chat"      /* file holding logged information */

/* **CHANGE** these strings for your configuration */
/* Not changing SU_NAME leaves a security hole in Chat */
#define SU_NAME  "Andrew"                 /* super-user's chat name */
#define SU_EMAIL "ad@ratree.psu.ac.th"    /* address of SU contact */



enum Command {QUIT, WHO, GO, HELP, PRIVATE, RECENT, EMAIL, INFO,
              BAN, SHUTDOWN, USERS, WARN, CLOSE, UNKNOWN };
                             /* chat commands */

enum Status {USER, SU};      /* status of chat participants */


typedef struct {
  char name[MAXLEN];         /* user name */
  int sockfd;                /* their socket descriptor */
  struct sockaddr_in addr;   /* address of their site */
  enum Status status;        /* USER or SU */
  int room_loc;              /* user's location in terms of rooms[] index */
  int private_contact;       /* other user in terms of their users[] index */
  int warned;                /* 0 or 1 */
  time_t act_time;           /* time of last action */
} User;     /* info about a chat user */


typedef struct {
  char *msg[MAX_MSGS];    /* recent messages stored in a room */
  int first, last, size;  
} Messages;

typedef struct {
  char name[MAXLEN];             /* room name */
  char desc[MAXLEN];             /* description of the room */
  Messages talk;                 /* recent talk in the room */
  int curr_members[MAX_USERS];   /* elements hold 1 or 0; index denotes user */
} Room;     /* info about a room */


/* function prototypes */
int port_arg(int argc, char *argv[]);
void init_users(User users[]);
void init_rooms(Room rooms[]);
int obtain_socket(int port);
void wait_contact(User users[], int ssockfd, fd_set *readmaskp);

/* process a new user */
void new_user(User users[], Room rooms[], int ssockfd);
void all_users(int sfd, User users[]);
void store_name(User users[], Room rooms[], int csockfd, char *name,
                                          struct sockaddr_in cli_addr);
int get_name(int sfd, char name[], User users[]);
int valid(int sfd, char name[], User users[]);
int in_users(User users[], char *name);
int is_su(User users[], char *name);
void add_user(User users[], int posn, char *name, int sfd, 
                                      struct sockaddr_in addr);
void get_site(struct sockaddr_in *addr, char site[]);
int is_banned(char site[]);
int free_uspot(User users[]);

/* process an existing user */
void old_user(User users[], Room rooms[], fd_set *readmaskp);
void process_old(User users[], int posn, Room rooms[]);
enum Command find_command(char msg[], char **args);
void lower_case(char cmd[]);

/* User commands */
void quit_cmd(User users[], int posn, Room rooms[]);
void remove_user(User users[], int posn, Room rooms[]);
void leave_room(Room rooms[], int rno, User users[], int posn);
void who_cmd(User users[], int posn, Room rooms[]);
void go_cmd(User users[], int posn, Room rooms[], char *args);
void get_word(char *args, char wd[]);
int get_rnum(Room rooms[], char *rname);
void enter_room(Room rooms[], int rno, User users[], int posn);
void help_cmd(User u, char *args);
void private_cmd(User users[], int posn, char *args);
int get_uno(User users[], int posn, char *args);
void recent_cmd(User u, Room r);
void email_cmd(User u, char *args);
void info_cmd(User u, Room rooms[]);
int sum_members(int members[]);

/* Super-User commands */
void shutdown_cmd(User users[], int posn, Room rooms[]);
void users_cmd(User users[], int posn, Room rooms[]);
void warn_cmd(User users[], int posn, char *args);
void close_cmd(User users[], int posn, Room rooms[], char *args);
void ban_cmd(User users[], int posn, Room rooms[], char *args);
void add_to_ban(char *site);

void talk(User users[], int posn, char *args, Room rooms[]);
void add_msg(Messages *t, char *msg);
void check_activity(User users[], int posn, Room rooms[]);

/* socket I/O */
int dputs(int sfd, char *msg);
int dgets(int sfd, char msg[]);

/* logging */
void write_log(char *msg);



void main(int argc, char *argv[])
/* Initialise the two main Chat data structures (users and rooms).
   Create a server socket (ssockfd) and wait for connections.
   A new user connection must come through ssockfd, but later contact
   will be via dedicated sockets for each user.
*/
{
  User users[MAX_USERS];      /* chat users */
  Room rooms[MAX_ROOMS];      /* chat rooms */

  int port, ssockfd;
  fd_set readmask;

  printf("Chat Starting...\n");
  port = port_arg(argc, argv);
  init_users(users);
  init_rooms(rooms);

  signal(SIGPIPE, SIG_IGN);    /* avoid termination due to 
                                  socket write errors */

  ssockfd = obtain_socket(port);

  while(1) {
    wait_contact(users, ssockfd, &readmask);
    if (FD_ISSET(ssockfd, &readmask))      /* new user has contacted server */
      new_user(users, rooms, ssockfd);
    else                                   /* message from existing user */
      old_user(users, rooms, &readmask);
  }
  /* execution never gets here; chat must be killed by OS commands */
}


int port_arg(int argc, char *argv[])
/* Get a port number, either from the command line or from PORT */
{
  int p;

  if (argc > 2) {
    fprintf(stderr, "Usage: chat [port]\n");
    exit(1);
  }
  if (argc == 2) {
    p = atoi(argv[1]);
    if (p <= 1023) {     /* port numbers <= 1023 are reserved */
      fprintf(stderr, "Port must be > 1023; using %d\n", PORT);
      p = PORT;
    }
    else
      printf("Using port: %d\n", p);
  }
  else if (argc == 1) {
    printf("Using default port number: %d\n", PORT);
    p = PORT;
  }
  return p;
}


void init_users(User users[])
/* Initialise the users array to be 'empty'. 
   The super-user (SU) has his/her name placed in the first slot, so
   that Chat knows who he/she is when they initially connect. */
{
  int i;

  for(i=0; i < MAX_USERS; i++) {
    users[i].name[0] = '\0';            /* empty name */
    users[i].sockfd = -1;               /* no socket */
    users[i].private_contact = -1;      /* no private contact */
    users[i].status = USER;
    users[i].warned = 0;
    users[i].room_loc = 0;              /* first room in rooms[] */
  }

  /* record super-user name and status */
  strcpy(users[0].name, SU_NAME);  
  printf("Super-User Chat name is \"%s\"\n", users[0].name);
  users[0].status = SU;
}


void init_rooms(Room rooms[])
/* Initialise the rooms array by reading in names and descriptions
   from the ROOMS file. Its format is:	
      name description\n
            :
   We assume that the first line describes the room
   where people are placed when they first connect to Chat.
*/
{
  FILE *fp;
  int i = 0, j;

  if ((fp = fopen(ROOMS, "r")) == NULL) {
    fprintf(stderr, "No room descriptions file: %s\n", ROOMS);
    exit(1);
  }
  while ((i < MAX_ROOMS) && 
         (fscanf(fp, "%s %[^\n]", rooms[i].name, rooms[i].desc) == 2)) {
    rooms[i].talk.first = rooms[i].talk.last = rooms[i].talk.size = 0;
    for (j=0; j < MAX_USERS; j++)
      rooms[i].curr_members[j] = 0;     /* no occupants as yet */
    i++;
  }
  fclose(fp);
  while (i < MAX_ROOMS) {   /* more space in rooms[] than descriptions */
    rooms[i].name[0] = '\0';
    i++;
  }
}


int obtain_socket(int port)
/* Perform the first four steps of creating a server:
   create a socket, initialise the address data structure,
   bind the address to the socket, and wait for connections. 
   See Stevens, Figure 6.2., p.261 for a nice pictorial overview. 
   See Stevens p.284-285 for a server example.
*/
{
  int sockfd;
  struct sockaddr_in serv_addr;

  /* open a TCP socket */
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    fprintf(stderr, "Could not open a socket");
    exit(1);
  }

  /* initialise socket address */
  bzero((char *)&serv_addr, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_addr.sin_port = htons(port);

  /* bind socket to address */
  if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
    fprintf(stderr, "Could not bind socket to address\n");
    exit(1);
  }

  /* set socket to listen for incoming connections */
  /* allow a queue of 10 */
  if (listen(sockfd, 10) == -1) {
    fprintf(stderr, "listen error\n");
    exit(1);
  }
  return sockfd;
}


void wait_contact(User users[], int ssockfd, fd_set *readmaskp)
/* The server is set to wait until a message arrives from one of 
   the specified socket descriptors. This is achieved by supplying 
   a set of descriptors in readmaskp to a select() call. 
   See Stevens p.328-331 for background on select() and 
   p.594-596 for some example code.
*/
{
  int i, contact;
  
  FD_ZERO(readmaskp);                      /* initialise mask */
  for (i=0; i < MAX_USERS; i++)
    if (users[i].sockfd != -1)             /* if socket is active */
      FD_SET(users[i].sockfd, readmaskp);  /* add to mask */

  FD_SET(ssockfd, readmaskp);              /* add server socket */

  /* now wait for connection */
  contact = select(FD_SETSIZE, readmaskp, (fd_set *)0, (fd_set *)0,
                                               (struct timeval *)0);

  /* contact is bound to the number of clients who have
     sent messages to the server */
  if (contact < 0) {
    fprintf(stderr, "select error\n");
    exit(1);
  }
}


void new_user(User users[], Room rooms[], int ssockfd)
/* A new user has contacted the server, so accept the contact,
   and assigns a new socket to them (csockfd). Their details
   are then recorded.
   accept() is explained in Stevens p.272-274 */
{
  char name[MAXLEN];
  int csockfd, clilen;
  struct sockaddr_in cli_addr;

  clilen = sizeof(cli_addr);
  csockfd  = accept(ssockfd, (struct sockaddr *)&cli_addr, &clilen);
  if (csockfd < 0)
    fprintf(stderr, "accept error\n");
  else {
    dputs(csockfd, "Welcome to Chat\n");
    all_users(csockfd, users);
    if (get_name(csockfd, name, users))
      store_name(users, rooms, csockfd, name, cli_addr);
  }
}


void all_users(int sfd, User users[])
/* Print a list of all the chat users. This allows the new
   user to choose a unique name.  */
{
  char msg[MAXLEN];
  int i, found = 0;

  strcpy(msg, "Present Chat users: ");
  for (i=0; i < MAX_USERS; i++)
    if (users[i].sockfd != -1) {
      found = 1;    /* found someone to mention */
      strcat(msg, users[i].name);
      strcat(msg, "  ");
    }
  if (!found)    /* no chat users! */
    dputs(sfd, "No one in Chat at the moment!");
  else
    dputs(sfd, msg);
}


void store_name(User users[], Room rooms[], int csockfd, char *name,
                                          struct sockaddr_in cli_addr)
/* Storing a name is complicated by having to check whether the
   person is the super-user, and by checking that they are not
   contacting chat from a banned site. */
{
  char site[MAXLEN];
  int posn;

  if ((posn = is_su(users, name)) != -1) {
    dputs(csockfd, "Hello, Mr. Super-User");
    add_user(users, posn, name, csockfd, cli_addr);
    enter_room(rooms, 0, users, posn);      /* rooms[0] is the Chat entry */
  }
  else {
    get_site(&cli_addr, site);
    if (is_banned(site)) {
      dputs(csockfd, "Your site is banned... goodbye");
      close(csockfd);
    }
    else {    /* not banned */
      if ((posn = free_uspot(users)) != -1) {
        add_user(users, posn, name, csockfd, cli_addr);
        enter_room(rooms, 0, users, posn);
      }
      else {
        dputs(csockfd, "Sorry, no space in chat at the moment. Try later.");
        close(csockfd);
      }
    }
  }
}


int get_name(int sfd, char name[], User users[])
/* get_name() gets the user to enter a name.  
   This could be coded by simply calling dgets(), but
   the normal blocking behaviour of a socket means that a
   dgets() call could be kept waiting indefinitely if the user
   did not enter a name. If dgets() is held up, then so will the
   entire chat system. 

   Instead, select() is used to monitor the socket for a finite 
   amount of time (specified in WAIT_NAME). If a message is sent to
   the socket (i.e. a name is typed) then the wait is terminated
   and dgets() is called to read the name. It is then tested for
   validity by valid().
   If nothing has arrived at the socket after the timeout then 
   get_name() returns 0.
*/
{
  int maxsock, nummesg;
  char msg[MAXLEN];
  struct timeval timeout;
  fd_set readmask;

  timeout.tv_sec = WAIT_NAME;  /* seconds delay */
  timeout.tv_usec = 0;         /* microseconds delay */
  FD_ZERO(&readmask);
  FD_SET(sfd, &readmask);
  maxsock = sfd+1;

  sprintf(msg, "You have %d seconds to enter a unique name: ", WAIT_NAME);
  dputs(sfd, msg);

  nummesg = select(maxsock, &readmask, (fd_set *)0, (fd_set *)0, &timeout);

  if (nummesg == 0) {                 /* timeout exhausted; no message */
    dputs(sfd, "Sorry, too slow... goodbye");
    close(sfd);
    return 0;
  }

  if (FD_ISSET(sfd, &readmask)) {     /* message waiting on socket */
    if (dgets(sfd, name) == -1) {     /* read error */
      dputs(sfd, "Chat input error... goodbye");
      close(sfd);
      return 0;
    }
    else {                            /* read is okay */
      if (!valid(sfd, name, users)) { /* name is not valid */
        dputs(sfd, "... goodbye");
        close(sfd);
        return 0;
      }
      else                            /* name is valid */
        return 1;
    }
  }

  /* should never reach this point */
  fprintf(stderr, "select() error in get_name\n");
  dputs(sfd, "Chat input selection error... goodbye");
  close(sfd);
  return 0;
}



int valid(int sfd, char name[], User users[])
/* Validity criteria:
   A name consisting of only '\0' is not valid. A name must
   not be used already (unless it is the super-user name).  */
{
  int posn;

  if (name[0] == '\0') {
    dputs(sfd, "An empty name is not valid");
    return 0;
  }
  if ((posn = in_users(users, name)) != -1) {    /* is name already used? */
    if ((users[posn].status == SU) && (users[posn].sockfd == -1))  
      return 1;         /* super-user's name & not already connected */
    else {
      dputs(sfd, "A person already has that name in Chat");
      return 0;
    }
  }
  else      /* name is not already used in users[] */
    return 1;
}


int in_users(User users[], char *name)
/* Is name already used in users? The search assumes that name 
   is not '\0', since empty users slots use '\0' in the name field.
   Return the index position if name is found.
*/
{
  int i;
  for (i=0; i < MAX_USERS; i++)
    if (strcmp(users[i].name, name) == 0)
      return i;
  return -1;
}


int is_su(User users[], char *name)
/* Is this the super-user name? Return its index position in users
   if it is found. */
{
  int i;
  for (i=0; i < MAX_USERS; i++)
    if (users[i].status == SU)
      if (strcmp(users[i].name, name) == 0)
        return i;
  return -1;
}

void add_user(User users[], int posn, char *name, int sfd, 
                                          struct sockaddr_in addr)
/* Add a new user to the users[] array.
   Log the user's entry into Chat.
*/
{
  char msg[MAXLEN];
  time_t t;

  strcpy(users[posn].name, name);
  users[posn].sockfd = sfd;
  users[posn].addr = addr;
  users[posn].act_time = time(NULL);
  dputs(sfd, "\nYou have been added to Chat\n");

  t = time(NULL);
  sprintf(msg, "%s added at %s", users[posn].name, ctime(&t));
  write_log(msg);
}


void get_site(struct sockaddr_in *addr, char site[])
/* Given the socket address of the client (addr), store its name
   in site[]. If the name cannot be found, store its dotted decimal 
   version. */
{
  struct hostent *host;

  host = gethostbyaddr((char *) &(addr->sin_addr), sizeof(struct in_addr),
                                AF_INET);   /* see Stevens p.395, p.649-650 */
  if (host != NULL)
    strcpy(site, host->h_name);   /* name of client's site (Stevens p.393) */
  else
    strcpy(site, inet_ntoa(addr->sin_addr));
                           /* client's site in dotted decimal form (p.277) */
}


int is_banned(char site[])
/* Is this site banned? Check against a list stored in the BANNED file. 
   This mechanism is fairly simple-minded, since site names can be 
   readily forged. It is also unfair to other users at the same site.
*/
{
  FILE *fp;
  char bansite[MAXLEN];

  if ((fp = fopen(BANNED, "r")) == NULL)
    fprintf(stderr, "Could not read %s\n", BANNED);
  else {
    while (fgets(bansite, MAXLEN, fp) != NULL) {
      bansite[strlen(bansite)-1] = '\0';   /* overwrite \n */
      if (strcmp(bansite, site) == 0) {
        fclose(fp);
        return 1;
      }
    }
    fclose(fp);
  }
  return 0;
}


int free_uspot(User users[])
/* Is there a free spot in users[] for a new user?
   Detect an empty spot by looking at the sockfd field. */
{
  int i;
  for (i=0; i < MAX_USERS; i++)
    if ((users[i].status == USER) && (users[i].sockfd == -1))
      return i;
  return -1;
}



/* Process existing Chat users */

void old_user(User users[], Room rooms[], fd_set *readmaskp)
/* old_user() is called after select() has been woken up.
   select() can be triggered by messages from more
   than one client, and so old_user() should choose fairly between
   multiple messages so that every user eventually gets processed.

   old_user() cycles through users[], always starting from the beginning
   of the array. Thus, for a single old_user() call, every user will
   eventually be processed, although users at the end of the array
   will always be serviced after ones at the beginning.

   Another issue with this coding approach is that one user is processed 
   at a time. However, the delay for other users is fairly small since
   all user messages (e.g. ".who") can be processed by Chat without
   waiting for further input. The one exception is when a user
   first connects to Chat and is prompted for their name. See get_name()
   for details.

   Users with no waiting messages are checked for activity --
   too long with no activity means they are disconnected.
*/
{
  int i;

  for (i=0; i < MAX_USERS; i++)
    if (users[i].sockfd != -1) {
      if (FD_ISSET(users[i].sockfd, readmaskp))
        process_old(users, i, rooms);
      else
        check_activity(users, i, rooms);
    }
}


void process_old(User users[], int posn, Room rooms[])
/* The user in slot posn of users[] has his message processed.
   Problems with the socket are dealt with first, then empty messages.
   A message starting with a '.' is a command, otherwise it is
   public (or private) talk. */
{
  char msg[MAXLEN], *args;
  enum Command cmd;

  if (dgets(users[posn].sockfd, msg) == -1) {    /* socket error */
    remove_user(users, posn, rooms);
    return;
  };
  if (msg[0] == '\0') {                 /* ignore empty messages */
    dputs(users[posn].sockfd, "I'll pass on that one!");
    return;
  }

  users[posn].act_time = time(NULL);    /* record this activity's time */

  if (msg[0] == '.') {                  /* message is a command */
    cmd = find_command(msg, &args);
    switch(cmd) {
      case QUIT: 
        quit_cmd(users, posn, rooms); break;
      case WHO:  
        who_cmd(users, posn, rooms); break;
      case GO: 
        go_cmd(users, posn, rooms, args); break;
      case HELP:  
        help_cmd(users[posn], args); break;
      case PRIVATE: 
        private_cmd(users, posn, args); break;
      case RECENT:  
        recent_cmd(users[posn], rooms[users[posn].room_loc]); break;
      case EMAIL: 
        email_cmd(users[posn], args); break;
      case INFO:
        info_cmd(users[posn], rooms); break;                                    
      case SHUTDOWN: 
        shutdown_cmd(users, posn, rooms); break;
      case USERS:  
        users_cmd(users, posn, rooms); break;
      case WARN: 
        warn_cmd(users, posn, args); break;
      case CLOSE:  
        close_cmd(users, posn, rooms, args); break;
      case BAN: 
        ban_cmd(users, posn, rooms, args); break;
      case UNKNOWN:
        dputs(users[posn].sockfd, "Chat did not understand that command"); break;
      default:
        dputs(users[posn].sockfd, "No way, Punk"); break;
    }
    if (cmd != QUIT)
      dputs(users[posn].sockfd, "");     /* newline after Chat's response */
  }
  else
    talk(users, posn, msg, rooms);       /* public or private talk */
}


enum Command find_command(char msg[], char **args)
/* The general format of a command message is:
     .command  [args]
   Extract the command from the message, and point args to
   any arguments after the command.
*/
{
  enum Command c;
  char cmd[MAXLEN];    /* command string is stored in here */
  int i = 1, j = 0;

  while ((msg[i] != ' ') && (msg[i] != '\0') &&
         (msg[i] != '\r') && (i < MAXLEN))
    cmd[j++] = msg[i++];
  cmd[j] = '\0';
  lower_case(cmd);
  
  /* all the present Chat commands start with a unique letter, 
     so choosing between them could be coded less expensively */

  if ((strcmp(cmd,"quit") == 0) || (strcmp(cmd,"q") == 0))
    c = QUIT;
  else if ((strcmp(cmd,"who") == 0) || (strcmp(cmd,"w") == 0))
    c = WHO;
  else if ((strcmp(cmd,"go") == 0) || (strcmp(cmd,"g") == 0))
    c = GO;
  else if ((strcmp(cmd,"help") == 0) || (strcmp(cmd,"h") == 0))
    c = HELP;
  else if ((strcmp(cmd,"private") == 0) || (strcmp(cmd,"p") == 0))
    c = PRIVATE;
  else if ((strcmp(cmd,"recent") == 0) || (strcmp(cmd,"r") == 0))
    c = RECENT;
  else if ((strcmp(cmd,"email") == 0) || (strcmp(cmd,"e-mail") == 0) ||
           (strcmp(cmd,"e") == 0))
    c = EMAIL;
  else if ((strcmp(cmd,"info") == 0) || (strcmp(cmd,"i") == 0))
    c = INFO;
  else if ((strcmp(cmd,"ban") == 0) || (strcmp(cmd,"b") == 0))
    c = BAN;
  else if ((strcmp(cmd,"shutdown") == 0) || (strcmp(cmd,"shut") == 0) ||
           (strcmp(cmd,"s") == 0))
    c = SHUTDOWN;
  else if ((strcmp(cmd,"warn") == 0) || (strcmp(cmd,"w") == 0))
    c = WARN;
  else if ((strcmp(cmd,"close") == 0) || (strcmp(cmd,"c") == 0))
    c = CLOSE;
  else if ((strcmp(cmd,"users") == 0) || (strcmp(cmd,"u") == 0))
    c = USERS;
  else
    c = UNKNOWN;

  while (msg[i] == ' ')  /* skip to end or to start of arguments */
    i++;
  *args = &msg[i];       /* arguments or '\0' */

  return c;
}


void lower_case(char wd[])
/* convert word to lowercase letters */
{
  int i = 0;
  while (wd[i] != '\0') {
    wd[i] = tolower(wd[i]);
    i++;
  }
}


/* User commands start here. These include:
      quit, who, go, help, private, recent, email, info
   The super-user commands start after these.
*/


void quit_cmd(User users[], int posn, Room rooms[])
/* Quit Chat */
{
  dputs(users[posn].sockfd, "Goodbye, come back soon");
  remove_user(users, posn, rooms);
}


void remove_user(User users[], int posn, Room rooms[])
/* First the user is removed from the room, then their
   socket link is closed. Finally, their details are 
   deleted from the users[] array. We log their departure as well.
*/
{
  char msg[MAXLEN];
  time_t t;

  leave_room(rooms, users[posn].room_loc, users, posn);

  /* log the departure of a user */
  t = time(NULL);
  sprintf(msg, "%s left at %s", users[posn].name, ctime(&t));
  write_log(msg);

  close(users[posn].sockfd); 

  if (users[posn].status != SU)
    users[posn].name[0] = '\0';
  users[posn].sockfd = -1;
  users[posn].private_contact = -1;
  users[posn].warned = 0;
}


void leave_room(Room rooms[], int rno, User users[], int posn)
/* Leaving a room means updating the curr_members array in
   rooms[]. Also, everyone else in the room must be told of the
   departure. */
{
  int i;
  char msg[MAXLEN];

  rooms[rno].curr_members[posn] = 0;  /* remove from room */
  users[posn].room_loc = -1;
  sprintf(msg, "You have left the %s", rooms[rno].name);
  dputs(users[posn].sockfd, msg);
  sprintf(msg, "%s has left the room", users[posn].name);
  for (i=0; i < MAX_USERS; i++)
    if (rooms[rno].curr_members[i])
      dputs(users[i].sockfd, msg);
}


void who_cmd(User users[], int posn, Room rooms[])
/* Who else is in the user's current room? */
{
  char msg[MAXLEN];
  int i, rloc, found = 0;

  rloc = users[posn].room_loc;
  sprintf(msg, "Other people in the %s:  ", rooms[rloc].name);
  for (i=0; i < MAX_USERS; i++)
    if ((rooms[rloc].curr_members[i]) && (i != posn)) {
      found = 1;   /* found someone */
      strcat(msg, users[i].name);
      strcat(msg, "  ");
    }
  if (!found)    /* no members */
    dputs(users[posn].sockfd, "Only you!");
  else
    dputs(users[posn].sockfd, msg);
}



void go_cmd(User users[], int posn, Room rooms[], char *args)
/* Go to another room. The destination room is extracted from
   the argument part of the command line.
*/
{
  char to_room[MAXLEN], msg[MAXLEN];
  int new_rm, old_rm;

  get_word(args, to_room);
  new_rm = get_rnum(rooms, to_room);   /* get rooms[] index of to_room */
  if (new_rm != -1) {
    old_rm = users[posn].room_loc;
    leave_room(rooms, old_rm, users, posn);
    enter_room(rooms, new_rm, users, posn);
  }
  else {
    sprintf(msg, "No room called %s", to_room);
    dputs(users[posn].sockfd, msg);
  }
}


void get_word(char *args, char wd[])
/* A word is any text up to a space or null char */
{
  int i = 0;

  while ((*args != ' ') && (*args != '\0') && (i < MAXLEN)) {
    wd[i++] = *args;
    args++;
  }
  wd[i] = '\0';
}


int get_rnum(Room rooms[], char *rname)
/* Get the index in rooms[] of the room called rname */
{
  int i;
  for (i=0; i < MAX_ROOMS; i++)
    if (rooms[i].name[0] != '\0')
      if (strcmp(rooms[i].name, rname) == 0)
        return i;
  return -1;
}



void enter_room(Room rooms[], int rno, User users[], int posn)
/* Enter a room. Firstly tell everyone in the room, and then
   update rooms[] and users[]. */
{
  int i;
  char msg[MAXLEN];

  sprintf(msg, "%s has entered the %s\n", users[posn].name, 
                                rooms[rno].name);
  for (i=0; i < MAX_USERS; i++)
    if (rooms[rno].curr_members[i])
      dputs(users[i].sockfd, msg);

  rooms[rno].curr_members[posn] = 1;      /* add to room */
  users[posn].room_loc = rno;

  sprintf(msg, "You have entered the %s", rooms[rno].name);
  dputs(users[posn].sockfd, msg);
  dputs(users[posn].sockfd, rooms[rno].desc);
  dputs(users[posn].sockfd, "");  /* new line */
}



void help_cmd(User u, char *args)
/* Help command. Fairly basic, but better than nothing. */
{
  char hcmd[MAXLEN];

  get_word(args, hcmd);
  if (hcmd[0] == '\0') {   /* no argument to help */
    dputs(u.sockfd, "Type .help <cmd>, where <cmd> is one of:");
    dputs(u.sockfd, "  quit, who, go, private, recent, email, info");
  }
  else if (strcmp(hcmd, "quit") == 0)
    dputs(u.sockfd, "Causes you to leave Chat");
  else if (strcmp(hcmd, "who") == 0)
    dputs(u.sockfd, "Lists the other people in the room");
  else if (strcmp(hcmd, "go") == 0)
    dputs(u.sockfd, ".go <room> takes you to <room>");
  else if (strcmp(hcmd, "private") == 0) {
    dputs(u.sockfd, ".private <name> puts you into private mode with <user>");
    dputs(u.sockfd, "<user> must do the same with you for private conversation to occur");
    dputs(u.sockfd, "\n.private puts you into public mode");
  }
  else if (strcmp(hcmd, "recent") == 0)
    dputs(u.sockfd, "Lists the recent public messages in the room");
  else if (strcmp(hcmd, "email") == 0)
    dputs(u.sockfd, ".email <text> sends <text> to the super-user");
  else if (strcmp(hcmd, "info") == 0)
    dputs(u.sockfd, "Gives information about all the Chat rooms");
  else
    dputs(u.sockfd, "Sorry, Chat does not understand that help request");
}



void private_cmd(User users[], int posn, char *args)
/* The private command has two functions. 1) If the user requests 
   .private <name> when in public mode then they go into provisional 
   private mode with <name>. Only when <name> also goes into private 
   mode with the user will private communication be allowed.
   2) If the user types .private when in private mode, then they
   go back into public mode, and the other person can no longer send 
   them private messages. This two-part aspect of private communication 
   means that both people must agree before private messages can happen.

   The code is lengthened by all the messages that have to be 
   sent to the two participants.
*/
{
  char msg[MAXLEN];
  int other_no;

  if (users[posn].private_contact == -1) { /* public mode --> private */
    if ((other_no = get_uno(users, posn, args)) != -1) {
      users[posn].private_contact = other_no;
      if (users[other_no].private_contact != posn) {
        sprintf(msg, "%s is not yet in private mode with you", users[other_no].name);
        dputs(users[posn].sockfd, msg);
        sprintf(msg, "%s has asked to be in private mode with you", users[posn].name);
        dputs(users[other_no].sockfd, msg);
      }
      else {  
        sprintf(msg, "%s is in private mode with you", users[other_no].name);
        dputs(users[posn].sockfd, msg);
        sprintf(msg, "%s is in private mode with you", users[posn].name);
        dputs(users[other_no].sockfd, msg);
      }
    }
  }
  else {  /* private mode --> public */
    sprintf(msg, "%s is terminating private mode", users[posn].name);
    dputs(users[users[posn].private_contact].sockfd, msg);
    users[posn].private_contact = -1;
    dputs(users[posn].sockfd, "You are back in public mode");
  }
}



int get_uno(User users[], int posn, char *args)
/* Extract a name from the arguments string, and find the user with 
   that name in users[]. Return his/her position, or -1 as an error. */
{
  char uname[MAXLEN], msg[MAXLEN];
  int i;

  get_word(args, uname);
  if (uname[0] == '\0') {
    dputs(users[posn].sockfd, "You must give a name");
    return -1;
  }

  for (i=0; i < MAX_USERS; i++)
    if (strcmp(users[i].name, uname) == 0)
      break;
  if (i == MAX_USERS) {
    sprintf(msg, "%s is not in Chat at the moment", uname);
    dputs(users[posn].sockfd, msg);
    return -1;
  }
  return i;
}


void recent_cmd(User u, Room r)
/* List the recent public mesages in the user's current room.
   The oldest message is listed first.
*/
{
  int i, count;

  if (r.talk.size == 0)
    dputs(u.sockfd, "Sorry, no recent public messages in here");
  else {
    dputs(u.sockfd, "Recent public messages in this room: ");
    i = r.talk.first;
    count = r.talk.size;
    while (count > 0) {
      dputs(u.sockfd, r.talk.msg[i]);
      i = (i+1) % MAX_MSGS;
      count--;
    }
  }
}


void email_cmd(User u, char *args)
/* Send an e-mail message to the super-user.
   The idea is for users to make comments, report problems
   without the super-user having to constantly check a 
   comments file or reports file.  */
{
  char msg[MAXLEN];
  FILE *fp;

  sprintf(msg, "/usr/bin/mail -s \'Chat Message\' %s", SU_EMAIL);
  fp = popen(msg, "w");
  if (fp == NULL) {
    dputs(u.sockfd, "Sorry, mail is not available");
    fprintf(stderr, "popen() error for e-mail\n");
  }
  else {
    fprintf(fp, "%s\n", args);   /* the user's text */
    fclose(fp);
    dputs(u.sockfd, "Your message has been e-mailed to the super-user");
  }
}


void info_cmd(User u, Room rooms[])
/* Obtain information about Chat rooms. This includes a
   list of all the room names with the number of occupants.
   The user's current room location is also printed.  */
{
  int i, sum, total;
  char msg[MAXLEN];

  dputs(u.sockfd, "Chat rooms with numbers of visitors");
  
  i = 0; total = 0;
  while ((rooms[i].name[0] != '\0') && (i < MAX_ROOMS)) {  /* end of rooms */
    sum = sum_members(rooms[i].curr_members);
    if (sum == 0)
      sprintf(msg, "%s: No one!", rooms[i].name);
    else
      sprintf(msg, "%s: %d", rooms[i].name, sum);
    dputs(u.sockfd, msg);
    total = total + sum;
    i++;
  }
  dputs(u.sockfd, "---------");
  sprintf(msg, "Total: %d", total);
  dputs(u.sockfd, msg);
  sprintf(msg, "You are in the %s", rooms[u.room_loc].name);
  dputs(u.sockfd, msg);
}


int sum_members(int curr_members[])
/* Return the number of people in the room. The curr_members array
   contains 0's and 1's, and the index position in curr_members
   corresponds to the index in the users array. So, a '1' in
   curr_members[3] means that users[3] is present in the room. 
*/
{
  int i, sum = 0;
  for (i=0; i < MAX_USERS; i++)
    sum = sum + curr_members[i];    /* contains 0 or 1 */
  return sum;
}


/* super-user commands start here. These include:
       shutdown, users, warn, close, and ban
   An ordinary user is not allowed to execute these commands. 
*/


void shutdown_cmd(User users[], int posn, Room rooms[])
/* Shutdown all the other users, apart from the super-user. */
{
  int i;

  if (users[posn].status != SU)
    dputs(users[posn].sockfd, "You do not have shutdown rights");
  else {
    for (i=0; i < MAX_USERS; i++)
      if ((users[i].sockfd != -1) && (i != posn)) {
        dputs(users[i].sockfd, "Sorry, Chat is closing down...Goodbye");
        remove_user(users, i, rooms);
      }
    dputs(users[posn].sockfd, "User Shutdown completed");
  }
}


void users_cmd(User users[], int posn, Room rooms[])
/* List information about all Chat users. This includes their
   users[] position, name, socket descriptor, private contact
   value, room name, user status (user=0, su=1), client site name,
   and last activity time.  */
{
  int i;
  char msg[MAXLEN], site[MAXLEN];
  time_t t;

  if (users[posn].status != SU)
    dputs(users[posn].sockfd, "You do not have users info rights");
  else {
    for (i=0; i < MAX_USERS; i++)
      if (users[i].sockfd != -1) {
        sprintf(msg, "\n  posn: %d \t name: %s", i, users[i].name);
        dputs(users[posn].sockfd, msg);
        sprintf(msg, "  sockfd: %d \t private_contact: %d", 
                 users[i].sockfd, users[i].private_contact);
        dputs(users[posn].sockfd, msg);
        get_site(&(users[i].addr), site);
        sprintf(msg, "  room: %s \t status: %d \t addr: %s", 
                 rooms[users[i].room_loc].name, users[i].status, site);
        dputs(users[posn].sockfd, msg);
        sprintf(msg, "  act_time: %s", ctime(&(users[i].act_time)));
        dputs(users[posn].sockfd, msg);
      }
    t = time(NULL);
    sprintf(msg, "Current time: %s", ctime(&t) );
    dputs(users[posn].sockfd, msg);
  }
}


void warn_cmd(User users[], int posn, char *args)
/* Warn a user. The message comes from args which
   contains the string "user message".  */
{
  int user_no;
  char msg[MAXLEN];

  if (users[posn].status != SU)
    dputs(users[posn].sockfd, "You do not have warning rights");
  else {
    if ((user_no = get_uno(users, posn, args)) != -1) {
      sprintf(msg, "Super-User WARNING: %s", args);
      dputs(users[user_no].sockfd, msg);
      sprintf(msg, "%s has been warned", users[user_no].name);
      dputs(users[posn].sockfd, msg);
    }
  }
}


void close_cmd(User users[], int posn, Room rooms[], char *args)
/* Close down a user */
{
  int user_no;

  if (users[posn].status != SU)
    dputs(users[posn].sockfd, "You do not have close rights");
  else {
    if ((user_no = get_uno(users, posn, args)) != -1) {
      dputs(users[user_no].sockfd, 
             "Sorry, but the Super-User is closing your Chat connection... Goodbye");
      remove_user(users, user_no, rooms);
    }
  }
}


void ban_cmd(User users[], int posn, Room rooms[], char *args)
/* Ban a user; actually it is their site which is banned by
   being added to the BANNED file. A side-effect of banning
   is that the user is disconnected.  */
{
  int user_no;
  char msg[MAXLEN], site[MAXLEN];

  if (users[posn].status != SU)
    dputs(users[posn].sockfd, "You do not have banning rights");
  else {
    if ((user_no = get_uno(users, posn, args)) != -1) {
      get_site( &(users[user_no].addr), site);
      add_to_ban(site);
      dputs(users[user_no].sockfd, 
                "The Super-User has banned you from Chat... Goodbye");
      sprintf(msg, "%s has been banned", users[user_no].name);
      dputs(users[posn].sockfd, msg);
      remove_user(users, user_no, rooms);
    }
  }
}


void add_to_ban(char *site)
/* Add site to the BANNED file */
{
  FILE *fp;

  if ((fp = fopen(BANNED, "a")) == NULL)
    fprintf(stderr, "Could not append to %s\n", BANNED);
  else {
    fprintf(fp, "%s\n", site);
    fclose(fp);
  }
}


void talk(User users[], int posn, char *args, Room rooms[])
/* General talk (i.e. an input line that does not start with a '.'). 
   A public message is sent to everyone else in the room
   prefixed with the users name, and is also stored in the
   recent messages queue for that room. 
   A private message is only sent to the other person, and is not 
   stored in the recents queue.
*/
{
  char msg[MAXLEN];
  int i, rloc;

  /* record public and private (!) talk in the log file */
  sprintf(msg, "talk: %s: %s\n", users[posn].name, args);
  fputs(msg, stdout);
  write_log(msg);

  rloc = users[posn].room_loc;
  sprintf(msg, "%s: %s", users[posn].name, args);
  
  if (users[posn].private_contact == -1) {   /* public message */
    for (i=0; i < MAX_USERS; i++)
      if ((rooms[rloc].curr_members[i]) && (i != posn)) 
        dputs(users[i].sockfd, msg);
    add_msg(&(rooms[rloc].talk), msg);
  }
  else {              /* private message */
    i = users[posn].private_contact;
    if (users[i].private_contact == posn)   /* both are private */
      dputs(users[i].sockfd, msg);
    else {
      sprintf(msg, "Sorry, but %s is not in private mode with you",
                           users[i].name);
      dputs(users[posn].sockfd, msg);
    }
  }
}


void add_msg(Messages *t, char *msg)
/* t points to a circular queue of MAX_MSGS elements */
{
  if (t->size == MAX_MSGS) {   /* full; so delete first (oldest) message */
    free(t->msg[t->first]);
    t->msg[t->first] = NULL;
    t->first = (t->first + 1) % MAX_MSGS;
    (t->size)--;
  }

  t->msg[t->last] =             /* add msg to the end of the queue */
    (char *) malloc((strlen(msg)+1)*sizeof(char));
  strcpy(t->msg[t->last], msg);
  t->last = (t->last + 1) % MAX_MSGS;
  t->size++;
}



void check_activity(User users[], int posn, Room rooms[])
/* Check to see how inactive a user has been by comparing
   their act_time field in users[] with the current time.
   If they have been inactive for more than CLOSE_TIME seconds
   then give them a warning, and make their act_time REPRIEVE_TIME
   seconds later. The next time that they are inactive for more
   than CLOSE_TIME seconds, they are disconnected.

   The effectivness of this code is reduced by the way that the Chat server
   suspends waiting for input. This means that a user may be 
   able to stay connected for quite some time if there has been no
   other user input, and so no chance to call check_activity().
*/
{
  char msg[MAXLEN];
  double inactive_time;

  if (users[posn].status != SU) {           /* never check super-user */
    inactive_time = difftime(time(NULL), users[posn].act_time);
    if (inactive_time > CLOSE_TIME) {       /* inactive too long */
      if (!users[posn].warned) {            /* not warned yet */
        sprintf(msg, "Chat WARNING: You have been inactive for over %d mins, do something NOW!", CLOSE_TIME/60);
        dputs(users[posn].sockfd, msg);
        users[posn].warned = 1;
        users[posn].act_time = users[posn].act_time + REPRIEVE_TIME;
      }
      else {    /* warned before */
        dputs(users[posn].sockfd, "Chat Message: No activity means... Goodbye");
        remove_user(users, posn, rooms);
      }
    }
  }
}


/* socket I/O */

int dputs(int sfd, char *msg)
/* Write msg to the sfd socket. Return the number of characters
   actually written, although this feature is not used in the
   rest of the Chat code. If no characters were written then 
   there is something wrong with the socket link.
*/
{
  int i;

  i = write(sfd, msg, strlen(msg));
  write(sfd, "\n", 1);

  if (i < 0)
    fprintf(stderr, "Write error to socket %d for msg:\"%s\"\n", sfd, msg);
  return (i >= 0) ? (i+1) : i;
}


int dgets(int sfd, char msg[])
/* Read at most MAXLEN characters from the sfd socket.
   read() returns the number of characters actually read,
   which is used to detect some errors. 
   The input is read into buf[] which is copied into msg[]
   with any non-printable characters removed (e.g. control
   characters). This may mean that msg[] is empty. The length
   of msg[] is returned.
*/
{
  char buf[MAXLEN]; 
  int i, len = 0, plen = 0;

  len = read(sfd, buf, MAXLEN);
  if (len == 0) {
    fprintf(stderr, "Socket %d has unexpectedly closed\n", sfd);
    return -1;
  }
  if (len == -1) { 
    fprintf(stderr, "Socket %d read error\n", sfd);
    return -1;
  }

  for (i = 0; i < len; i++)     /* get rid of any nasty chars */
    if (isprint(buf[i]))
      msg[plen++] = buf[i];
  msg[plen] = '\0'; 

  return plen;        /* length of printable characters in msg */
}



/* for logging */
/* Currently this is used to log three forms of activity:
    1. Users entering Chat
    2. Users leaving Chat
    3. General talk (both public and private)
*/

void write_log(char *msg)
/* Append msg to the end of the LOG file */
{
  FILE *fp;

  if ((fp = fopen(LOG, "a")) == NULL)
    fprintf(stderr, "Could not append to %s\n", LOG);
  else {
    fputs(msg, fp);
    fclose(fp);
  }
}



