diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8138c2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.nrepl-port diff --git a/src/chat_server/core.clj b/src/chat_server/core.clj index 3031c2a..e3b453d 100644 --- a/src/chat_server/core.clj +++ b/src/chat_server/core.clj @@ -1,41 +1,95 @@ (ns chat-server.core (:require [clj-sockets.core :as socket] - [chat-server.repl :as repl]) + [chat-server.repl :as repl] + [clojure.string :as string]) (:gen-class)) -(def clients (ref {})) +(def clients (ref [])) +(defn write-line + "Write a single message to a client, returning the client." + [client message] + (socket/write-line (:socket client) message) + client) -(defn serve-client - [nick client] - (doseq [line (socket/read-lines client)] - (println "got message from user:" line) - (let [other-clients (vals (dissoc @clients nick))] - (case - (first (clojure.string/split line #" ")) - ;; Instructor Note: explain why a doall is needed here - "MSG" (doall (map #(socket/write-line % (str nick ": " (subs line 4))) other-clients)) +(defn error + "Write an error to a socket, returning nil." + [socket message] + (socket/write-line socket (str "ERROR: " message))) - ;;TODO: Process other kinds of command here +(defn nick-exists? + "Return truthy if a nick is already in use, otherwise nil." + [nick] + (let [nicks (map #(-> % deref :nick) @clients)] + (some #{nick} nicks))) - ;; else - (socket/write-line client "ERROR: I don't understand"))))) +(defn set-nick + "Set the nick of an existing client. This function is transactional." + [client nick] + (dosync + (if (nick-exists? nick) + (error (:socket @client) "nick already exists") + (send-off client assoc :nick nick)))) -(defn new-client +(defn send-message + "Send a message to all clients but the originating one." + [client message] + (doseq [d-client @clients] + (when-not (= client d-client) + (send-off d-client write-line (str (:nick @client) ": " message))))) + +(defn terminate-client! + "Close the socket and remove the client from the list." + [client] + (try + (socket/close-socket (:socket @client)) + (dosync + (alter clients (partial remove #{client}))) + (catch Throwable e + (println (.getMessage e))))) + +(defn listen-client + "Listen for and dispatch incoming messages from a client." [client] - (let [command (socket/read-line client)] - (println "got message:" command) - (if-let [[_ nick] (re-matches #"USER (.*)" command)] - ;; Instructor note: explain why we have to use a transaction here to make sure checking if user exists and adding them happens atomically - (if (dosync - (when-not (get @clients nick) - (alter clients assoc nick client))) - - (serve-client nick client) - - (do - (socket/write-line client "ERROR: Nick already taken") - (socket/close-socket client)))))) + (let [{:keys [socket nick channels] :or {channels []}} @client] + (loop [line (socket/read-line socket)] + (let [[command & words] (string/split line #" ")] + (case command + "USER" (set-nick client (string/join "-" words)) + "MSG" (send-message client (string/join " " words)) + "QUIT" (terminate-client! client) + (error socket "I don't understand"))) + (when-not (.isClosed socket) + (recur (socket/read-line socket)))))) + +(defn handle-client-error + "Handle a client error." + [the-agent exception] + (let [s (:socket @the-agent) + msg (.getMessage exception)] + (when-not (.isClosed s) + (error s msg)) + (println msg) + (terminate-client! the-agent))) + +(defn new-client + "Takes a freshly opened socket connection, creates a new client and + calls the dispatcher." + [s] + (loop [line (socket/read-line s)] + (if-let [[_ nick] (re-matches #"USER (.*)" line)] + (if-let [client (dosync + (when-not (nick-exists? nick) + (let [client (agent {:socket s :nick nick :channels []} + :error-mode :continue + :error-handler handle-client-error)] + (alter clients conj client) + client)))] + (listen-client client) + (do (error s "nick is already taken, try another") + (recur (socket/read-line s)))) + (do (error s "first set a nick with USER") + (recur (socket/read-line s)))))) (defn -main "The hello world of chat servers"