176 lines
5.7 KiB
Common Lisp
176 lines
5.7 KiB
Common Lisp
(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)
|