clojure notehub removed; let's start from scratch
This commit is contained in:
@@ -1,126 +0,0 @@
|
||||
(ns notehub.api
|
||||
(:import
|
||||
[java.util Calendar])
|
||||
(:use
|
||||
[iokv.core]
|
||||
[ring.util.codec :only [url-encode]]
|
||||
[clojure.string :rename {replace sreplace}
|
||||
:only [replace blank? trim lower-case split-lines split]])
|
||||
(:require
|
||||
[ring.util.codec]
|
||||
[hiccup.util :as util]
|
||||
[notehub.storage :as storage]))
|
||||
|
||||
(def version "1.4")
|
||||
|
||||
(defn log
|
||||
"Logs args to the server stdout"
|
||||
[string & args]
|
||||
(apply printf (str "%s:" string) (str (storage/get-current-date) ":LOG") args)
|
||||
(println))
|
||||
|
||||
(defn url
|
||||
"Creates a local url from the given substrings"
|
||||
[& args]
|
||||
(apply str (interpose "/" (cons "" (map url-encode args)))))
|
||||
|
||||
; Concatenates all fields to a string
|
||||
(defn build-key
|
||||
"Returns a storage-key for the given note coordinates"
|
||||
[year month day title]
|
||||
(apply str (interpose "/" [year month day title])))
|
||||
|
||||
(defn derive-title [md-text]
|
||||
(sreplace (first (split-lines md-text))
|
||||
#"(#+|_|\*+|<.*?>)" ""))
|
||||
|
||||
(defn get-date
|
||||
"Returns today's date"
|
||||
[]
|
||||
(map #(+ (second %) (.get (Calendar/getInstance) (first %)))
|
||||
{Calendar/YEAR 0, Calendar/MONTH 1, Calendar/DAY_OF_MONTH 0}))
|
||||
|
||||
(defn- create-response
|
||||
([success] {:success success})
|
||||
([success message & params]
|
||||
(assoc (create-response success) :message (apply format message params))))
|
||||
|
||||
(defn- get-path [host-url token & [description]]
|
||||
(if (= :url description)
|
||||
(str host-url "/" token)
|
||||
(let [[year month day title] (split token #"/")]
|
||||
(if description
|
||||
(str host-url "/" (storage/create-short-url token {:year year :month month :day day :title title}))
|
||||
(str host-url (url year month day title))))))
|
||||
|
||||
(defn version-manager [f params]
|
||||
(if-let [req-version (:version params)]
|
||||
(let [req-version (Double/parseDouble req-version)
|
||||
version (Double/parseDouble version)]
|
||||
(if (< req-version version)
|
||||
{:status (create-response false "Deprecated API version")}
|
||||
(f params)))
|
||||
{:status (create-response false "API version expected")}))
|
||||
|
||||
(defn get-note [{:keys [noteID hostURL]}]
|
||||
(if (storage/note-exists? noteID)
|
||||
(let [note (storage/get-note noteID)]
|
||||
(storage/increment-note-view noteID)
|
||||
{:note note
|
||||
:title (derive-title note)
|
||||
:longURL (get-path hostURL noteID)
|
||||
:shortURL (get-path hostURL noteID :id)
|
||||
:statistics (storage/get-note-statistics noteID)
|
||||
:status (create-response true)
|
||||
:publisher (storage/get-publisher noteID)})
|
||||
{:status (create-response false "noteID '%s' unknown" noteID)}))
|
||||
|
||||
(defn propose-title [note]
|
||||
(let [raw-title (filter #(or (= \- %) (Character/isLetterOrDigit %))
|
||||
(-> note derive-title trim (sreplace " " "-") lower-case))
|
||||
max-length (get-setting :max-title-length #(Integer/parseInt %) 80)]
|
||||
(apply str (take max-length raw-title))))
|
||||
|
||||
(defn post-note
|
||||
[{:keys [note pid signature password hostURL] :as params}]
|
||||
;(log "post-note: %s" {:pid pid :signature signature :password password :note note})
|
||||
(let [errors (filter identity
|
||||
[(when-not (storage/valid-publisher? pid) "pid invalid")
|
||||
(when-not (= signature (storage/sign pid (storage/get-psk pid) note))
|
||||
"signature invalid")
|
||||
(when (blank? note) "note is empty")])]
|
||||
(if (empty? errors)
|
||||
(let [[year month day] (map str (get-date))
|
||||
params (select-keys params [:text-size :header-size :text-font :header-font :theme])
|
||||
proposed-title (propose-title note)
|
||||
title (first (drop-while #(storage/note-exists? (build-key year month day %))
|
||||
(cons proposed-title
|
||||
(map #(str proposed-title "-" (+ 2 %)) (range)))))
|
||||
noteID (build-key year month day title)
|
||||
new-params (assoc params :year year :month month :day day :title title)
|
||||
short-url (get-path hostURL (storage/create-short-url noteID new-params) :url)
|
||||
long-url (get-path hostURL noteID)]
|
||||
(do
|
||||
(storage/add-note noteID note pid password)
|
||||
{:noteID noteID
|
||||
:longURL (if (empty? params) long-url (str (util/url long-url params)))
|
||||
:shortURL short-url
|
||||
:status (create-response true)}))
|
||||
{:status (create-response false (first errors))})))
|
||||
|
||||
|
||||
(defn update-note [{:keys [noteID note pid signature password hostURL]}]
|
||||
;(log "update-note: %s" {:pid pid :noteID noteID :signature signature :password password :note note})
|
||||
(let [errors (filter identity
|
||||
[(when-not (storage/valid-publisher? pid) "pid invalid")
|
||||
(when-not (= signature (storage/sign pid (storage/get-psk pid) noteID note password))
|
||||
"signature invalid")
|
||||
(when (blank? note) "note is empty")
|
||||
(when-not (storage/valid-password? noteID password) "password invalid")])]
|
||||
(if (empty? errors)
|
||||
(do
|
||||
(storage/edit-note noteID note)
|
||||
{:longURL (get-path hostURL noteID)
|
||||
:shortURL (get-path hostURL noteID :id)
|
||||
:status (create-response true)})
|
||||
{:status (create-response false (first errors))})))
|
||||
@@ -1,260 +0,0 @@
|
||||
(ns notehub.css
|
||||
(:require [garden.core :refer [css]]
|
||||
[garden.stylesheet :refer [at-media]]
|
||||
[garden.units :as u :refer [px pt em]]))
|
||||
|
||||
(def themes
|
||||
{
|
||||
"dark"
|
||||
{
|
||||
:background {
|
||||
:normal "#333",
|
||||
:halftone "#444"
|
||||
},
|
||||
:foreground {
|
||||
:normal "#ccc",
|
||||
:halftone "#bbb"
|
||||
},
|
||||
:link {
|
||||
:fresh "#6b8",
|
||||
:visited "#496",
|
||||
:hover "#7c9"
|
||||
}
|
||||
},
|
||||
"solarized-light"
|
||||
{
|
||||
:background {
|
||||
:normal "#fdf6e3",
|
||||
:halftone "#eee8d5"
|
||||
},
|
||||
:foreground {
|
||||
:normal "#657b83",
|
||||
:halftone "#839496"
|
||||
},
|
||||
:link {
|
||||
:fresh "#b58900",
|
||||
:visited "#cb4b16",
|
||||
:hover "#dc322f"
|
||||
}
|
||||
},
|
||||
"solarized-dark"
|
||||
{
|
||||
:background {
|
||||
:normal "#073642",
|
||||
:halftone "#002b36"
|
||||
},
|
||||
:foreground {
|
||||
:normal "#93a1a1",
|
||||
:halftone "#839191"
|
||||
},
|
||||
:link {
|
||||
:fresh "#cb4b16",
|
||||
:visited "#b58900",
|
||||
:hover "#dc322f"
|
||||
}
|
||||
},
|
||||
"default"
|
||||
{
|
||||
:background {
|
||||
:normal "#fff",
|
||||
:halftone "#efefef"
|
||||
},
|
||||
:foreground {
|
||||
:normal "#333",
|
||||
:halftone "#888"
|
||||
},
|
||||
:link {
|
||||
:fresh "#097",
|
||||
:visited "#054",
|
||||
:hover "#0a8"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
(defn generate [params]
|
||||
(let [theme (themes (params "theme" "default"))
|
||||
; VARIABLES
|
||||
background (get-in theme [:background :normal])
|
||||
foreground (get-in theme [:foreground :normal])
|
||||
background-halftone (get-in theme [:background :halftone])
|
||||
foreground-halftone (get-in theme [:foreground :halftone])
|
||||
link-fresh (get-in theme [:link :fresh])
|
||||
link-visited (get-in theme [:link :visited])
|
||||
link-hover (get-in theme [:link :hover])
|
||||
width (px 800)
|
||||
header-font (or (params "header-font") "Noticia Text")
|
||||
text-font (or (params "text-font") "Georgia")
|
||||
header-size-factor (Float/parseFloat (or (params "header-size") "1"))
|
||||
text-size-factor (Float/parseFloat (or (params "text-size") "1"))
|
||||
|
||||
; MIXINS
|
||||
helvetica {
|
||||
:font-weight 300
|
||||
:font-family "'Helvetica Neue','Helvetica','Arial','Lucida Grande','sans-serif'"
|
||||
}
|
||||
central-element {
|
||||
:margin-left "auto"
|
||||
:margin-right "auto"
|
||||
}
|
||||
thin-border {
|
||||
:border (print-str "1px solid" foreground)
|
||||
}]
|
||||
(css
|
||||
[:.ui-border { :border-radius (px 3) } thin-border]
|
||||
|
||||
[:a {
|
||||
:color link-fresh
|
||||
:text-decoration "none"
|
||||
:border-bottom "1px dotted"
|
||||
}]
|
||||
[:a:hover { :color link-hover }]
|
||||
[:a:visited { :color link-visited }]
|
||||
[:#draft {
|
||||
:margin-bottom (em 3)
|
||||
}]
|
||||
[:.button {
|
||||
:cursor "pointer"
|
||||
}]
|
||||
[:.ui-elem {
|
||||
:border-radius (px 3)
|
||||
:padding (em 0.3)
|
||||
:opacity 0.8
|
||||
:font-size (em 1)
|
||||
:background background
|
||||
}
|
||||
helvetica thin-border]
|
||||
[:.landing-button, :textarea, :fieldset { :border "none" }]
|
||||
[:.landing-button {
|
||||
:box-shadow "0 2px 5px #aaa"
|
||||
:text-decoration "none"
|
||||
:font-size (em 1.5)
|
||||
:background "#0a2"
|
||||
:border-radius (px 10)
|
||||
:padding (px 10)
|
||||
}
|
||||
helvetica]
|
||||
[:.landing-button:hover { :background "#0b2" }]
|
||||
|
||||
[:.helvetica helvetica]
|
||||
|
||||
[:#footer {
|
||||
:width "100%"
|
||||
:font-size (em 0.8)
|
||||
:padding-bottom (em 1)
|
||||
:text-align "center"
|
||||
}
|
||||
helvetica]
|
||||
(at-media {:screen true :max-width (px 767)} [:#footer {:font-size (em 0.4)}])
|
||||
["#footer a" { :border "none" }]
|
||||
|
||||
[:html, :body {
|
||||
:background background
|
||||
:color foreground
|
||||
:margin 0
|
||||
:padding 0
|
||||
}]
|
||||
[:#hero {
|
||||
:padding-top (em 5)
|
||||
:padding-bottom (em 5)
|
||||
:text-align "center"
|
||||
}]
|
||||
[:h1, :h2, :h3, :h4, :h5, :h6 {
|
||||
:font-weight "bold"
|
||||
:font-family (str header-font ",'Noticia Text','PT Serif','Georgia'")
|
||||
}]
|
||||
[:h1 { :font-size (em (* 1.8 header-size-factor)) }]
|
||||
[:h2 { :font-size (em (* 1.6 header-size-factor)) }]
|
||||
[:h3 { :font-size (em (* 1.4 header-size-factor)) }]
|
||||
[:h4 { :font-size (em (* 1.2 header-size-factor)) }]
|
||||
[:h5 { :font-size (em (* 1.1 header-size-factor)) }]
|
||||
[:h6 { :font-size (em (* 1 header-size-factor)) }]
|
||||
|
||||
["#hero h1" { :font-size (em 2.5) }]
|
||||
["#hero h2" { :margin (em 2) } helvetica ]
|
||||
|
||||
[:article {
|
||||
:font-family (str text-font ", 'Georgia'")
|
||||
:margin-top (em 5)
|
||||
:text-align "justify"
|
||||
:flex 1
|
||||
:-webkit-flex 1
|
||||
}
|
||||
central-element]
|
||||
|
||||
(at-media {:screen true :min-width (px 1024)} [:article {:width width}])
|
||||
(at-media {:screen true :max-width (px 1023)} [:article {:width "90%"}])
|
||||
|
||||
[:.central-element central-element]
|
||||
|
||||
(at-media {:screen true :min-width (px 1024)} [:.central-element {:width width}])
|
||||
(at-media {:screen true :max-width (px 1023)} [:.central-element {:width "90%"}])
|
||||
|
||||
["article img" { :max-width "100%" }]
|
||||
|
||||
["article p" {
|
||||
:font-size (em (* 1.2 text-size-factor))
|
||||
:line-height "140%"
|
||||
}]
|
||||
|
||||
["article > h1:first-child" {
|
||||
:text-align "center"
|
||||
:font-size (em (* 2 header-size-factor))
|
||||
:margin (em 2)
|
||||
}]
|
||||
|
||||
[:.centered { :text-align "center" }]
|
||||
[:.bottom-space { :margin-bottom (em 7) }]
|
||||
[:code, :pre {
|
||||
:font-family "monospace"
|
||||
:background background-halftone
|
||||
:font-size (em (* 1.2 text-size-factor))
|
||||
}]
|
||||
|
||||
[:pre {
|
||||
:border-radius (px 3)
|
||||
:padding (em 0.5)
|
||||
:border (str "1px dotted" foreground-halftone)
|
||||
}]
|
||||
|
||||
["*:focus" { :outline "0px none transparent" }]
|
||||
(at-media {:screen true :min-width (px 1024)} [:textarea {:width width}])
|
||||
|
||||
[:textarea {
|
||||
:border-radius (px 5)
|
||||
:font-family "Courier"
|
||||
:font-size (em 1)
|
||||
:height (px 500)
|
||||
}]
|
||||
[:.hidden { :display "none" }]
|
||||
[:#dashed-line {
|
||||
:border-bottom (str "1px dashed" foreground-halftone)
|
||||
:margin-top (em 3)
|
||||
:margin-bottom (em 3)
|
||||
}]
|
||||
[:table {
|
||||
:width "100%"
|
||||
:border-collapse "collapse"
|
||||
}
|
||||
helvetica]
|
||||
[:th {
|
||||
:padding (em 0.3)
|
||||
:line-height (em 2.5)
|
||||
:background-color background-halftone
|
||||
}]
|
||||
[:td {
|
||||
:border-top (str "1px dotted" foreground-halftone)
|
||||
:padding (em 0.3)
|
||||
:line-height (em 2.5)
|
||||
}]
|
||||
[:.middot { :padding (em 0.5) }]
|
||||
|
||||
[:body { :display "-webkit-flex" }]
|
||||
|
||||
[:body {
|
||||
:display "flex"
|
||||
:min-height "100vh"
|
||||
:flex-direction "column"
|
||||
:-webkit-flex-direction "column"
|
||||
}]
|
||||
)))
|
||||
@@ -1,169 +0,0 @@
|
||||
(ns notehub.handler
|
||||
(:use
|
||||
compojure.core
|
||||
iokv.core
|
||||
notehub.views
|
||||
[clojure.string :rename {replace sreplace} :only [replace]])
|
||||
(:require
|
||||
[ring.adapter.jetty :as jetty]
|
||||
[clojure.core.cache :as cache]
|
||||
[hiccup.util :as util]
|
||||
[compojure.handler :as handler]
|
||||
[compojure.route :as route]
|
||||
[notehub.api :as api]
|
||||
[notehub.storage :as storage]
|
||||
[cheshire.core :refer :all])
|
||||
(:gen-class))
|
||||
|
||||
(defn current-timestamp []
|
||||
(quot (System/currentTimeMillis) 100000000))
|
||||
|
||||
; note page cache
|
||||
(def page-cache (atom (cache/lru-cache-factory {})))
|
||||
|
||||
; TODO: make sure the status is really set to the response!!!!
|
||||
(defn- response
|
||||
"Sets a custom message for each needed HTTP status.
|
||||
The message to be assigned is extracted with a dynamically generated key"
|
||||
[code]
|
||||
{:status code
|
||||
:body (let [message (get-message (keyword (str "status-" code)))]
|
||||
(layout :no-js {} message
|
||||
[:article [:h1 message]]))})
|
||||
|
||||
(defn redirect [url]
|
||||
{:status 302
|
||||
:headers {"Location" (str url)}
|
||||
:body ""})
|
||||
|
||||
(defn return-content-type [ctype content]
|
||||
{:headers {"Content-Type" ctype}
|
||||
:body content})
|
||||
|
||||
(defroutes api-routes
|
||||
(GET "/note" {params :params}
|
||||
(generate-string (api/version-manager api/get-note params)))
|
||||
|
||||
(POST "/note" {params :params}
|
||||
(generate-string (api/version-manager api/post-note params)))
|
||||
|
||||
(PUT "/note" {params :params}
|
||||
(generate-string (api/version-manager api/update-note params))))
|
||||
|
||||
(defroutes app-routes
|
||||
(GET "/api" [] (layout :no-js {} (get-message :api-title)
|
||||
[:article (md-to-html (slurp "API.md"))]))
|
||||
|
||||
(context "/api" []
|
||||
#(ring.util.response/content-type (api-routes %) "application/json"))
|
||||
|
||||
(GET "/" [] landing-page)
|
||||
|
||||
(POST "/propose-title" {body :body}
|
||||
(let [note (slurp body)]
|
||||
(return-content-type
|
||||
"text/plain; charset=utf-8"
|
||||
(api/propose-title note))))
|
||||
|
||||
(GET "/:year/:month/:day/:title/export" [year month day title]
|
||||
(when-let [md-text (:note (api/get-note {:noteID (api/build-key year month day title)}))]
|
||||
(return-content-type "text/plain; charset=utf-8" md-text)))
|
||||
|
||||
(GET "/:year/:month/:day/:title/stats" [year month day title]
|
||||
(let [note-id (api/build-key year month day title)]
|
||||
(statistics-page (api/derive-title (storage/get-note note-id))
|
||||
(storage/get-note-statistics note-id)
|
||||
(storage/get-publisher note-id))))
|
||||
|
||||
(GET "/:year/:month/:day/:title/edit" [year month day title]
|
||||
(let [note-id (api/build-key year month day title)]
|
||||
(note-update-page
|
||||
note-id
|
||||
(:note (api/get-note {:noteID note-id})))))
|
||||
|
||||
(GET "/new" [] (new-note-page
|
||||
(str
|
||||
(current-timestamp)
|
||||
(storage/sign (rand-int Integer/MAX_VALUE)))))
|
||||
|
||||
(GET "/:year/:month/:day/:title" [year month day title :as params]
|
||||
(let [params (assoc (:query-params params)
|
||||
:year year :month month :day day :title title)
|
||||
note-id (api/build-key year month day title)
|
||||
short-url (storage/create-short-url note-id params)]
|
||||
(when (storage/note-exists? note-id)
|
||||
(if (cache/has? @page-cache short-url)
|
||||
(do
|
||||
(swap! page-cache cache/hit short-url)
|
||||
(storage/increment-note-view note-id))
|
||||
(swap! page-cache cache/miss short-url
|
||||
(note-page (api/get-note {:noteID note-id})
|
||||
(api/url short-url)
|
||||
params)))
|
||||
(cache/lookup @page-cache short-url))))
|
||||
|
||||
(GET "/:short-url" [short-url]
|
||||
(when-let [params (storage/resolve-url short-url)]
|
||||
(let [{:keys [year month day title]} params
|
||||
rest-params (dissoc params :year :month :day :title)
|
||||
core-url (api/url year month day title)
|
||||
long-url (if (empty? rest-params) core-url (util/url core-url rest-params))]
|
||||
(redirect long-url))))
|
||||
|
||||
(POST "/post-note" [session note signature password]
|
||||
(if (and session
|
||||
(.startsWith session
|
||||
(str (current-timestamp)))
|
||||
(= signature (storage/sign session note)))
|
||||
(let [pid "NoteHub"
|
||||
psk (storage/get-psk pid)
|
||||
params {:note note
|
||||
:pid pid
|
||||
:signature (storage/sign pid psk note)
|
||||
:password password}]
|
||||
(if (storage/valid-publisher? pid)
|
||||
(let [resp (api/post-note params)]
|
||||
(if (get-in resp [:status :success])
|
||||
(redirect (:longURL resp))
|
||||
(response 400)))
|
||||
(response 500)))
|
||||
(response 400)))
|
||||
|
||||
(POST "/update-note" [noteID note password]
|
||||
(let [pid "NoteHub"
|
||||
psk (storage/get-psk pid)
|
||||
params {:noteID noteID :note note :password password :pid pid}]
|
||||
(if (storage/valid-publisher? pid)
|
||||
(let [resp (api/update-note (assoc params
|
||||
:signature
|
||||
(storage/sign pid psk noteID note password)))]
|
||||
(if (get-in resp [:status :success])
|
||||
(do
|
||||
(doseq [url (storage/get-short-urls noteID)]
|
||||
(swap! page-cache cache/evict url))
|
||||
(redirect (:longURL resp)))
|
||||
(response 403)))
|
||||
(response 500))))
|
||||
|
||||
(route/resources "/")
|
||||
(route/not-found (response 404)))
|
||||
|
||||
(def app
|
||||
(let [handler (handler/site app-routes)]
|
||||
(fn [request]
|
||||
(let [{:keys [server-name server-port]} request
|
||||
hostURL (str "https://" server-name
|
||||
(when (not= 80 server-port) (str ":" server-port)))
|
||||
request (assoc-in request [:params :hostURL] hostURL)]
|
||||
(if (get-setting :dev-mode)
|
||||
(handler request)
|
||||
(try (handler request)
|
||||
(catch Exception e
|
||||
(do
|
||||
;TODO (log e)
|
||||
(response 500)))))))))
|
||||
|
||||
(defn -main [& [port]]
|
||||
(jetty/run-jetty #'app
|
||||
{:port (if port (Integer/parseInt port) 8080)}))
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
(ns notehub.storage
|
||||
(:use [iokv.core]
|
||||
[zeus.core]
|
||||
[clojure.string :only (blank? replace) :rename {replace sreplace}])
|
||||
(:require [taoensso.carmine :as car :refer (wcar)]))
|
||||
|
||||
(def conn {:pool {} :spec {:uri (get-setting :db-url)}})
|
||||
|
||||
(let [md5Instance (java.security.MessageDigest/getInstance "MD5")]
|
||||
(defn sign
|
||||
"Returns the MD5 hash for the concatenation of all passed parameters"
|
||||
[& args]
|
||||
(let [input (sreplace (apply str args) #"\r+" "")]
|
||||
(do (.reset md5Instance)
|
||||
(.update md5Instance (.getBytes input))
|
||||
(apply str
|
||||
(map #(let [c (Integer/toHexString (bit-and 0xff %))]
|
||||
(if (= 1 (count c)) (str "0" c) c))
|
||||
(.digest md5Instance)))))))
|
||||
|
||||
(defmacro redis [cmd & body]
|
||||
`(car/wcar conn
|
||||
(~(symbol "car" (name cmd))
|
||||
~@body)))
|
||||
|
||||
(defn get-current-date []
|
||||
(.getTime (java.util.Date.)))
|
||||
|
||||
(defn valid-publisher? [pid]
|
||||
(= 1 (redis :hexists :publisher-key pid)))
|
||||
|
||||
(defn register-publisher [pid]
|
||||
"Returns nil if given PID exists or a PSK otherwise"
|
||||
(when (not (valid-publisher? pid))
|
||||
(let [psk (sign (str (rand-int Integer/MAX_VALUE) pid))]
|
||||
(redis :hset :publisher-key pid psk)
|
||||
psk)))
|
||||
|
||||
(when (and (get-setting :dev-mode)
|
||||
(not (valid-publisher? "NoteHub"))
|
||||
(register-publisher "NoteHub")))
|
||||
|
||||
(defn revoke-publisher [pid]
|
||||
(redis :hdel :publisher-key pid))
|
||||
|
||||
(defn get-psk [pid]
|
||||
(redis :hget :publisher-key pid))
|
||||
|
||||
(defn edit-note [noteID text]
|
||||
(redis :hset :edited noteID (get-current-date))
|
||||
(redis :hset :note noteID (zip text)))
|
||||
|
||||
(defn add-note
|
||||
([noteID text pid] (add-note noteID text pid nil))
|
||||
([noteID text pid passwd]
|
||||
(redis :hset :note noteID (zip text))
|
||||
(redis :hset :published noteID (get-current-date))
|
||||
(redis :hset :publisher noteID pid)
|
||||
(when (not (blank? passwd))
|
||||
(redis :hset :password noteID passwd))))
|
||||
|
||||
(defn valid-password? [noteID passwd]
|
||||
(let [stored (redis :hget :password noteID)]
|
||||
(and (not= 0 stored) (= stored passwd))))
|
||||
|
||||
(defn get-note-views [noteID]
|
||||
(redis :hget :views noteID))
|
||||
|
||||
(defn get-publisher [noteID]
|
||||
(redis :hget :publisher noteID))
|
||||
|
||||
(defn get-note-statistics [noteID]
|
||||
{:views (get-note-views noteID)
|
||||
:published (redis :hget :published noteID)
|
||||
:edited (redis :hget :edited noteID)})
|
||||
|
||||
(defn note-exists? [noteID]
|
||||
(= 1 (redis :hexists :note noteID)))
|
||||
|
||||
(defn get-note [noteID]
|
||||
(when (note-exists? noteID)
|
||||
(unzip (redis :hget :note noteID))))
|
||||
|
||||
(defn increment-note-view [noteID]
|
||||
(redis :hincrby :views noteID 1))
|
||||
|
||||
(defn short-url-exists? [url]
|
||||
(= 1 (redis :hexists :short-url url)))
|
||||
|
||||
(defn resolve-url [url]
|
||||
(let [value (redis :hget :short-url url)]
|
||||
(when value ; TODO: necessary?
|
||||
(read-string value))))
|
||||
|
||||
(defn delete-short-url [url]
|
||||
(when-let [params (redis :hget :short-url url)]
|
||||
(redis :hdel :short-url params)
|
||||
(redis :hdel :short-url url)))
|
||||
|
||||
(defn get-short-urls [noteID]
|
||||
(redis :smembers (str noteID :urls)))
|
||||
|
||||
(defn delete-note [noteID]
|
||||
(doseq [kw [:password :views :note :published :edited :publisher]]
|
||||
(redis :hdel kw noteID))
|
||||
(doseq [url (get-short-urls noteID)]
|
||||
(delete-short-url url))
|
||||
(redis :del (str noteID :urls)))
|
||||
|
||||
(defn create-short-url
|
||||
"Creates a short url for the given request metadata or extracts
|
||||
one if it was already created"
|
||||
[noteID params]
|
||||
(let [key (str (into (sorted-map) (clojure.walk/keywordize-keys params)))]
|
||||
(if (short-url-exists? key)
|
||||
(redis :hget :short-url key)
|
||||
(let [hash-stream (partition 5 (repeatedly #(rand-int 36)))
|
||||
hash-to-string (fn [hash]
|
||||
(apply str
|
||||
; map first 10 numbers to digits
|
||||
; and the rest to chars
|
||||
(map #(char (+ (if (< 9 %) 87 48) %)) hash)))
|
||||
url (first
|
||||
(remove short-url-exists?
|
||||
(map hash-to-string hash-stream)))]
|
||||
; we create two mappings: key params -> short url and back,
|
||||
; s.t. we can later easily check whether a short url already exists
|
||||
(redis :hset :short-url url key)
|
||||
(redis :hset :short-url key url)
|
||||
; we save all short urls of a note for removal later
|
||||
(redis :sadd (str noteID :urls) url)
|
||||
url))))
|
||||
|
||||
(defn gc [password dry]
|
||||
(println (get-setting :admin-pw))
|
||||
(when (= password (get-setting :admin-pw))
|
||||
(let [N 30
|
||||
timestamp (- (get-current-date) (* N 24 60 60 1000))
|
||||
all-notes (map first (partition 2 (redis :hgetall :note)))
|
||||
old-notes (filter #(< (Long/parseLong (redis :hget :published %)) timestamp) all-notes)
|
||||
unpopular-notes (filter #(< (try (Long/parseLong (redis :hget :views %))
|
||||
(catch Exception a 0)) N) old-notes)]
|
||||
(println "timestamp:" (str (java.util.Date. timestamp)))
|
||||
(doseq [note-id unpopular-notes]
|
||||
(do (println (if dry "to be deleted" "deleting") note-id)
|
||||
(when-not dry (delete-note note-id))))
|
||||
(println (count unpopular-notes) "deleted"))))
|
||||
@@ -1,124 +0,0 @@
|
||||
(ns notehub.views
|
||||
(:use
|
||||
iokv.core
|
||||
[clojure.string :rename {replace sreplace} :only [replace]]
|
||||
[hiccup.form]
|
||||
[hiccup.core]
|
||||
[hiccup.element]
|
||||
[hiccup.util :only [escape-html]]
|
||||
[hiccup.page :only [include-js html5]])
|
||||
(:require [notehub.css :as css])
|
||||
(:import (org.pegdown PegDownProcessor Extensions)))
|
||||
|
||||
(def get-message (get-map "messages"))
|
||||
|
||||
(def md-processor
|
||||
(PegDownProcessor. (int (bit-and-not Extensions/ALL Extensions/HARDWRAPS))))
|
||||
|
||||
(defn md-to-html [md-text]
|
||||
(.markdownToHtml md-processor md-text))
|
||||
|
||||
; Creates the main html layout
|
||||
(defn layout
|
||||
[js? style title & content]
|
||||
(html5
|
||||
[:head
|
||||
[:title (print-str (get-message :name) "—" title)]
|
||||
[:meta {:charset "UTF-8"}]
|
||||
[:meta {:name "viewport" :content "width=device-width, initial-scale=1.0"}]
|
||||
[:link {:rel "stylesheet" :type "text/css"
|
||||
:href
|
||||
(format "https://fonts.googleapis.com/css?family=PT+Serif:700|Noticia+Text:700%s&subset=latin,cyrillic"
|
||||
(reduce (fn [acc e]
|
||||
(if-let [font (style e)]
|
||||
(str acc "|" (sreplace font #" " "+"))
|
||||
acc)) "" ["text-font" "header-font"]))}]
|
||||
[:style (css/generate style)]
|
||||
(if (= :js js?)
|
||||
(html
|
||||
(include-js "//cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js")
|
||||
(include-js "//cdnjs.cloudflare.com/ajax/libs/blueimp-md5/1.0.1/js/md5.min.js")
|
||||
(include-js "/js/publishing.js")
|
||||
[:body {:onload "onLoad()"} content])
|
||||
[:body content])]))
|
||||
|
||||
(defn- sanitize
|
||||
"Breakes all usages of <script> & <iframe>"
|
||||
[input]
|
||||
(sreplace input #"(</?(iframe|script).*?>|javascript:)" ""))
|
||||
|
||||
; input form for the markdown text with a preview area
|
||||
(defn- input-form [form-url command fields content passwd-msg]
|
||||
(let [css-class (when (= :publish command) :hidden)]
|
||||
(layout :js {} (get-message :new-page)
|
||||
[:article#preview {:style "flex: none; -webkit-flex: none"} ""]
|
||||
[:div#dashed-line {:class css-class}]
|
||||
[:div.central-element.helvetica {:style "margin-bottom: 3em"}
|
||||
(form-to {:autocomplete :off} [:post form-url]
|
||||
(hidden-field :action command)
|
||||
(hidden-field :password)
|
||||
fields
|
||||
(text-area :note content)
|
||||
[:fieldset#input-elems {:class css-class}
|
||||
(text-field {:class "ui-elem"
|
||||
:placeholder (get-message passwd-msg)}
|
||||
:plain-password) " "
|
||||
(submit-button {:class "button ui-elem"
|
||||
:id :publish-button} (get-message command))
|
||||
[:br]
|
||||
[:br]
|
||||
[:div#proposed-title {:style "color: #aaa; font-size: 0.8em"}]])])))
|
||||
|
||||
(def landing-page
|
||||
(layout :no-js {} (get-message :page-title)
|
||||
[:div#hero
|
||||
[:h1 (get-message :name)]
|
||||
[:h2 (get-message :title)]
|
||||
[:br]
|
||||
[:a.landing-button {:href "/new" :style "color: white"} (get-message :new-page)]]
|
||||
[:div#dashed-line]
|
||||
[:article.helvetica.bottom-space
|
||||
{:style "font-size: 1em"}
|
||||
(md-to-html (slurp "LANDING.md"))]
|
||||
[:div#footer (md-to-html (get-message :footer))]))
|
||||
|
||||
|
||||
(defn statistics-page [note-title stats publisher]
|
||||
(let [page-title (get-message :statistics)
|
||||
info (assoc stats :publisher publisher)]
|
||||
(layout :no-js {} page-title
|
||||
[:div.central-element.helvetica {:style "width: 80%"}
|
||||
[:h2 note-title]
|
||||
[:h3.helvetica page-title]
|
||||
[:table#stats
|
||||
(map
|
||||
#(when-let [v (% info)]
|
||||
[:tr
|
||||
[:td (str (get-message %) ":")]
|
||||
[:td (if (or (= % :published) (= % :edited))
|
||||
(str (java.util.Date. (Long/parseLong v))) v)]])
|
||||
[:published :edited :publisher :views])]])))
|
||||
|
||||
(defn note-update-page [note-id note]
|
||||
(input-form "/update-note"
|
||||
:update
|
||||
(html (hidden-field :noteID note-id))
|
||||
note
|
||||
:enter-passwd))
|
||||
|
||||
(defn new-note-page [session]
|
||||
(input-form "/post-note" :publish
|
||||
(html (hidden-field :session session)
|
||||
(hidden-field {:id :signature} :signature))
|
||||
(get-message :loading) :set-passwd))
|
||||
|
||||
(defn note-page [note short-url css-params]
|
||||
(layout :no-js css-params (:title note)
|
||||
[:article.bottom-space (md-to-html (sanitize (:note note)))]
|
||||
(let [urls {:short-url short-url
|
||||
:notehub "/"}
|
||||
links (map #(link-to
|
||||
(urls % (str (:longURL note) "/" (name %)))
|
||||
(get-message %))
|
||||
[:notehub :stats :edit :export :short-url])]
|
||||
[:div#footer (interpose [:span.middot "·"] links)])))
|
||||
Reference in New Issue
Block a user