summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbin/repl.bat2
-rwxr-xr-xbin/repl.sh2
-rw-r--r--lib/commons-collections-3.2.1.jarbin0 -> 575389 bytes
-rw-r--r--scripts/mias3.py76
-rw-r--r--src/admin.clj18
-rw-r--r--src/cache_dot_clj/cache.clj241
-rw-r--r--src/multikey_cache.clj30
-rw-r--r--src/site.clj44
-rw-r--r--src/tags.clj6
-rw-r--r--src/user.clj93
-rwxr-xr-xsrc/utils.clj20
-rw-r--r--static/js/admin.js3
-rw-r--r--static/js/pichat.js35
-rw-r--r--template/messagepane.st2
-rw-r--r--template/rooms/chat.st6
-rw-r--r--template/rooms/mgmt.st10
16 files changed, 369 insertions, 219 deletions
diff --git a/bin/repl.bat b/bin/repl.bat
index 09b9edd..cc9a44b 100755
--- a/bin/repl.bat
+++ b/bin/repl.bat
@@ -1,3 +1,3 @@
REM Windows REPL script
SHIFT
-java -Xmx320m -server -cp lib/commons-io-1.4.jar;lib/commons-fileupload-1.2.1.jar;lib/commons-codec-1.3.jar;lib/commons-pool-1.5.5.jar;lib/clojure.jar;lib/clojure-contrib.jar;lib/compojure-3.2v3.jar;lib/jetty-6.1.24.jar;lib/jetty-util-6.1.24.jar;lib/servlet-api-2.5-6.1.14.jar;lib/jline-0.9.94.jar;lib/postgresql-8.4-701.jdbc4.jar;lib/stringtemplate-3.2.1.jar;lib/antlr-2.7.7.jar;lib/mail-1.4.4.jar;lib/jdom-1.1.jar;lib/rome-1.0.jar;lib/htmlcleaner-2.1.jar;lib/redis-clojure-1.0.4.jar;lib/jedis-1.4.0.jar;src/ jline.ConsoleRunner clojure.main -i %0 -r %* \ No newline at end of file
+java -Xmx320m -server -cp lib/commons-io-1.4.jar;lib/commons-fileupload-1.2.1.jar;lib/commons-codec-1.3.jar;lib/commons-collections-3.2.1.jar;lib/commons-pool-1.5.5.jar;lib/clojure.jar;lib/clojure-contrib.jar;lib/compojure-3.2v3.jar;lib/jetty-6.1.24.jar;lib/jetty-util-6.1.24.jar;lib/servlet-api-2.5-6.1.14.jar;lib/jline-0.9.94.jar;lib/postgresql-8.4-701.jdbc4.jar;lib/stringtemplate-3.2.1.jar;lib/antlr-2.7.7.jar;lib/mail-1.4.4.jar;lib/jdom-1.1.jar;lib/rome-1.0.jar;lib/htmlcleaner-2.1.jar;lib/redis-clojure-1.0.4.jar;lib/jedis-1.5.1.jar;src/ jline.ConsoleRunner clojure.main -i %0 -r %* \ No newline at end of file
diff --git a/bin/repl.sh b/bin/repl.sh
index 9edeaaa..ce16e1d 100755
--- a/bin/repl.sh
+++ b/bin/repl.sh
@@ -1,3 +1,3 @@
#!/bin/sh
-java -Xmx320m -server -cp .:lib/commons-io-1.4.jar:lib/commons-fileupload-1.2.1.jar:lib/commons-codec-1.3.jar:lib/commons-pool-1.5.5.jar:lib/jline-0.9.94.jar:lib/clojure.jar:lib/clojure-contrib.jar:lib/compojure-3.2v3.jar:lib/jetty-6.1.24.jar:lib/jetty-util-6.1.24.jar:lib/servlet-api-2.5-6.1.14.jar:lib/postgresql-8.4-701.jdbc4.jar:lib/stringtemplate-3.2.1.jar:lib/antlr-2.7.7.jar:lib/mail-1.4.4.jar:lib/jdom-1.1.jar:lib/rome-1.0.jar:lib/htmlcleaner-2.1.jar:lib/redis-clojure-1.0.4.jar:lib/jedis-1.4.0.jar:src/ jline.ConsoleRunner clojure.main -i $1 -r $@ \ No newline at end of file
+java -Xmx320m -server -cp .:lib/commons-io-1.4.jar:lib/commons-fileupload-1.2.1.jar:lib/commons-codec-1.3.jar:lib/commons-collections-3.2.1.jar:lib/commons-pool-1.5.5.jar:lib/jline-0.9.94.jar:lib/clojure.jar:lib/clojure-contrib.jar:lib/compojure-3.2v3.jar:lib/jetty-6.1.24.jar:lib/jetty-util-6.1.24.jar:lib/servlet-api-2.5-6.1.14.jar:lib/postgresql-8.4-701.jdbc4.jar:lib/stringtemplate-3.2.1.jar:lib/antlr-2.7.7.jar:lib/mail-1.4.4.jar:lib/jdom-1.1.jar:lib/rome-1.0.jar:lib/htmlcleaner-2.1.jar:lib/redis-clojure-1.0.4.jar:lib/jedis-1.4.0.jar:src/ jline.ConsoleRunner clojure.main -i $1 -r $@ \ No newline at end of file
diff --git a/lib/commons-collections-3.2.1.jar b/lib/commons-collections-3.2.1.jar
new file mode 100644
index 0000000..c35fa1f
--- /dev/null
+++ b/lib/commons-collections-3.2.1.jar
Binary files differ
diff --git a/scripts/mias3.py b/scripts/mias3.py
deleted file mode 100644
index 9a276d6..0000000
--- a/scripts/mias3.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import datetime
-import mimetypes
-import os
-import sys
-import time
-import S3
-
-CONN = None
-AWS_ACCESS_KEY_ID = 'AKIAJAQK4CDDP6I6SNVA'
-AWS_SECRET_ACCESS_KEY = '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'
-BUCKET_NAME = 'dumpfm'
-COUNTER = 0
-
-def retry_func(f, count):
- try:
- f()
- except:
- if count <= 1: raise
- else:
- print 'Error! retrying %s more time(s)' % (count - 1)
- retry_func(f, count - 1)
-
-def upload_file(path):
- global COUNTER
- path = os.path.normpath(path)
- if path == '.' or not os.path.isfile(path):
- return
- filedata = open(path, 'rb').read()
- size = os.path.getsize(path)
- content_type = mimetypes.guess_type(path)[0]
- if not content_type:
- content_type = 'text/plain'
-
- path = path.replace('\\', '/') # Windows hack
- start = time.time()
- def do_upload():
- CONN.put(BUCKET_NAME, path, S3.S3Object(filedata),
- {'x-amz-acl': 'public-read', 'Content-Type': 'video/x-flv'})
- retry_func(do_upload, 3)
-
- ms_took = (time.time() - start) * 1000
- print "uploaded %s (%0.0fms) (%sKB)" % (path, ms_took, size / 1024)
- COUNTER += 1
-
-def upload_directory(path):
- for f in sorted(os.listdir(path)):
- subpath = os.path.join(path, f)
- if os.path.isdir(subpath):
- upload_directory(subpath)
- else:
- upload_file(subpath)
-
-def do_upload(path):
- global CONN
- CONN = S3.AWSAuthConnection(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
-
- start = time.time()
-
- if os.path.isdir(path):
- upload_directory(path)
- else:
- upload_file(path)
-
- s_took = (time.time() - start)
- print "uploaded %s files in %0.0fs" % (COUNTER, s_took)
-
-
-if __name__ == "__main__":
- if len(sys.argv) == 1:
- print 'usage: s3upload.py path'
- sys.exit(1)
-
- args = sys.argv[1:]
- for path in args:
- do_upload(path)
- print
diff --git a/src/admin.clj b/src/admin.clj
index 9dbc65c..3593072 100644
--- a/src/admin.clj
+++ b/src/admin.clj
@@ -130,15 +130,15 @@ AND cancelled = false
(cond (not mute) (resp-error "INVALID_MUTE_ID")
(not active) (resp-error "EXPIRED_MUTE")
(not reason) (resp-error "NO_REASON")
- :else (assert-update
- (do-update :mutes [qry mute-id]
- {:cancelled true
- :cancel_admin_id admin-id
- :cancel_reason reason})
- (do
- (update! *active-mutes*)
- (resp-success "OK"))
- (resp-error "UPDATE_ERROR")))))
+ :else (if (assert-update
+ (do-update :mutes [qry mute-id]
+ {:cancelled true
+ :cancel_admin_id admin-id
+ :cancel_reason reason}))
+ (do
+ (update! *active-mutes*)
+ (resp-success "OK"))
+ (resp-error "UPDATE_ERROR")))))
(defn handle-cancel-mute! [session params]
(if-vip
diff --git a/src/cache_dot_clj/cache.clj b/src/cache_dot_clj/cache.clj
new file mode 100644
index 0000000..7dcfbfe
--- /dev/null
+++ b/src/cache_dot_clj/cache.clj
@@ -0,0 +1,241 @@
+;; Release 0.0.3
+;; https://github.com/alienscience/cache-dot-clj/tree/master/src
+
+(ns cache-dot-clj.cache "Resettable memoize")
+
+(declare naive-strategy)
+
+(defn- external-memoize
+ "Conventional memoize for use with external caching packages"
+ [f f-name strategy]
+ (let [{:keys [init lookup miss! invalidate!]} strategy
+ cache (init f-name)]
+ {:memoized
+ (fn [& args]
+ (let [[in-cache? res] (lookup cache args)]
+ (if in-cache?
+ res
+ (miss! cache args (apply f args)))))
+ :invalidate
+ (fn [args]
+ (invalidate! cache args))
+ }))
+
+;; TODO Move some of doc up a level
+(defn- internal-memoize
+ "Returns a map containing:
+
+ {:memoized fn ;; Memoized version of given function
+ :invalidate fn ;; Invalidate arguments in the cache
+ }
+
+ The memoized version of the function keeps a cache of the mapping from
+ arguments to results and, when calls with the same arguments are repeated
+ often, has higher performance at the expense of higher memory use.
+
+ The invalidation function takes a set of function arguments and causes
+ the appropriate cache entry to re-evaluate the memoized function.
+ Invalidation can be used to support the memoization of functions
+ that can be effected by external events.
+
+ Takes a cache strategy. The strategy is provided as a map
+ containing the following keys. All keys are mandatory!
+
+ - :init – the initial value for the cache and strategy state
+ - :cache – access function to access the cache
+ - :lookup – determines whether a value is in the cache or not
+ - :hit – a function called with the cache state and the argument
+ list in case of a cache hit
+ - :miss – a function called with the cache state, the argument list
+ and the computation result in case of a cache miss
+ - :invalidate - a function called with the cache state, the argument
+ list and the computation result that is used to
+ invalidate the cache entry for the computation.
+ "
+ [f _ strategy]
+ (let [{:keys [init cache lookup hit miss invalidate]} strategy
+ cache-state (atom init)
+ hit-or-miss (fn [state args]
+ (if (lookup state args)
+ (hit state args)
+ (miss state args (delay (apply f args)))))
+ mark-dirty (fn [state args]
+ (if (lookup state args)
+ (invalidate state args (delay (apply f args)))
+ state))]
+ {:memoized
+ (fn [& args]
+ (let [cs (swap! cache-state hit-or-miss args)]
+ (-> cs cache (get args) deref)))
+ :invalidate
+ (fn [args]
+ (swap! cache-state mark-dirty args)
+ nil)}))
+
+(defmacro defn-cached
+ "Defines a cached function, like defn-memo from clojure.contrib.def
+ e.g
+ (defn-cached fib
+ (lru-cache-strategy 10)
+ [n]
+ (if (<= n 1)
+ n
+ (+ (fib (dec n)) (fib (- n 2)))))"
+ [fn-name cache-strategy & defn-stuff]
+ `(let [f-name# (str *ns* "." '~fn-name)]
+ (defn ~fn-name ~@defn-stuff)
+ (alter-var-root (var ~fn-name)
+ cached* f-name# ~cache-strategy)
+ (var ~fn-name)))
+
+(def function-utils* (atom {}))
+
+(def memoizers* {:external-memoize external-memoize
+ :internal-memoize internal-memoize})
+
+(defn cached*
+ "Sets up a cache for the given function with the given name"
+ [f f-name strategy]
+ (let [memoizer (-> strategy :plugs-into memoizers*)
+ internals (memoizer f f-name strategy)
+ cached-f (:memoized internals)
+ utils (dissoc internals :memoized)]
+ (if (and (= memoizer external-memoize)
+ (= f-name :anon))
+ (throw (Exception. (str (strategy :description)
+ " does not support anonymous functions"))))
+ (if-not (empty? utils)
+ (swap! function-utils* assoc cached-f utils))
+ cached-f))
+
+(defmacro cached
+ "Returns a cached function that can be invalidated by calling
+ invalidate-cache e.g
+ (def fib (cached fib (lru-cache-stategy 5)))"
+ [f strategy]
+ (if-not (symbol? f)
+ `(cached* ~f :anon ~strategy)
+ `(let [f-name# (str *ns* "." '~f)]
+ (cached* ~f f-name# ~strategy))))
+
+(defn invalidate-cache
+ "Invalidates the cache for the function call with the given arguments
+ causing it to be re-evaluated e.g
+ (invalidate-cache fib 30) ;; A call to (fib 30) will not use the cache
+ (invalidate-cache fib 29) ;; A call to (fib 29) will not use the cache
+ (fib 18) ;; A call to (fib 18) will use the cache"
+ [cached-f & args]
+ (if-let [inv-fn (:invalidate (@function-utils* cached-f))]
+ (inv-fn args)))
+
+
+;;======== Stategies for for memoize ==========================================
+
+(def #^{:doc "A naive strategy for testing external-memoize"}
+ naive-external-strategy
+ {:init (fn [_] (atom {}))
+ :lookup (fn [m args]
+ (let [v (get @m args ::not-found)]
+ (if (= v ::not-found)
+ [false nil]
+ [true v])))
+ :miss! (fn [m args res]
+ (swap! m assoc args res)
+ res)
+ :invalidate! (fn [m args]
+ (swap! m dissoc args)
+ nil)
+ :description "Naive external strategy"
+ :plugs-into :external-memoize})
+
+(def #^{:doc "The naive save-all cache strategy for memoize."}
+ naive-strategy
+ {:init {}
+ :cache identity
+ :lookup contains?
+ :hit (fn [state _] state)
+ :miss assoc
+ :invalidate assoc
+ :plugs-into :internal-memoize})
+
+(defn lru-cache-strategy
+ "Implements a LRU cache strategy, which drops the least recently used
+ argument lists from the cache. If the given limit of items in the cache
+ is reached, the longest unaccessed item is removed from the cache. In
+ case there is a tie, the removal order is unspecified."
+ [limit]
+ {:init {:lru (into {} (for [x (range (- limit) 0)] [x x]))
+ :tick 0
+ :cache {}}
+ :cache :cache
+ :lookup (fn [state k] (contains? (:cache state) k))
+ :hit (fn [state args]
+ (-> state
+ (assoc-in [:lru args] (:tick state))
+ (update-in [:tick] inc)))
+ :miss (fn [state args result]
+ (let [k (apply min-key (:lru state) (keys (:lru state)))]
+ (-> state
+ (update-in [:lru] dissoc k)
+ (update-in [:cache] dissoc k)
+ (assoc-in [:lru args] (:tick state))
+ (update-in [:tick] inc)
+ (assoc-in [:cache args] result))))
+ :invalidate (fn [state args placeholder]
+ (if (contains? (:lru state) args)
+ (assoc-in state [:cache args] placeholder)))
+ :plugs-into :internal-memoize})
+
+
+(defn ttl-cache-strategy
+ "Implements a time-to-live cache strategy. Upon access to the cache
+ all expired items will be removed. The time to live is defined by
+ the given expiry time span. Items will only be removed on function
+ call. No background activity is done."
+ [ttl]
+ (let [dissoc-dead (fn [state now]
+ (let [ks (map key (filter #(> (- now (val %)) ttl)
+ (:ttl state)))
+ dissoc-ks #(apply dissoc % ks)]
+ (-> state
+ (update-in [:ttl] dissoc-ks)
+ (update-in [:cache] dissoc-ks))))]
+ {:init {:ttl {} :cache {}}
+ :cache :cache
+ :lookup (fn [state args]
+ (when-let [t (get (:ttl state) args)]
+ (< (- (System/currentTimeMillis) t) ttl)))
+ :hit (fn [state args]
+ (dissoc-dead state (System/currentTimeMillis)))
+ :miss (fn [state args result]
+ (let [now (System/currentTimeMillis)]
+ (-> state
+ (dissoc-dead now)
+ (assoc-in [:ttl args] now)
+ (assoc-in [:cache args] result))))
+ :invalidate (fn [state args placeholder]
+ (if (contains? (:ttl state) args)
+ (assoc-in state [:cache args] placeholder)))
+ :plugs-into :internal-memoize}))
+
+(defn lu-cache-strategy
+ "Implements a least-used cache strategy. Upon access to the cache
+ it will be tracked which items are requested. If the cache size reaches
+ the given limit, items with the lowest usage count will be removed. In
+ case of ties the removal order is unspecified."
+ [limit]
+ {:init {:lu (into {} (for [x (range (- limit) 0)] [x x])) :cache {}}
+ :cache :cache
+ :lookup (fn [state k] (contains? (:cache state) k))
+ :hit (fn [state args] (update-in state [:lu args] inc))
+ :miss (fn [state args result]
+ (let [k (apply min-key (:lu state) (keys (:lu state)))]
+ (-> state
+ (update-in [:lu] dissoc k)
+ (update-in [:cache] dissoc k)
+ (assoc-in [:lu args] 0)
+ (assoc-in [:cache args] result))))
+ :invalidate (fn [state args placeholder]
+ (if (contains? (:lu state) args)
+ (assoc-in state [:cache args] placeholder)))
+ :plugs-into :internal-memoize}) \ No newline at end of file
diff --git a/src/multikey_cache.clj b/src/multikey_cache.clj
new file mode 100644
index 0000000..74822a8
--- /dev/null
+++ b/src/multikey_cache.clj
@@ -0,0 +1,30 @@
+(ns multikey-cache
+ (:import java.util.Collections
+ org.apache.commons.collections.map.LRUMap)
+ (:use clojure.contrib.seq-utils
+ cache-dot-clj.cache
+ ))
+
+(defn multikey-lru-cache [f max-size]
+ {:f f
+ :lru (java.util.Collections/synchronizedMap (LRUMap. max-size))
+ })
+
+(defn get-keys [c keys]
+ (let [f (:f c)
+ lru (:lru c)
+ uncached (filter (complement #(.containsKey lru %)) keys)]
+ (when-not (empty? uncached)
+ (doseq [[k v] (map list uncached (f uncached))]
+ (.put lru k v)))
+ (map #(.get lru %) keys)))
+
+(defn get-key [c key]
+ (first (get-keys c [key])))
+
+(defn has-key [c key]
+ (.containsKey (:lru c) key))
+
+(defn invalidate-key [c key]
+ (.remove (:lru c) key)
+ nil)
diff --git a/src/site.clj b/src/site.clj
index b7b8787..90db5dc 100644
--- a/src/site.clj
+++ b/src/site.clj
@@ -117,27 +117,6 @@
(if-not (:admin_only msg)
msg)))
-;; User-id/nick cache
-;; I keep needing to grab user-id from a nick so I thought I'd cache them
-;; sostler todo: will replace this w/ user/user-id-cache soon
-(def *user-id-cache* (ref {}))
-(def *user-id-cache-size* 500)
-
-(defn user-id-from-nick [nick]
- (let [nick (lower-case nick)
- found (@*user-id-cache* nick)]
- (if found
- found
- (let [query (str "SELECT user_id FROM users WHERE lower(nick) = ?")
- res (first (do-select [query nick]))]
- (if (nil? res)
- nil
- (let [found (res :user_id)]
- (dosync
- (if (> (count @*user-id-cache*) *user-id-cache-size*) (ref-set *user-id-cache* {}))
- (alter *user-id-cache* assoc nick found))
- found))))))
-
;; Login code
(defn session-map-from-db
@@ -353,26 +332,15 @@ WHERE user_id IN
(.toString st)))
(resp-error "NO_USER"))))
-(defn update-user-db [user-id attr val]
- (with-connection *db*
- (update-values "users" ["user_id = ?" user-id] {attr val}))
- (update-cache! user-id attr val))
-
-(defn update-avatar [session url]
- (update-user-db (session :user_id) "avatar" url)
- [(session-assoc :avatar url)
- (resp-success url)])
-
(defn update-profile [session params]
- (let [user-id (session :user_id)
- attr (params :attr)
- val (params :val)
- attr-set #{"avatar" "contact" "bio"}]
+ (let [user-id (session :user_id)
+ attr (params :attr)
+ val (params :val)
+ attr-set #{"contact" "bio"}]
(cond (not user-id) (resp-error "MUST_LOGIN")
(not (and user-id attr val)) (resp-error "BAD_REQUEST")
(not (contains? attr-set attr)) (resp-error "BAD_REQUEST")
- (= attr "avatar") (update-avatar session val)
- :else (do (update-user-db user-id attr val)
+ :else (do (update-user-info! (:nick session) user-id attr val)
(resp-success "OK")))))
;; Generic Handler
@@ -1133,7 +1101,7 @@ WHERE user_id IN
url (image-url-from-file "avatars" date dest)]
(do
(copy (:tempfile image) dest)
- (update-user-db (session :user_id) "avatar" url)
+ (update-user-info! (:nick session) (:user_id session) "avatar" url)
[(session-assoc :avatar url)
[200 url]])))
diff --git a/src/tags.clj b/src/tags.clj
index fb530ea..76bc762 100644
--- a/src/tags.clj
+++ b/src/tags.clj
@@ -336,10 +336,8 @@ WHERE EXISTS
(let [ids (map maybe-parse-int ids)
query (fetch-dumps-by-message-id-query :num-messages (count ids))
raw-rows (do-select (vec (concat [query] ids)))
- tagged-rows (map parse-tags-from-row-as-tag-map raw-rows)
- index-func (fn [row]
- (index-of #(= (:message_id row) %) ids))]
- (for [m (sort-by index-func tagged-rows)]
+ tagged-rows (map parse-tags-from-row-as-tag-map raw-rows)]
+ (for [m (sort-by-index-in tagged-rows ids :message_id)]
(let [favers (get (:tags m) "favorite")
favorited (and viewer-nick
(boolean (some #(= % viewer-nick) favers)))
diff --git a/src/user.clj b/src/user.clj
index 8380bce..c516fd2 100644
--- a/src/user.clj
+++ b/src/user.clj
@@ -1,5 +1,7 @@
(ns user
- (:use compojure
+ (:use clojure.contrib.sql
+ compojure
+ cache-dot-clj.cache
utils))
(defstruct user-struct :nick :user_id :avatar :last-seen)
@@ -18,71 +20,38 @@
;;; User info cache
-(def user-cache-size 99999)
-(def user-nick-cache (ref {}))
-(def user-id-cache (ref {}))
+(defn-cached fetch-nick-cached
+ (lru-cache-strategy 2000)
+ "Retrieves user info from database"
+ [nick]
+ (first (do-select ["SELECT * FROM users WHERE lower(nick) = ? LIMIT 1"
+ (lower-case nick)])))
-(defn update-cache! [uid attr val]
- (dosync
- (if-let [info (get @user-id-cache uid)]
- (let [nick (lower-case (:nick info))
- new-info (assoc info attr val)]
- (alter user-id-cache assoc uid new-info)
- (alter user-nick-cache assoc nick new-info)))))
-
+(def fetch-nick (comp fetch-nick-cached lower-case))
-(defn fetch-nick [nick]
- (let [lcnick (lower-case nick)]
- (if (contains? user-nick-cache lcnick)
- (get user-nick-cache lcnick)
- (let [info (first
- (do-select ["SELECT * FROM users WHERE lower(nick) = ? LIMIT 1"
- lcnick]))
- user-id (:user_id info)]
- (dosync
- (alter user-nick-cache assoc lcnick info)
- (if (and info user-id)
- (alter user-id-cache assoc user-id info)))
- info))))
+(defn-cached nick-from-user-id
+ (lru-cache-strategy 10000)
+ "Retrieves nick for user id"
+ [uid]
+ (:nick (first (do-select ["SELECT nick FROM users WHERE user_id = ? LIMIT 1" uid]))))
-(defn fetch-nicks [nicks]
- (let [lcnicks (map lower-case nicks)
- cache @user-nick-cache
- to-fetch (filter #(not (contains? cache %)) lcnicks)
- fetched-info (do-select ["SELECT * FROM users WHERE lower(nick) = ANY(?)"
- (sql-array "text" to-fetch)])
- info-map (zipmap (map (comp lower-case :nick) fetched-info)
- fetched-info)]
- (doseq [nick to-fetch]
- (let [info (get info-map nick)]
- (dosync
- (alter user-nick-cache assoc nick info)
- (if info
- (alter user-id-cache assoc (:user_id info) info)))))
- (filter
- boolean
- (for [nick lcnicks]
- (get @user-nick-cache nick)))))
+(def fetch-user-id
+ (comp fetch-nick nick-from-user-id))
-(defn fetch-user-id [uid]
- (if (contains? @user-id-cache uid)
- (get @user-id-cache uid)
- (if-let [info (first
- (do-select ["SELECT * FROM users WHERE user_id = ? LIMIT 1" uid]))]
- (dosync
- (alter user-nick-cache assoc (lower-case (:nick info)) info)
- (alter user-id-cache assoc uid info)))))
+(def user-id-from-nick
+ (comp :user_id fetch-nick))
+
+;; user login
(defn authorize-nick-hash [nick hash]
- (let [db-user (fetch-nick nick)]
- (and db-user (= (db-user :hash) hash) db-user)))
+ (if-let [db-user (fetch-nick nick)]
+ (and (= (db-user :hash) hash) db-user)))
(defn update-nick-hash [nick hash]
- (if (not (assert-update
- (do-update :users ["nick=?" nick]
- {:hash hash})))
- ; TODO: logging
- (println (format "Error updating hash for %s" nick))))
+ (do-update :users ["nick=?" nick]
+ {:hash hash}))
+
+;; user pw reset
(defn reset-token [nick hash ts]
(sha1-hash nick hash ts))
@@ -96,3 +65,11 @@
(if-let [info (and nick (fetch-nick nick))]
(and (= token (reset-token (info :nick) (info :hash) ts))
(>= ts (ms-ago (days 2))))))
+
+
+;; user db update & cache invalidation
+
+(defn update-user-info! [nick user-id attr val]
+ (with-connection *db*
+ (update-values "users" ["user_id = ?" user-id] {attr val}))
+ (invalidate-cache fetch-nick-cached (lower-case nick)))
diff --git a/src/utils.clj b/src/utils.clj
index ee64789..84f185a 100755
--- a/src/utils.clj
+++ b/src/utils.clj
@@ -269,8 +269,7 @@
(insert-values table cols values)))
(defn assert-update
- ([res ok err] (if (not (= (first res) 1)) err ok))
- ([res] (assert-update res true false)))
+ ([res] (= (first res) 1)))
(defn sql-array [type arr]
(with-connection *db*
@@ -447,10 +446,15 @@
{ :result result :time (System/currentTimeMillis)})
result)))))
-;; From Programming Clojure by Stuart Halloway
+;; Misc index/sorting funcs
-(defn index-filter [pred coll]
- (for [[idx elt] (indexed coll) :when (pred elt)] idx))
-
-(defn index-of [pred coll]
- (first (index-filter pred coll)))
+(defn sort-by-index-in
+ ([c1 c2] (sort-by-index-in c1 c2 nil nil))
+ ([c1 c2 f1] (sort-by-index-in c1 c2 f1 nil))
+ ([c1 c2 f1 f2]
+ (let [c2-map (zipmap (if f2 (map f2 c2) c2)
+ (range (count c2)))
+ sort-func (if f1
+ #(get c2-map (f1 %))
+ #(get c2-map %))]
+ (sort-by sort-func c1))))
diff --git a/static/js/admin.js b/static/js/admin.js
index fc18685..948fa99 100644
--- a/static/js/admin.js
+++ b/static/js/admin.js
@@ -37,6 +37,8 @@ Admin.mute = function(nick) {
if (!r) { reason.addClass('ui-state-error'); }
if (!t || !u || !r) { return; }
+ html.dialog("option", "disabled", true);
+
$.ajax({
type: 'POST',
timeout: 5000,
@@ -47,6 +49,7 @@ Admin.mute = function(nick) {
success: close,
error: function(s) {
alert("Error muting user: " + s.responseText);
+ close();
}
});
};
diff --git a/static/js/pichat.js b/static/js/pichat.js
index 884e01f..24b8f0c 100644
--- a/static/js/pichat.js
+++ b/static/js/pichat.js
@@ -5,11 +5,16 @@ expires='; expires='+date.toUTCString();}
var path=options.path?'; path='+(options.path):'';var domain=options.domain?'; domain='+(options.domain):'';var secure=options.secure?'; secure':'';document.cookie=[name,'=',encodeURIComponent(value),expires,path,domain,secure].join('');}else{var cookieValue=null;if(document.cookie&&document.cookie!=''){var cookies=document.cookie.split(';');for(var i=0;i<cookies.length;i++){var cookie=jQuery.trim(cookies[i]);if(cookie.substring(0,name.length+1)==(name+'=')){cookieValue=decodeURIComponent(cookie.substring(name.length+1));break;}}}
return cookieValue;}};
-var cache = {}
-var PendingMessages = {}
-var MessageContentCache = {}
-var RawFavs = {}
-var MaxImagePosts = 30
+// The root domain is used so that subdomains don't result in
+// spurious extra urls (e.g. both dump.fm/nick and sub.dump.fm/nick)
+var RootDomain = location.href.match(/http:\/\/(\w)+\./)
+ ? 'http://dump.fm/' : '/';
+
+var cache = {};
+var PendingMessages = {};
+var MessageContentCache = {};
+var RawFavs = {};
+var MaxImagePosts = 30;
// todo: preload these. also, look into image sprites (no go on animating their sizes tho)
// css clipping perhaps?
@@ -139,7 +144,7 @@ function getImagesAsArray(text) {
function topicReplace(text) {
text = $.trim(text).toLowerCase();
var topicLabel = text.substring(1);
- return ' <a target="_blank" href="http://dump.fm/t/' + topicLabel + '">' + text + '</a> ';
+ return ' <a target="_blank" href="'+ RootDomain + 't/' + topicLabel + '">' + text + '</a> ';
}
function recipientReplace(atText, recips) {
@@ -161,7 +166,7 @@ function recipientReplace(atText, recips) {
}
if (matchedRecip) {
- return space + '<a target="_blank" href="http://dump.fm/' + matchedRecip + '">@' + matchedRecip + '</a>';
+ return space + '<a target="_blank" href="' + RootDomain + matchedRecip + '">@' + matchedRecip + '</a>';
} else {
return space + atText;
}
@@ -369,7 +374,7 @@ function buildMessageDiv(msg, opts) {
var displayStyle = ((ImgsEnabled && LastMsgContainsImage) || (TextEnabled && !LastMsgContainsImage)) ? '' : ' style="display: none"';
return '<div class="msgDiv dump ' + loadingClass + containsImageClass + '" ' + msgId + displayStyle + '>'
- + '<span class="nick"><b><a href="http://dump.fm/' + nick + ' ">' + nick + '</a></b>'
+ + '<span class="nick"><b><a href="' + RootDomain + nick + ' ">' + nick + '</a></b>'
+ ' <img src="'+Imgs.chatThumbDot+'" class="chat-thumb" onclick="Tag.favorite(this)"> '
+ '</span>'
+ '<span class="content">' + builtContent + '</span>'
@@ -379,13 +384,13 @@ function buildMessageDiv(msg, opts) {
function buildUserDiv(user) {
if (user.avatar) {
return '<div class="username">'
- + '<a href="http://dump.fm/' + escapeHtml(user.nick) + '" target="_blank">'
+ + '<a href="' + RootDomain + escapeHtml(user.nick) + '" target="_blank">'
+ '<img src="' + user.avatar + '" width="50" height="50">'
+ escapeHtml(user.nick) + '</a></div>';
} else {
return '<div class="username">'
- + '<a href="http://dump.fm/' + escapeHtml(user.nick) + '" target="_blank">'
- + '<img src="/static/img/noinfo.png" width="50" height="50">'
+ + '<a href="' + RootDomain + escapeHtml(user.nick) + '" target="_blank">'
+ + '<img src="' + RootDomain + 'static/img/noinfo.png" width="50" height="50">'
+ escapeHtml(user.nick) + '</a></div>';
}
}
@@ -394,8 +399,8 @@ function buildUserDiv(user) {
function buildFav(f) {
var h = '<div class="fav-note">'
- + '<img src="http://dump.fm/static/img/thumbs/chatheartover.gif">'
- + '<a href="http://dump.fm/' + f.from + '">' + f.from + '</a>'
+ + '<img src="' + RootDomain + 'static/img/thumbs/chatheartover.gif">'
+ + '<a href="' + RootDomain + f.from + '">' + f.from + '</a>'
+ '&nbsp;<span>just faved you!</span>'
+ '</div>';
return $(h);
@@ -429,7 +434,7 @@ function updateFavs(fs) {
function buildGrowlDataAndPopDatShit(msg) {
var nick = escapeHtml(msg.nick);
- nick = '<a href="http://dump.fm/' + nick + ' " style="color:pink">' + nick + '</a>:'
+ nick = '<a href="' + RootDomain + nick + ' " style="color:pink">' + nick + '</a>:'
var msg = buildMsgContent(msg.content)
growl(nick, msg)
}
@@ -939,9 +944,9 @@ function setupUploadAvatar(elementId) {
+ maxWidth + "x" + maxHeight);
return;
}
-
var s = '<img id="dashavatarPic" src="' + r + '" />';
$('#dashavatar').html(s).show();
+ $('#dashtotal').css('background-image', 'url(' + r + ')');
};
new AjaxUpload(elementId, {
action: '/upload/avatar',
diff --git a/template/messagepane.st b/template/messagepane.st
index 0f910de..9273826 100644
--- a/template/messagepane.st
+++ b/template/messagepane.st
@@ -2,7 +2,7 @@
$if(user_nick)$
<div id="userList">
$users: { u |
- <div class="username"><a href="/$u.nick$" target="_blank">
+ <div class="username"><a href="$domain$/$u.nick$" target="_blank">
$u.score_ent$
$if(u.avatar)$
<img src="$u.avatar$" width="50" height="50">
diff --git a/template/rooms/chat.st b/template/rooms/chat.st
index f8829ca..63c8d11 100644
--- a/template/rooms/chat.st
+++ b/template/rooms/chat.st
@@ -83,9 +83,9 @@
</div>
</div>
- <link href="/static/css/pages.css" rel="stylesheet" type="text/css" media="screen" />
- <link href="/static/css/header.css" rel="stylesheet" type="text/css" media="screen" />
- <script type="text/javascript" src="/static/js/win.js"></script>
+ <link href="$domain$/static/css/pages.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="$domain$/static/css/header.css" rel="stylesheet" type="text/css" media="screen" />
+ <script type="text/javascript" src="$domain$/static/js/win.js"></script>
<script type="text/javascript">
var topZ = 7;
CreateDropdownWindow('', '500px', true, 'dis_welcome', 300, 70);
diff --git a/template/rooms/mgmt.st b/template/rooms/mgmt.st
index dc7060b..b8d4985 100644
--- a/template/rooms/mgmt.st
+++ b/template/rooms/mgmt.st
@@ -78,19 +78,19 @@ window.location = "http://dump.fm/error/ie";
<div align="center">
<img style="visibility:hidden;width:0px;height:0px;" border=0 width=0 height=0 />
<a class="img_roll" href="$domain$"></a>
- <span class="fulltxt"> <a href="http://dump.fm/m/mgmtfull">View in fullscreen!</a></span>
+ <span class="fulltxt"> <a href="$domain$/m/mgmtfull">View in fullscreen!</a></span>
</div>
</div>
<div id="userList">
$users: { u |
- <div class="username"><a href="/$u.nick$" target="_blank">
+ <div class="username"><a href="$domain$/$u.nick$" target="_blank">
$u.score_ent$
$if(u.avatar)$
<img src="$u.avatar$" width="50" height="50">
$else$
- <img src="/static/img/noinfo.png">
+ <img src="$domain$/static/img/noinfo.png">
$endif$
$u.nick$</a><br>
</div>
@@ -103,9 +103,9 @@ window.location = "http://dump.fm/error/ie";
<span class="nick">
<b><a href="$domain$/$m.nick$" target="_blank">$m.nick$</a></b>
$if(m.favorited)$
- <img src="/static/img/thumbs/chatheartover.gif" class="chat-thumb" onclick="Tag.favorite(this)">
+ <img src="$domain$/static/img/thumbs/chatheartover.gif" class="chat-thumb" onclick="Tag.favorite(this)">
$else$
- <img src="/static/img/thumbs/smallheart.gif" class="chat-thumb" onclick="Tag.favorite(this)">
+ <img src="$domain$/static/img/thumbs/smallheart.gif" class="chat-thumb" onclick="Tag.favorite(this)">
$endif$
</span>
<span class="content">$m.content$</span>