diff options
| -rwxr-xr-x | bin/repl.bat | 2 | ||||
| -rwxr-xr-x | bin/repl.sh | 2 | ||||
| -rw-r--r-- | lib/commons-collections-3.2.1.jar | bin | 0 -> 575389 bytes | |||
| -rw-r--r-- | scripts/mias3.py | 76 | ||||
| -rw-r--r-- | src/admin.clj | 18 | ||||
| -rw-r--r-- | src/cache_dot_clj/cache.clj | 241 | ||||
| -rw-r--r-- | src/multikey_cache.clj | 30 | ||||
| -rw-r--r-- | src/site.clj | 44 | ||||
| -rw-r--r-- | src/tags.clj | 6 | ||||
| -rw-r--r-- | src/user.clj | 93 | ||||
| -rwxr-xr-x | src/utils.clj | 20 | ||||
| -rw-r--r-- | static/js/admin.js | 3 | ||||
| -rw-r--r-- | static/js/pichat.js | 35 | ||||
| -rw-r--r-- | template/messagepane.st | 2 | ||||
| -rw-r--r-- | template/rooms/chat.st | 6 | ||||
| -rw-r--r-- | template/rooms/mgmt.st | 10 |
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 Binary files differnew file mode 100644 index 0000000..c35fa1f --- /dev/null +++ b/lib/commons-collections-3.2.1.jar 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>' + ' <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> |
