(ns rooms (:import java.util.Date) (:use clojure.contrib.str-utils utils user)) (defstruct message-struct :nick :content :created_on :msg_id) (def *run-flusher* true) (def *flusher-sleep* (seconds 4)) (def *user-timeout* (seconds 15)) (def rooms (ref {})) (def flusher (agent nil)) (defn flush-inactive-users! [x] (doseq [[rid room] @rooms] (dosync (let [users (room :users) now (System/currentTimeMillis) cutoff (- now *user-timeout*) alive? (fn [[n u]] (> (u :last-seen) cutoff))] (ref-set users (into {} (filter alive? (ensure users))))))) (Thread/sleep *flusher-sleep*) (when *run-flusher* (send *agent* #'flush-inactive-users!)) x) (defn start-user-flusher! [] (send flusher flush-inactive-users!)) (def *default-room* "dumpfm") (defn default-room? [key] (= (lower-case key) *default-room*)) (defn lookup-room [key] (and key (@rooms (lower-case key)))) (defn fetch-room [key] (first (do-select ["SELECT * FROM rooms WHERE key = LOWER(?) AND active" key]))) (defn fetch-rooms [] (do-select ["SELECT * FROM ROOMS WHERE active"])) (defn lookup-room-key [room-id] (or (some #(and (= (:room_id %) room-id) (:room_key %)) (vals @rooms)) (:key (first (do-select ["SELECT key FROM rooms where room_id = ?" room-id]))))) (defn count-messages-by-room [room-id image-only] (let [query (str "SELECT COUNT(*) FROM messages m, users u WHERE room_id = ? AND m.user_id = u.user_id" (if image-only " AND m.is_image = true " ""))] (do-count [query room-id]))) (defn fetch-messages-by-room ([room-id image-only] (fetch-messages-by-room room-id image-only 0)) ([room-id image-only offset] (let [query (str "SELECT m.content, m.message_id, m.created_on, u.nick, u.avatar FROM users u, messages m WHERE room_id = ? AND m.user_id = u.user_id " (if image-only "AND m.is_image = true " "") "ORDER BY created_on DESC LIMIT ? OFFSET ?")] (do-select [query room-id *dumps-per-page* offset])))) (defn build-room-map-from-db [room-db] {:admin_only (room-db :admin_only) :room_id (room-db :room_id) :key (room-db :key) :name (room-db :name) :description (room-db :description) :users (ref {}) :messages (ref (fetch-messages-by-room (room-db :room_id) false)) :topic (ref nil) }) (defn load-rooms! [] (dosync (doseq [room-db (fetch-rooms)] (alter rooms assoc (lower-case (room-db :key)) (build-room-map-from-db room-db))))) ;; Room helpers (defn login-user [user room] (alter (room :users) assoc (user :nick) user)) ; Note: To ensure that the msg's timestamp is consistent ; with other msg creations, build-msg should be used ; within a dosync. (defn build-msg [nick content msg-id] (struct message-struct nick content (new Date) msg-id)) (def message-count-limit 200) (defn add-message [msg room] (insert-and-truncate! (room :messages) msg message-count-limit)) (defn insert-message-into-db! [user-id room-id content is-image] (:message_id (first (do-select ["INSERT INTO messages (user_id, room_id, content, is_image) VALUES (?, ?, ?, ?) RETURNING message_id" user-id room-id content is-image])))) (defn create-and-add-room! [key] (do-select ["INSERT INTO rooms (key, name, description) VALUES (?, ?, ?) RETURNING room_id" key key key]) (if-let [room-db (fetch-room key)] (dosync (alter rooms assoc (lower-case key) (build-room-map-from-db room-db)) room-db))) ; TODO: cache (defn get-or-create-room! [key] (:room_id (or (first (do-select ["SELECT room_id FROM rooms WHERE lower(key) = ?" (lower-case key)])) (create-and-add-room! key) (throw (Exception. (str "Unable to create room " key)))))) (defn- fetch-or-create-bot-id! [nick] (:user_id (or (fetch-nick nick) (first (do-select ["INSERT INTO users (nick, hash, email) VALUES (?, ?, ?) RETURNING user_id" nick "GARBAGE" "info@dump.fm"]))))) (def room-bot-id-cache (ref {})) (defn get-or-create-room-bot! [room-key] (let [nick (str "~" room-key)] (or (get @room-bot-id-cache nick) (let [id (fetch-or-create-bot-id! nick) r [nick id]] (dosync (commute room-bot-id-cache assoc nick r)) r))))