Browse Source

first api version implemented

master
Christian Mueller 12 years ago
parent
commit
f39ae0a593
  1. 5
      API.md
  2. 1
      messages
  3. 97
      src/NoteHub/api.clj
  4. 53
      src/NoteHub/storage.clj
  5. 19
      src/NoteHub/views/pages.clj
  6. 33
      test/NoteHub/test/api.clj
  7. 1
      test/NoteHub/test/storage.clj
  8. 1
      test/NoteHub/test/views/pages.clj

5
API.md

@ -4,7 +4,7 @@
## Prerequisites ## 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. 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", shortURL: "http://notehub.org/0vrcp",
statistics: { statistics: {
published: "2014-1-3", published: "2014-1-3",
edited: "2014-1-12",
views: 24 views: 24
}, },
status: { 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. 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 ## Note Creation

1
messages

@ -16,6 +16,7 @@ enter-passwd = Password
publish = Publish publish = Publish
update = Save update = Save
published = Published published = Published
edited = Edited
article-views = Article Views article-views = Article Views
statistics = Statistics statistics = Statistics
stats = statistics stats = statistics

97
src/NoteHub/api.clj

@ -1,12 +1,37 @@
(ns NoteHub.api (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 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 (defn- create-response
([success] { :success success }) ([success] { :success success })
([success message] ([success message & params]
(assoc (create-response success) :message message))) (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")] (let [md5Instance (java.security.MessageDigest/getInstance "MD5")]
(defn get-signature (defn get-signature
@ -17,9 +42,63 @@
(.update md5Instance (.getBytes input)) (.update md5Instance (.getBytes input))
(.toString (new java.math.BigInteger 1 (.digest md5Instance)) 16))))) (.toString (new java.math.BigInteger 1 (.digest md5Instance)) 16)))))
(defn get-note [noteID]) (defn get-note [noteID]
(defn post-note [& args]) {:note (storage/get-note noteID)
(defn update-note [& args]) :longURL (getURL noteID)
(defn register-publisher [& args]) :shortURL (getURL noteID :short)
(defn revoke-publisher [& args]) :statistics (storage/get-note-statistics noteID)
(defn valid-publisher? [& args]) :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))})))

53
src/NoteHub/storage.clj

@ -11,12 +11,34 @@
(when-not (dev-mode?) (when-not (dev-mode?)
{:url (get-setting :db-url)}))) {:url (get-setting :db-url)})))
(defn get-current-date []
(str (java.util.Date.)))
; DB hierarchy levels ; DB hierarchy levels
(def note "note") (def note "note")
(def published "published")
(def edited "edited")
(def views "views") (def views "views")
(def password "password") (def password "password")
(def sessions "sessions") (def sessions "sessions")
(def short-url "short-url") (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 (defn create-session
[] []
@ -36,6 +58,7 @@
[noteID text passwd] [noteID text passwd]
(let [stored-password (redis/hget db password noteID)] (let [stored-password (redis/hget db password noteID)]
(when (and stored-password (= passwd stored-password)) (when (and stored-password (= passwd stored-password))
(redis/hset db edited noteID (get-current-date))
(redis/hset db note noteID text)))) (redis/hset db note noteID text))))
(defn add-note (defn add-note
@ -43,6 +66,7 @@
([noteID text passwd] ([noteID text passwd]
(do (do
(redis/hset db note noteID text) (redis/hset db note noteID text)
(redis/hset db published noteID (get-current-date))
(when (not (blank? passwd)) (when (not (blank? passwd))
(redis/hset db password noteID passwd))))) (redis/hset db password noteID passwd)))))
@ -55,17 +79,21 @@
text)))) text))))
(defn get-note-views (defn get-note-views
"Returns the number of views for the specified noteID"
[noteID] [noteID]
(redis/hget db views 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? (defn note-exists?
"Returns true if the note with the specified noteID"
[noteID] [noteID]
(redis/hexists db note noteID)) (redis/hexists db note noteID))
(defn delete-note (defn delete-note
"Deletes the note with the specified coordinates"
[noteID] [noteID]
(doseq [kw [password views note]] (doseq [kw [password views note]]
; TODO: delete short url by looking for the title ; TODO: delete short url by looking for the title
@ -76,6 +104,9 @@
[url] [url]
(redis/hexists db short-url url)) (redis/hexists db short-url url))
(defn get-short-url [noteID]
(redis/hget db short-url noteID))
(defn resolve-url (defn resolve-url
"Resolves short url by providing all metadata of the request" "Resolves short url by providing all metadata of the request"
[url] [url]
@ -92,12 +123,12 @@
(redis/hdel db short-url value)))) (redis/hdel db short-url value))))
(defn create-short-url (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" one if it was already created"
[metadata] [arg]
(let [request (str (into (sorted-map) metadata))] (let [key (if (map? arg) (str (into (sorted-map) arg)) arg)]
(if (short-url-exists? request) (if (short-url-exists? key)
(redis/hget db short-url request) (redis/hget db short-url key)
(let [hash-stream (partition 5 (repeatedly #(rand-int 36))) (let [hash-stream (partition 5 (repeatedly #(rand-int 36)))
hash-to-string (fn [hash] hash-to-string (fn [hash]
(apply str (apply str
@ -108,8 +139,8 @@
(remove short-url-exists? (remove short-url-exists?
(map hash-to-string hash-stream)))] (map hash-to-string hash-stream)))]
(do (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 ; 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 url key)
(redis/hset db short-url request url) (redis/hset db short-url key url)
url))))) url)))))

19
src/NoteHub/views/pages.clj

@ -1,7 +1,8 @@
(ns NoteHub.views.pages (ns NoteHub.views.pages
(:require [hiccup.util :as util]) (:require [hiccup.util :as util])
(:use (:use
[NoteHub.storage] [NoteHub.storage] ; TODO: delete this
[NoteHub.api :only [build-key get-date]]
[NoteHub.settings] [NoteHub.settings]
[NoteHub.views.common] [NoteHub.views.common]
[clojure.string :rename {replace sreplace} [clojure.string :rename {replace sreplace}
@ -12,15 +13,7 @@
[hiccup.element] [hiccup.element]
[noir.response :only [redirect status content-type]] [noir.response :only [redirect status content-type]]
[noir.core :only [defpage defpartial]] [noir.core :only [defpage defpartial]]
[noir.statuses]) [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))
(defn get-hash (defn get-hash
"A simple hash-function, which computes a hash from the text field "A simple hash-function, which computes a hash from the text field
@ -66,12 +59,6 @@
links (interpose separator links)] links (interpose separator links)]
[:div#panel (map identity 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 ; Routes
; ====== ; ======

33
test/NoteHub/test/api.clj

@ -1,5 +1,8 @@
(ns NoteHub.test.api (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 note "Hello world, this is a test note!")
(def note2 "Another test note") (def note2 "Another test note")
@ -9,37 +12,37 @@
(defmacro isnt [arg] `(is (not ~arg))) (defmacro isnt [arg] `(is (not ~arg)))
(defn register-publisher-fixture [f] (defn register-publisher-fixture [f]
(def psk (register-publisher pid)) (def psk (storage/register-publisher pid))
(f) (f)
(revoke-publisher pid)) (storage/revoke-publisher pid))
#_
(deftest api (deftest api
(testing "API" (testing "API"
(testing "publisher registration" (testing "publisher registration"
(let [psk2 (register-publisher pid2)] (let [psk2 (storage/register-publisher pid2)]
(is (valid-publisher? pid)) (is (storage/valid-publisher? pid))
(is (valid-publisher? pid2)) (is (storage/valid-publisher? pid2))
(is (revoke-publisher pid2)) (is (storage/revoke-publisher pid2))
(isnt (revoke-publisher "anyPID")) (isnt (storage/revoke-publisher "anyPID"))
(isnt (valid-publisher? "any_PID")) (isnt (storage/valid-publisher? "any_PID"))
(isnt (valid-publisher? pid2)))) (isnt (storage/valid-publisher? pid2))))
(testing "note publishing & retrieval" #_ (testing "note publishing & retrieval"
(let [post-response (post-note note pid (get-signature pid psk note)) (let [post-response (post-note note pid (get-signature pid psk note))
get-response (get-note (:noteID post-response))] get-response (get-note (:noteID post-response))]
(is (:success (:status post-response))) (is (:success (:status post-response)))
(is (:success (:status get-response))) (is (:success (:status get-response)))
(is (= note (:note get-response))) (is (= note (:note get-response)))
; TODO: test all response fields!!!!
(is (= (:longURL post-response) (:longURL get-response))) (is (= (:longURL post-response) (:longURL get-response)))
(is (= (:shortURL post-response) (:shortURL 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 psk note)))))
(isnt (:success (:status (post-note note pid (get-signature pid "random_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))))) (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 (: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))))))) (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") (let [post-response (post-note note pid (get-signature pid psk note) "passwd")
note-id (:noteID post-response) note-id (:noteID post-response)
get-response (get-note note-id) get-response (get-note note-id)

1
test/NoteHub/test/storage.clj

@ -1,5 +1,6 @@
(ns NoteHub.test.storage (ns NoteHub.test.storage
(:use [NoteHub.storage] (:use [NoteHub.storage]
[NoteHub.api :only [build-key]]
[NoteHub.views.pages] [NoteHub.views.pages]
[clojure.test])) [clojure.test]))

1
test/NoteHub/test/views/pages.clj

@ -1,5 +1,6 @@
(ns NoteHub.test.views.pages (ns NoteHub.test.views.pages
(:use [NoteHub.views.pages] (:use [NoteHub.views.pages]
[NoteHub.api :only [build-key get-date]]
[noir.util.test] [noir.util.test]
[NoteHub.views.common :only [url]] [NoteHub.views.common :only [url]]
[NoteHub.storage] [NoteHub.storage]

Loading…
Cancel
Save