diff options
| -rwxr-xr-x | compojure-3.2/src/compojure/http/middleware.clj | 2 | ||||
| -rw-r--r--[-rwxr-xr-x] | lib/compojure-3.2v1.jar | bin | 401005 -> 401074 bytes | |||
| -rwxr-xr-x | src/cookie_login.clj | 76 | ||||
| -rwxr-xr-x | src/site.clj | 81 | ||||
| -rwxr-xr-x | static/js/pichat.js | 61 | ||||
| -rw-r--r-- | static/tests/scrolling.html | 128 | ||||
| -rw-r--r-- | static/webcam/webcam.js | 16 |
7 files changed, 253 insertions, 111 deletions
diff --git a/compojure-3.2/src/compojure/http/middleware.clj b/compojure-3.2/src/compojure/http/middleware.clj index f9a2dab..7c68bab 100755 --- a/compojure-3.2/src/compojure/http/middleware.clj +++ b/compojure-3.2/src/compojure/http/middleware.clj @@ -114,7 +114,7 @@ (let [default (or (:default options) "text/html")] (if-let [ext (extension (:uri request))] (let [mimetypes (or (:mimetypes options) default-mimetypes)] - (get mimetypes ext default)) + (get mimetypes (.toLowerCase ext) default)) default))) (defn with-mimetypes diff --git a/lib/compojure-3.2v1.jar b/lib/compojure-3.2v1.jar Binary files differindex c41f5e9..bc19fe3 100755..100644 --- a/lib/compojure-3.2v1.jar +++ b/lib/compojure-3.2v1.jar diff --git a/src/cookie_login.clj b/src/cookie_login.clj index 9e501f4..e507876 100755 --- a/src/cookie_login.clj +++ b/src/cookie_login.clj @@ -1,23 +1,28 @@ (ns cookie-login (:use compojure)) -(defn clear-login-token [token-key] +(def *login-token-key* :login-token) +(def *login-token-expiry* (* 1000 60 60 24 7)) ; one week + +(defn clear-login-token "Creates an expiration cookie for a given cookie name." + [token-key] (set-cookie token-key "dummy" :expires "Thu, 01-Jan-1970 00:00:01 GMT")) + (defn handle-request-with-login-token "Validates login token, handles request, and updates cookies and session - repository. If token is invalid or an exception is raised while reading it, - the token cookie is expired." - [handler request expiry token-key token-maker token-reader] - (if-let [session-info (token-reader (get-in request [:cookies token-key]))] + repository. If the token is invalid, the token cookie is expired." + [handler request token-maker token-reader login-token-key login-token-expiry] + (if-let [session-info (token-reader (get-in request + [:cookies login-token-key]))] (let [response (handler (merge-with merge request {:session session-info})) ; Session variable priority: ; 1) variables set by handler - ; 2) session variables from token-reader + ; 2) variables from token-reader ; 3) variables from repository session-map (merge (request :session) session-info @@ -25,50 +30,45 @@ (merge-with merge response {:session session-map} - (token-maker session-info expiry))) + (token-maker session-info))) (merge (handler request) - (clear-login-token token-key)))) - -; Default expiration is a week. -(def *default-login-token-expiry* (* 1000 60 60 24 7)) -(def *default-login-token-key* :login-token) + (clear-login-token login-token-key)))) (defn with-cookie-login "Middleware to support automatic cookie login. Must be placed after - the with-session middleware! + the with-session middleware. - Accepts five configuration options: - - token-key: - The cookie name to store the login-token under. - Defaults to 'login-token'. - - expiry: - The number of milliseconds a login token is valid for. - Defaults to one week. - - is-logged-in?: - Function to apply to request's session map to determine whether to - process login token or not. If a truthy value is returned, +Must be given three arguments: + - process-login-token? + Function to apply to request map to determine whether to + process login token or not. If a false value is returned, then the next handler is called without further processing. - token-maker: - Function to generate new login token from session map and - milliseconds until login token expiry. + Function to generate new login token from session map. - token-reader: Function to generate session map from login token. Should return nil if login token is invalid. -" - [handler options] - (let [token-key (or (options :default-token-key) *default-login-token-key*) - expiry (or (options :expiry) *default-login-token-expiry*) - is-logged-in? (options :is-logged-in?) - token-maker (options :token-maker) - token-reader (options :token-reader)] + + The following variables can be rebound: + - *login-token-key* + The cookie name to store the login-token under. + Defaults to 'login-token'. + + - *login-token-expiry* + The number of milliseconds a login token is valid for. + Defaults to one week. +" + [handler process-login-token? token-maker token-reader] + (let [login-token-key *login-token-key* + login-token-expiry *login-token-expiry*] (fn [request] - (if (or (is-logged-in? (request :session)) - (not (get-in request [:cookies token-key]))) - (handler request) + (if (and (get-in request [:cookies login-token-key]) + (process-login-token? request)) (handle-request-with-login-token handler request - expiry - token-key token-maker - token-reader)))))
\ No newline at end of file + token-reader + login-token-key + login-token-expiry) + (handler request))))) diff --git a/src/site.clj b/src/site.clj index b1774fd..e0a40b9 100755 --- a/src/site.clj +++ b/src/site.clj @@ -7,13 +7,18 @@ org.apache.commons.codec.digest.DigestUtils javax.servlet.http.Cookie org.antlr.stringtemplate.StringTemplateGroup) - (:use clojure.contrib.str-utils + (:use clojure.xml + clojure.contrib.str-utils clojure.contrib.duck-streams clojure.contrib.sql compojure cookie-login utils)) +(def *run-flusher* true) +(def *flusher-sleep-ms* 4000) +(def *user-timeout-ms* 15000) + (def template-group (new StringTemplateGroup "dumpfm" "template")) (.setRefreshInterval template-group 3) @@ -25,24 +30,19 @@ (System/currentTimeMillis))) (def rooms (ref {})) - -(def run-flusher true) -(def flusher-sleep-ms 4000) -(def user-timeout-ms 15000) - (def flusher (agent nil)) (defn flush! [x] - (when run-flusher + (when *run-flusher* (send-off *agent* #'flush!)) (doseq [[rid room] @rooms] (dosync (let [users (room :users) now (System/currentTimeMillis) - alive? (fn [[n u]] (> (u :last-seen) (- now user-timeout-ms)))] + alive? (fn [[n u]] (> (u :last-seen) (- now *user-timeout-ms*)))] (ref-set users (into {} (filter alive? @users)))))) - (. Thread (sleep flusher-sleep-ms)) + (. Thread (sleep *flusher-sleep-ms*)) x) ;; Configuration @@ -59,6 +59,9 @@ ;; Utils +(defn ms-in-future [ms] + (+ ms (System/currentTimeMillis))) + (defn swap [f] (fn [& more] (apply f (reverse more)))) @@ -141,7 +144,7 @@ "messages" (map process-message-for-json (new-messages room since))}) -(def dumps-per-page 20) +(def *dumps-per-page* 20) (defn maybe-parse-int [s f] (if s (Integer/parseInt s) f)) @@ -164,7 +167,7 @@ "WHERE room_id = ? AND m.user_id = u.user_id " (if image-only "AND m.is_image = true " "") "ORDER BY created_on DESC " - "LIMIT " dumps-per-page " OFFSET ?")] + "LIMIT " *dumps-per-page* " OFFSET ?")] (do-select [query room-id offset])))) (defn count-messages-by-nick [nick image-only] @@ -184,9 +187,18 @@ "AND r.room_id = m.room_id AND r.admin_only = false " (if image-only "AND m.is_image = true " "") "ORDER BY created_on DESC " - "LIMIT " dumps-per-page " OFFSET ?")] + "LIMIT " *dumps-per-page* " OFFSET ?")] (do-select [query nick 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))}) + ;; Templates (defn fetch-template [template-name session] @@ -214,10 +226,10 @@ ;; login-token functions -(defn is-logged-in? +(defn logged-in? "Test whether user is logged in by presence of nick key in session." - [session] - (contains? session :nick)) + [request] + (contains? (request :session) :nick)) (defn encode-login-token [nick hash expiry] (let [token-hash (sha1-hash hash expiry)] @@ -239,11 +251,11 @@ db-info))))) (defn make-login-token - [{nick :nick hash :hash} expiry] - (let [expiration (+ (System/currentTimeMillis) expiry)] - (set-cookie *default-login-token-key* (encode-login-token nick - hash - expiration) + [{nick :nick hash :hash}] + (let [expiration (ms-in-future *login-token-expiry*)] + (set-cookie *login-token-key* (encode-login-token nick + hash + expiration) :expires (gmt-string (new Date expiration))))) ;; Landing @@ -259,8 +271,8 @@ db-user (authorize-nick-hash nick hash) remember-me (= (params :rememberme) "yes") login-cookie (if remember-me - (make-login-token db-user *default-login-token-expiry*) - (clear-login-token *default-login-token-key*))] + (make-login-token db-user *login-token-expiry*) + (clear-login-token *login-token-key*))] (if db-user [(session-assoc-from-db db-user) login-cookie @@ -269,7 +281,7 @@ (defn logout [session] [(session-dissoc :nick :user_id :is_admin :avatar) - (clear-login-token *default-login-token-key*) + (clear-login-token *login-token-key*) (redirect-to "/")]) ;; Registration @@ -298,7 +310,7 @@ is-home (and nick (= nick profile-nick)) has-avatar (non-empty-string? (user-info :avatar)) offset (maybe-parse-int offset 0) - dump-offset (* offset dumps-per-page) + dump-offset (* offset *dumps-per-page*) dumps (fetch-messages-by-nick profile-nick true dump-offset) dump-count (count-messages-by-nick profile-nick true) st (fetch-template "profile" session)] @@ -310,7 +322,7 @@ (if (non-empty-string? v) (escape-html v))))) (.setAttribute st "dumps" (to-array (map process-message-for-output dumps))) - (if (< (+ dump-offset dumps-per-page) dump-count) + (if (< (+ dump-offset *dumps-per-page*) dump-count) (.setAttribute st "next" (inc offset))) (if (not= offset 0) (.setAttribute st "prev" (max (dec offset) 0))) @@ -462,13 +474,13 @@ (defn log [session room offset params] (let [st (fetch-template "log" session) offset (maybe-parse-int offset 0) - dump-offset (* offset dumps-per-page) + dump-offset (* offset *dumps-per-page*) image-only (and (not (room :admin_only)) (not= (params :show) "all")) dumps (to-array (map process-message-for-output (fetch-messages-by-room (room :room_id) image-only dump-offset))) dump-count (count-messages-by-room (room :room_id) image-only)] - (if (< (+ dump-offset dumps-per-page) dump-count) + (if (< (+ dump-offset *dumps-per-page*) dump-count) (.setAttribute st "next" (inc offset))) (if (not= offset 0) (.setAttribute st "prev" (max (dec offset) 0))) @@ -585,15 +597,12 @@ "zip" "application/zip"}) (decorate static - (with-mimetypes)) + (with-mimetypes {:mimetypes mimetypes})) (decorate pichat - (with-cookie-login {:is-logged-in? is-logged-in? - :token-maker make-login-token - :token-reader read-login-token}) (with-mimetypes {:mimetypes mimetypes}) + (with-cookie-login (comp not logged-in?) make-login-token read-login-token) (with-session {:type :memory, :expires (* 60 60)})) - (decorate multipart (with-mimetypes {:mimetypes mimetypes}) @@ -605,13 +614,7 @@ (dosync (doseq [room-db (fetch-rooms)] (alter rooms assoc (room-db :key) - {: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))}))) + (build-room-map-from-db room-db)))) (run-server {:port 8080} "/static/*" (servlet static) diff --git a/static/js/pichat.js b/static/js/pichat.js index 6b9c3e8..33788e2 100755 --- a/static/js/pichat.js +++ b/static/js/pichat.js @@ -1,24 +1,44 @@ var cache = {} var pendingMessages = {} +var MaxImagePosts = 40 + function escapeHtml(txt) { - if (!txt) { return ""; } - else { return $("<span>").text(txt).html(); } + if (!txt) { return ""; } + else { return $("<span>").text(txt).html(); } } function linkify(text) { - var URLRegex = /((\b(http\:\/\/|https\:\/\/|ftp\:\/\/)|(www\.))+(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi; - return text.replace(URLRegex, linkReplace); + LastMsgContainsImage = false + var URLRegex = /((\b(http\:\/\/|https\:\/\/|ftp\:\/\/)|(www\.))+(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi; + return text.replace(URLRegex, linkReplace); } +// durty hack to use a global to check this... but otherwise i'd have to rewrite the String.replace function? :/ +var LastMsgContainsImage = false function linkReplace(match){ - var PicRegex = /\.(jpg|jpeg|png|gif|bmp)$/i; - var matchWithoutParams = match.replace(/\?.*$/i, "") - if (PicRegex.test(matchWithoutParams)){ - return "<a target='_blank' href='" + match + "'><img src='" + match + "'></a>" - } else { - return "<a target='_blank' href='" + match + "'>" + match + "</a>" - } + var PicRegex = /\.(jpg|jpeg|png|gif|bmp)$/i; + var matchWithoutParams = match.replace(/\?.*$/i, "") + if (PicRegex.test(matchWithoutParams)){ + LastMsgContainsImage = true + return "<a target='_blank' href='" + match + "'><img src='" + match + "'></a>" + } else { + return "<a target='_blank' href='" + match + "'>" + match + "</a>" + } +} + +var ImageMsgCount = 0 +function removeOldMessages(){ + // don't count posts that are all text + if (LastMsgContainsImage) ImageMsgCount += 1; + while (ImageMsgCount > MaxImagePosts) { + var imgMsg = $(".contains-image:first") + if (imgMsg.length) { + imgMsg.prevAll().remove() // remove all text messages before the image message + imgMsg.remove() + } else break; + ImageMsgCount -= 1; + } } function buildMsgContent(content) { @@ -26,13 +46,15 @@ function buildMsgContent(content) { } function buildMessageDiv(msg, isLoading) { - var nick = escapeHtml(msg.nick); - var msgId = !isLoading ? 'id="message-' + msg.msg_id + '"' : ''; - var loadingClass = isLoading ? ' loading' : ''; - return '<div class="msgDiv ' + loadingClass + '" ' + msgId + '>' - + '<b><a href="/u/' + nick + ' ">' + nick + '</a>: </b>' - + buildMsgContent(msg.content) - + '</div>'; + removeOldMessages() + var nick = escapeHtml(msg.nick); + var msgId = !isLoading ? 'id="message-' + msg.msg_id + '"' : ''; + var loadingClass = isLoading ? ' loading' : ''; + var containsImageClass = LastMsgContainsImage ? ' contains-image' : ''; + return '<div class="msgDiv ' + loadingClass + containsImageClass + '" ' + msgId + '>' + + '<b><a href="/u/' + nick + ' ">' + nick + '</a>: </b>' + + buildMsgContent(msg.content) + + '</div>'; } function buildUserDiv(user) { @@ -199,7 +221,6 @@ function initChat() { // see /static/webcam/webcam.js if ('webcam' in window) webcam.init() - setTimeout(refresh, 1000); } @@ -279,7 +300,7 @@ function isScrolledToBottom(){ } function scrollIfPossible(){ - if (lastScriptedScrolledPosition == messageList.scrollTop || isScrolledToBottom()) + if (lastScriptedScrolledPosition <= messageList.scrollTop || isScrolledToBottom()) scrollToEnd() } diff --git a/static/tests/scrolling.html b/static/tests/scrolling.html new file mode 100644 index 0000000..00e7b89 --- /dev/null +++ b/static/tests/scrolling.html @@ -0,0 +1,128 @@ +<html> +<head> +<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script> +<script> + +var imageQueue = [] + +function fetch(){ + $.ajax({ + "url": "http://pipes.yahoo.com/pipes/pipe.run?_id=59b7cad3b8fb595fa7cb3c2fdf0ab328&_render=json&_callback=fetched", + "dataType": "jsonp" + }) + log("fetching more images") +} + +function fetched(data){ + log("images fetched") + var imageUrls = [] + try { + var images = data['value']['items'][0]['recent-images']['recent-image'] + for(var i = 0; i < images.length; i++) + imageQueue.push(images[i]['img']) + } catch(e) { + console.log("couldn't parse object:") + console.log(data) + } +} + +function log(m){ + $("#log").html(m) +} + +function go(){ + messagePane = $("#chat")[0] + maxImages = $("#max-images")[0] + imagePoster() + scrollToEnd() + scrollWatcher() +} + +function imagePoster(){ + if (!imageQueue.length){ + log("queue empty") + } else if (Paused) { + log("paused") + } else { + log(imageQueue.length + " images in queue ... posting image") + var image = imageQueue.shift() + imagePost(image) + } + setTimeout(imagePoster, 500) +} + +imagePosts = 0 +function imagePost(image){ + imagePosts += 1 + while (imagePosts > maxImages.value) { + var imgs = $(".image-post:first") + if (imgs.length) { + imgs.remove() + } else { + break + } + imagePosts -= 1 + } + var i = $("<img/>").attr("src", image) //.load(scrollIfPossible).error(scrollIfPossible) + var d = $("<div class='image-post'/>").html("username: ") + d.append(i).appendTo("#chat") +} + +var Paused = false +function pausego(){ + Paused = !Paused + $("#pausego-button").html(Paused ? "go" : "pause") +} + +function isScrolledToBottom(){ + var threshold = 50; + + var containerHeight = messagePane.style.pixelHeight || messagePane.offsetHeight + var currentHeight = (messagePane.scrollHeight > 0) ? messagePane.scrollHeight : 0 + + var result = (currentHeight - messagePane.scrollTop - containerHeight < threshold); + + return result; +} + +function scrollIfPossible(){ + if (lastScriptedScrolledPosition <= messagePane.scrollTop || isScrolledToBottom()) + scrollToEnd() +} + +var lastScriptedScrolledPosition = 0 +function scrollToEnd(){ + messagePane.scrollTop = messagePane.scrollHeight + lastScriptedScrolledPosition = messagePane.scrollTop +} + +function scrollWatcher(){ + scrollIfPossible() + setTimeout(scrollWatcher, 500) +} + +</script> +<style> + #chat { width: 500px; height: 90%; overflow: scroll; } + img { max-height: 300px; } +</style> +</head> +<body> + <div id="chat"> + test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> + test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> + test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> + test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> + test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> + test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> + </div> + <p id="log"></p> + <button onclick="fetch()">add images</button> + <button onclick="pausego()" id="pausego-button">pause</button> + <br /> + max image posts: <input name="max-images" id="max-images" value="50" /> +</body> +<script> + go() +</script> +</html>
\ No newline at end of file diff --git a/static/webcam/webcam.js b/static/webcam/webcam.js index 9829fd7..f5a5536 100644 --- a/static/webcam/webcam.js +++ b/static/webcam/webcam.js @@ -1,21 +1,11 @@ /* JPEGCam v1.0.8 */ /* Webcam library for capturing JPEG images and submitting to a server */ -/* Copyright (c) 2008 - 2009 Joseph Huckaby <jhuckaby@goldcartridge.com> */ +/* Copyright (c) 2008 - 2009 + Joseph Huckaby <jhuckaby@goldcartridge.com> + AND TIMB, ESQ. <http://bon.gs> */ /* Licensed under the GNU Lesser Public License */ /* http://www.gnu.org/licenses/lgpl.html */ -/* Usage: - <script language="JavaScript"> - document.write( webcam.get_html(320, 240) ); - webcam.set_api_url( 'test.php' ); - webcam.set_hook( 'onComplete', 'my_callback_function' ); - function my_callback_function(response) { - alert("Success! PHP returned: " + response); - } - </script> - <a href="javascript:void(webcam.snap())">Take Snapshot</a> -*/ - // Everything is under a 'webcam' Namespace window.webcam = { version: '1.0.8', |
