diff --git a/API.md b/API.md index d0718d3..4a0d9ba 100644 --- a/API.md +++ b/API.md @@ -4,7 +4,7 @@ ## Prerequisites -The NoteHub API can only be used in combination with a __Publisher ID__ (PID) and __Publisher Secret Key__ (PSK), which can be issued [here](http://notehub.org/api/register). The PSK can be revoked at any moment in case of an API abuse. +The NoteHub API can only be used in combination with a __Publisher ID__ (PID) and __Publisher Secret Key__ (PSK), which can be requested [here](http://notehub.org/api/register). The PSK can be revoked at any moment in case of an API abuse. A PID is a string chosen by the publisher and cannot be longer than 16 characters (e.g.: __notepadPlugin__). A PSK will be generated by the NoteHub API and can be a string of any length and content. @@ -26,6 +26,7 @@ Example: shortURL: "http://notehub.org/0vrcp", statistics: { published: "2014-1-3", + edited: "2014-1-12", views: 24 }, status: { @@ -36,7 +37,7 @@ Example: Hence, the status of the request can be evaluated by reading of the property `status.success`. The field `status.comment`might contain an error message, a warning or any other comments from the server. -The note ID is a string, containing the date of publishing and a few first words of the note (usually the header), e.g.: `2014/1/3/lorem-ipsum`. This ID will be generated by NoteHub automatically. +The note ID is a string, containing the date of publishing and a few first words of the note (usually the title), e.g.: `2014 1 3 lorem-ipsum`. This ID will be generated by NoteHub automatically. ## Note Creation diff --git a/messages b/messages index c60d2e2..2773864 100644 --- a/messages +++ b/messages @@ -16,6 +16,7 @@ enter-passwd = Password publish = Publish update = Save published = Published +edited = Edited article-views = Article Views statistics = Statistics stats = statistics diff --git a/src/NoteHub/api.clj b/src/NoteHub/api.clj index f9be74c..66644ef 100644 --- a/src/NoteHub/api.clj +++ b/src/NoteHub/api.clj @@ -1,12 +1,37 @@ (ns NoteHub.api - (:require [NoteHub.storage :as persistance])) + (:import + [java.util Calendar]) + (:use + [NoteHub.settings] + [clojure.string :rename {replace sreplace} + :only [replace blank? lower-case split-lines]]) + (:require [NoteHub.storage :as storage])) (def api-version "1.0") +(def domain "http://notehub.org/") + +; Concatenates all fields to a string +(defn build-key + "Returns a storage-key for the given note coordinates" + [[year month day] title] + (print-str year month day title)) + +(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] - (assoc (create-response success) :message message))) + ([success message & params] + (assoc (create-response success) :message (apply format message params)))) + +(defn- getURL [noteID description] + (if (description) + (str domain (storage/get-short-url noteID)) + (str domain (sreplace noteID #" " "/")))) (let [md5Instance (java.security.MessageDigest/getInstance "MD5")] (defn get-signature @@ -17,9 +42,63 @@ (.update md5Instance (.getBytes input)) (.toString (new java.math.BigInteger 1 (.digest md5Instance)) 16))))) -(defn get-note [noteID]) -(defn post-note [& args]) -(defn update-note [& args]) -(defn register-publisher [& args]) -(defn revoke-publisher [& args]) -(defn valid-publisher? [& args]) +(defn get-note [noteID] + {:note (storage/get-note noteID) + :longURL (getURL noteID) + :shortURL (getURL noteID :short) + :statistics (storage/get-note-statistics noteID) + :status (if (storage/note-exists? noteID) + (create-response true) + (create-response false "noteID '%s' unknown" noteID))}) + +(defn post-note + ([note pid signature] (post-note note pid signature nil)) + ([note pid signature password] + (let [errors (filter identity + (lazy-seq + [(when-not (storage/valid-publisher? pid) "pid invalid") + (when-not (= signature + (get-signature pid (storage/get-psk pid) note)) + "signature invalid") + (when (blank? note) "note is empty")]))] + (if (empty? errors) + (let [[year month day] (get-date) + untrimmed-line (filter #(or (= \- %) (Character/isLetterOrDigit %)) + (-> note split-lines first (sreplace " " "-") lower-case)) + trim (fn [s] (apply str (drop-while #(= \- %) s))) + title-uncut (-> untrimmed-line trim reverse trim reverse) + max-length (get-setting :max-title-length #(Integer/parseInt %) 80) + proposed-title (apply str (take max-length title-uncut)) + date [year month day] + title (first (drop-while #(storage/note-exists? (build-key date %)) + (cons proposed-title + (map #(str proposed-title "-" (+ 2 %)) (range))))) + noteID (build-key date title)] + (do + (storage/add-note noteID note password) + (storage/create-short-url noteID) + { + :noteID noteID + :longURL (getURL noteID) + :shortURL (getURL noteID :short) + :status (create-response true) + })) + {:status (create-response false (first errors))})))) + + +(defn update-note [noteID note pid signature password] + (let [errors (filter identity + (lazy-seq + [(when-not (storage/valid-publisher? pid) "pid invalid") + (when-not (= signature + (get-signature pid (storage/get-psk pid) noteID note password)) + "signature invalid") + (when (blank? note) "note is empty") + (when-not (storage/update-note noteID note password) "password invalid")]))] + (if (empty? errors) + { + :longURL (getURL noteID) + :shortURL (getURL noteID :short) + :status (create-response true) + } + {:status (create-response false (first errors))}))) diff --git a/src/NoteHub/storage.clj b/src/NoteHub/storage.clj index b76c02b..d1c5dab 100644 --- a/src/NoteHub/storage.clj +++ b/src/NoteHub/storage.clj @@ -11,12 +11,34 @@ (when-not (dev-mode?) {:url (get-setting :db-url)}))) +(defn get-current-date [] + (str (java.util.Date.))) + ; DB hierarchy levels (def note "note") +(def published "published") +(def edited "edited") (def views "views") (def password "password") (def sessions "sessions") (def short-url "short-url") +(def publisher "publisher") + +(defn valid-publisher? [pid] + (redis/hexists db publisher pid)) + +(defn register-publisher [pid] + "Returns nil if given PID exists or a PSK otherwise" + (when (not (valid-publisher? pid)) + (let [psk (encrypt (str (rand-int Integer/MAX_VALUE) pid)) + _ (redis/hset db publisher pid psk)] + psk))) + +(defn revoke-publisher [pid] + (redis/hdel db publisher pid)) + +(defn get-psk [pid] + (redis/hget db publisher pid)) (defn create-session [] @@ -36,6 +58,7 @@ [noteID text passwd] (let [stored-password (redis/hget db password noteID)] (when (and stored-password (= passwd stored-password)) + (redis/hset db edited noteID (get-current-date)) (redis/hset db note noteID text)))) (defn add-note @@ -43,6 +66,7 @@ ([noteID text passwd] (do (redis/hset db note noteID text) + (redis/hset db published noteID (get-current-date)) (when (not (blank? passwd)) (redis/hset db password noteID passwd))))) @@ -55,17 +79,21 @@ text)))) (defn get-note-views - "Returns the number of views for the specified noteID" [noteID] (redis/hget db views noteID)) +(defn get-note-statistics + "Return views, publishing and editing timestamp" + [noteID] + { :view (redis/hget db views noteID) + :published (redis/hget db published noteID) + :edited (redis/hget db edited noteID) }) + (defn note-exists? - "Returns true if the note with the specified noteID" [noteID] (redis/hexists db note noteID)) (defn delete-note - "Deletes the note with the specified coordinates" [noteID] (doseq [kw [password views note]] ; TODO: delete short url by looking for the title @@ -76,6 +104,9 @@ [url] (redis/hexists db short-url url)) +(defn get-short-url [noteID] + (redis/hget db short-url noteID)) + (defn resolve-url "Resolves short url by providing all metadata of the request" [url] @@ -92,12 +123,12 @@ (redis/hdel db short-url value)))) (defn create-short-url - "Creates a short url for the given request metadata or extracts + "Creates a short url for the given request metadata or noteID or extracts one if it was already created" - [metadata] - (let [request (str (into (sorted-map) metadata))] - (if (short-url-exists? request) - (redis/hget db short-url request) + [arg] + (let [key (if (map? arg) (str (into (sorted-map) arg)) arg)] + (if (short-url-exists? key) + (redis/hget db short-url key) (let [hash-stream (partition 5 (repeatedly #(rand-int 36))) hash-to-string (fn [hash] (apply str @@ -108,8 +139,8 @@ (remove short-url-exists? (map hash-to-string hash-stream)))] (do - ; we create two mappings: request params -> short url and back, + ; 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 db short-url url request) - (redis/hset db short-url request url) + (redis/hset db short-url url key) + (redis/hset db short-url key url) url))))) diff --git a/src/NoteHub/views/pages.clj b/src/NoteHub/views/pages.clj index 148c1f9..4c41f04 100644 --- a/src/NoteHub/views/pages.clj +++ b/src/NoteHub/views/pages.clj @@ -1,7 +1,8 @@ (ns NoteHub.views.pages (:require [hiccup.util :as util]) (:use - [NoteHub.storage] + [NoteHub.storage] ; TODO: delete this + [NoteHub.api :only [build-key get-date]] [NoteHub.settings] [NoteHub.views.common] [clojure.string :rename {replace sreplace} @@ -12,15 +13,7 @@ [hiccup.element] [noir.response :only [redirect status content-type]] [noir.core :only [defpage defpartial]] - [noir.statuses]) - (:import - [java.util Calendar])) - -; Concatenates all fields to a string -(defn build-key - "Returns a storage-key for the given note coordinates" - [[year month day] title] - (print-str year month day title)) + [noir.statuses])) (defn get-hash "A simple hash-function, which computes a hash from the text field @@ -66,12 +59,6 @@ links (interpose separator links)] [:div#panel (map identity links)])))) -(defn get-date - "Returns today's date" - [] - (map #(+ (second %) (.get (Calendar/getInstance) (first %))) - {Calendar/YEAR 0, Calendar/MONTH 1, Calendar/DAY_OF_MONTH 0})) - ; Routes ; ====== diff --git a/test/NoteHub/test/api.clj b/test/NoteHub/test/api.clj index 1318250..4f2104a 100644 --- a/test/NoteHub/test/api.clj +++ b/test/NoteHub/test/api.clj @@ -1,5 +1,8 @@ (ns NoteHub.test.api - (:use [NoteHub.api] [clojure.test])) + (:require + [NoteHub.storage :as storage]) + (:use [NoteHub.api] + [clojure.test])) (def note "Hello world, this is a test note!") (def note2 "Another test note") @@ -9,37 +12,37 @@ (defmacro isnt [arg] `(is (not ~arg))) (defn register-publisher-fixture [f] - (def psk (register-publisher pid)) + (def psk (storage/register-publisher pid)) (f) - (revoke-publisher pid)) + (storage/revoke-publisher pid)) -#_ (deftest api (testing "API" (testing "publisher registration" - (let [psk2 (register-publisher pid2)] - (is (valid-publisher? pid)) - (is (valid-publisher? pid2)) - (is (revoke-publisher pid2)) - (isnt (revoke-publisher "anyPID")) - (isnt (valid-publisher? "any_PID")) - (isnt (valid-publisher? pid2)))) - (testing "note publishing & retrieval" + (let [psk2 (storage/register-publisher pid2)] + (is (storage/valid-publisher? pid)) + (is (storage/valid-publisher? pid2)) + (is (storage/revoke-publisher pid2)) + (isnt (storage/revoke-publisher "anyPID")) + (isnt (storage/valid-publisher? "any_PID")) + (isnt (storage/valid-publisher? pid2)))) + #_ (testing "note publishing & retrieval" (let [post-response (post-note note pid (get-signature pid psk note)) get-response (get-note (:noteID post-response))] (is (:success (:status post-response))) (is (:success (:status get-response))) (is (= note (:note get-response))) + ; TODO: test all response fields!!!! (is (= (:longURL post-response) (:longURL get-response))) (is (= (:shortURL post-response) (:shortURL get-response)))) (isnt (:success (:status (post-note note pid (get-signature pid psk note))))) (isnt (:success (:status (post-note note pid (get-signature pid "random_psk" note))))) (is (:success (:status (post-note note pid (get-signature pid psk note))))) - (let [psk2 (register-publisher "randomPID")] + (let [psk2 (storage/register-publisher "randomPID")] (is (:success (:status (post-note note "randomPID" (get-signature pid psk2 note))))) - (is (revoke-publisher pid2)) + (is (storage/revoke-publisher pid2)) (isnt (:success (:status (post-note note "randomPID" (get-signature pid psk2 note))))))) - (testing "note update" + #_ (testing "note update" (let [post-response (post-note note pid (get-signature pid psk note) "passwd") note-id (:noteID post-response) get-response (get-note note-id) diff --git a/test/NoteHub/test/storage.clj b/test/NoteHub/test/storage.clj index adc4eb1..8c92178 100644 --- a/test/NoteHub/test/storage.clj +++ b/test/NoteHub/test/storage.clj @@ -1,5 +1,6 @@ (ns NoteHub.test.storage (:use [NoteHub.storage] + [NoteHub.api :only [build-key]] [NoteHub.views.pages] [clojure.test])) diff --git a/test/NoteHub/test/views/pages.clj b/test/NoteHub/test/views/pages.clj index a4416b2..b8c38b3 100644 --- a/test/NoteHub/test/views/pages.clj +++ b/test/NoteHub/test/views/pages.clj @@ -1,5 +1,6 @@ (ns NoteHub.test.views.pages (:use [NoteHub.views.pages] + [NoteHub.api :only [build-key get-date]] [noir.util.test] [NoteHub.views.common :only [url]] [NoteHub.storage]