A little chat server

I (FW) created a little (~4k) chatserver in Tcl, which allows people to connect via a telnet client, or some other raw text-based program, and chat with others. People are assigned a unique name (the word "Unnamed" followed by an integer) and may subsequently rename themselves to their liking. Features included are a couple of different methods of communicating - the "say" command, which just sends a standard message, and the "me" command, which is roughly equivilant to the /me command in IRC (the one character shortcuts '"' and ':' are available for these, respectively); the "who" command, which lists the people online; the "name" command, which lets you reset your name; and the "help" command, which simply spits out a summary of the aforementioned commands.

I fancy this code so lucid it's almost self-documenting (thank the language for that, not me) and it serves to show a lot of the elegance of Tcl.

Update: Fixed a small bug with the : shortcut to the "me" command.


You might want to have a look at http://www.dedasys.com/freesoftware/ where you can find tclchat, which has both web and tk interfaces.


 #!/bin/sh
 # the next line restarts using tclsh \
 exec tclsh "$0" ${1+"$@"}


 set port 1234

 proc handle_connection {client_socket host port} {
   global client_sockets counter name2socket socket2name

   set client_name "Unnamed$counter"
   incr counter

   announce "* $client_name has connected."

   lappend client_sockets $client_socket

   set name2socket($client_name) $client_socket
   set socket2name($client_socket) $client_name

   fconfigure $client_socket -buffering line
   fileevent $client_socket readable [list receive_line_from $client_socket]

   puts $client_socket "You are logged in as \"$client_name\".  Type \"help\" to see a rundown of commands."
 }

 proc receive_line_from {client_socket} {
   global client_sockets name2socket socket2name

   set client_name $socket2name($client_socket)

   if {[catch {gets $client_socket line} send_error]} {
     clean_up_client $client_socket
     announce "* $client_name has disconnected \[Error: $error\]."
     return
   } elseif {[eof $client_socket]} {
     clean_up_client $client_socket
     announce "* $client_name has disconnected \[Connection closed by client\]."
     return
   }

   if {$line == ""} {
     puts $client_socket "You must enter some command."
     return
   }

   set first_character [string index $line 0]
   set rest [string range $line 1 end]
   if {$first_character == "\""} {
     set command_name "say"
     set command_data $rest
   } elseif {$first_character == ":"} {
     set command_name "me"
     set command_data $rest
   } elseif {![regexp {^(.+?) (.*)} $line dummy command_name command_data]} {
     # If the above line fails to find a command with supplied data, then set the
     # command name to the whole string and the data to an empty string
     set command_name $line
     set command_data ""
   }


   switch -- $command_name {
     say {
       announce "$client_name says, \"$command_data\""
     }

     me {
       announce "$client_name $command_data"
     }

     who {
       puts $client_socket "The following people are online:"
       puts $client_socket "------------"
       foreach wsocket $client_sockets {
         puts $client_socket $socket2name($wsocket)
       }
       puts $client_socket "------------"
     }

     name {
       set new_name $command_data
       if {$new_name == $client_name} {
         puts $client_socket "You already are using that name."
       } elseif {[string is word $new_name] && [string length $new_name] <= 20} {
         foreach wsocket $client_sockets {
           if {$socket2name($wsocket) == $new_name} {
             puts $client_socket "That name is already in use."
             return
           }
         }

         set socket2name($client_socket) $new_name
         unset name2socket($client_name)
         set name2socket($new_name) $client_socket
         announce "* $client_name is now known as $new_name."
       } else {
         puts $client_socket "You must pick a name which is at most 20 characters long and which consists of only alphanumeric characters and underscores."
       }
     }

     help {
       puts $client_socket "Command rundown:"
       puts $client_socket "  say Hello (or) \"Hello"
       puts $client_socket "  me waves (or) :waves"
       puts $client_socket "  who"
       puts $client_socket "  name New_Name"
       puts $client_socket "  help"
     }

     default {
       puts $client_socket "Invalid command."
     }
   }
 }

 proc announce {message} {
   global client_sockets

   foreach client_socket $client_sockets {
     puts $client_socket $message
   }
 }

 proc clean_up_client {client_socket} {
   global name2socket socket2name client_sockets

   close $client_socket

   set pos [lsearch -exact $client_sockets $client_socket]
   set client_sockets [lreplace $client_sockets $pos $pos]

   unset name2socket($socket2name($client_socket)) socket2name($client_socket)
 }

 set client_sockets [list]
 array set name2socket [list]
 array set socket2name [list]

 set counter 1

 if {[catch {socket -server handle_connection $port} listen_error]} {
   puts "Failed to listen for connections on $port: $listen_error"
 } else {
   puts "Server started on port $port!"
   vwait forever
 }