Task

Write a server for a minimal text based chat.

People should be able to connect via ‘telnet’, sign on with a nickname, and type messages which will then be seen by all other connected users. Arrivals and departures of chat members should generate appropriate notification messages.




 
(ql:quickload '(:usocket :simple-actors :bordeaux-threads))
 
(defpackage :chat-server
  (:use :common-lisp :usocket :simple-actors :bordeaux-threads)
  (:export :accept-connections))
 
(in-package :chat-server)
 
(defvar *whitespace* '(#\Space #\Tab #\Page #\Vt #\Newline #\Return))
 
(defun send-message (users from-user message)
  (loop for (nil . actor) in users
     do (send actor :message from-user message)))
 
(defun socket-format (socket format-control &rest format-arguments)
  (apply #'format (socket-stream socket) format-control format-arguments)
  (finish-output (socket-stream socket)))
 
(defvar *log* *standard-output*)
 
(defmacro log-errors (&body body)
  `(handler-case
       (progn ,@body)
     (t (err)
       (format *log* "Error: ~a" err))))
 
(defparameter *user-manager*
  (let ((users nil))
    (actor (action &rest args)
       (format *log* "Handling message ~s~%" (cons action args))
       (ecase action
	 (:newuser
	  (destructuring-bind (username session-actor)
	      args
	    (cond ((assoc username users :test #'equalp)
		   (send session-actor :rejected
			 (format nil "Username ~a is already taken. Send /NICK new-nick with a valid name to enter the chat~%" username)))
		  ((equalp username "Server")
		   (send session-actor :rejected
			 (format nil "Server is not a valid username. Send /NICK new-nick with a valid name to enter the chat~%")))
		  (t (send-message users "Server" (format nil "~a has joined the chat." username))
		     (send session-actor :accepted
			   (format nil "Welcome to the Rosetta Code chat server in Common Lisp. ~a users connected.~%"
				   (length users)))
		     (pushnew (cons username session-actor)
			      users
			      :key #'car
			      :test #'equalp)))))
	 (:who
	  (destructuring-bind (username) args
	    (let ((actor (cdr (assoc username users :test #'equalp))))
	      (send actor :message "Server"
		    "Users connected right now:")
	      (loop for (user . nil) in users
		   do (send actor :message "Server" user)))))
	 (:message
	  (apply #'send-message users args))
	 (:dropuser
	  (destructuring-bind (username) args
	    (let ((user-actor (cdr (assoc username users :test #'equalp))))
	      (send user-actor :close)
	      (send user-actor 'stop))
	    (setf users (remove username users
				:key #'car
				:test #'equalp))
	    (send-message users "Server" (format nil "~a has left." username))))))))
 
(defmacro drop-connection-on-error (&body body)
  `(handler-case (progn ,@body)
     (t (err)
       (format *log* "Error: ~a; Closing connection" err)
       (send self :close)
       (send self 'stop)
       (send *user-manager* :dropuser username))))
 
(defun parse-command (message)
  (let* ((space-at (position #\Space message))
	 (after-space (and space-at
			   (position-if (lambda (ch)
				     (not (char= ch #\Space)))
					message :start (1+ space-at)))))
    (values (subseq message 0 space-at)
	    (and after-space
		 (string-trim *whitespace*
			      (subseq message after-space))))))
 
(defun help (socket)
  (socket-format socket "/QUIT to quit, /WHO to list users.~%"))
 
(defun make-user (username socket)
  (let* ((state :unregistered)
	 (actor 
	  (actor (message &rest args)
	    (drop-connection-on-error
	      (ecase message
		(:register
		 (send *user-manager* :newuser username self))
		(:accepted
		 (destructuring-bind (message) args
		   (write-string message (socket-stream socket))
		   (finish-output (socket-stream socket))
		   (setf state :registered)))
		(:rejected
		 (destructuring-bind (message) args
		   (write-string message (socket-stream socket))
		   (finish-output (socket-stream socket))
		   (setf state :unregistered)))
		(:user-typed
		 (destructuring-bind (message) args
		   (when (> (length message) 0)
		     (if (char= (aref message 0) #\/)
			 (multiple-value-bind (cmd arg)
			     (parse-command message)
			   (cond ((equalp cmd "/nick")
				  (ecase state
				    (:unregistered
				     (setf username arg)
				     (send *user-manager* :newuser username self))
				    (:registered
				     (socket-format socket
						    "Can't change your name after successfully registering~%"))))
				 ((equalp cmd "/help")
				  (help socket))
				 ((equalp cmd "/who")
				  (send *user-manager* :who username))
				 ((equalp cmd "/quit")
				  (socket-format socket
					  "Goodbye.~%")
				  (send *user-manager* :dropuser username))
				 (t
				  (socket-format socket
					  "Unknown command~%"))))
			 (send *user-manager* :message username message)))))
		(:message
		 (destructuring-bind (from-user message) args
		   (socket-format socket "<~a> ~a~%" from-user message)))
		(:close
		 (log-errors 
		   (close (socket-stream socket)))))))))
    (bt:make-thread (lambda ()
		      (handler-case
			  (loop for line = (read-line (socket-stream socket) nil :eof)
			     do (if (eq line :eof)
				    (send *user-manager* :dropuser username)
				    (send actor :user-typed (remove #\Return line))))
			(t () (send *user-manager* :dropuser username))))
 
		    :name "Reader thread")
    actor))
 
 
(defun initialize-user (socket)
  (bt:make-thread
   (lambda ()
     (format *log* "Handling new connection ~s" socket)
     (log-errors
       (loop do
	    (socket-format socket "Your name: ")
	    (let ((name (string-trim *whitespace* (read-line (socket-stream socket)))))
	      (format *log* "Registering user ~a" name)
	      (cond ((equalp name "Server")
		     (socket-format socket
				    "Server is not a valid username.~%"))
		    (t (send *user-manager*
			     :newuser name (make-user name socket))
		       (return)))))))
   :name "INITIALIZE-USER"))
 
 
(defun accept-connections ()
  (let ((accepting-socket (socket-listen "0.0.0.0" 7070)))
    (loop for new-connection = (socket-accept accepting-socket)
	 do (initialize-user new-connection))))
 
(make-thread #'accept-connections)
 