Browse Source

Merge branch 'node'

master
Christian Müller 10 years ago
parent
commit
0cc086b1c7
  1. 18
      .gitignore
  2. 42
      .vscode/launch.json
  3. 147
      API.md
  4. 19
      LANDING.md
  5. 1
      Procfile
  6. 8
      README.md
  7. 10
      jsconfig.json
  8. 28
      messages
  9. 34
      package.json
  10. 26
      project.clj
  11. 35
      resources/edit.html
  12. 6
      resources/footer.html
  13. 121
      resources/public/api-test.html
  14. 37
      resources/public/index.html
  15. 56
      resources/public/js/publishing.js
  16. 284
      resources/public/style.css
  17. 15
      resources/template.html
  18. 10
      scripts/expose.sh
  19. 9
      scripts/run.sh
  20. 112
      server.js
  21. 68
      src/migrate.js
  22. 126
      src/notehub/api.clj
  23. 260
      src/notehub/css.clj
  24. 169
      src/notehub/handler.clj
  25. 155
      src/notehub/storage.clj
  26. 124
      src/notehub/views.clj
  27. 58
      src/storage.js
  28. 41
      src/view.js
  29. 1
      system.properties
  30. 214
      test/notehub/test/api.clj
  31. 129
      test/notehub/test/handler.clj
  32. 70
      test/notehub/test/storage.clj

18
.gitignore vendored

@ -1,14 +1,6 @@
Makefile
PrivateMakefile
dump.rdb dump.rdb
resources/public/cljs bin/
pom.xml node_modules/
*jar npm-debug.log
/lib/ database.sqlite
/classes/ database.sqlite-journal
.lein-*
.crossover-cljs
target/
.nrepl-port
.idea/
NoteHub.iml

42
.vscode/launch.json vendored

@ -0,0 +1,42 @@
{
"version": "0.1.0",
// List of configurations. Add new configurations or edit existing ones.
// ONLY "node" and "mono" are supported, change "type" to switch.
"configurations": [
{
// Name of configuration; appears in the launch configuration drop down menu.
"name": "Launch storage.js",
// Type of configuration. Possible values: "node", "mono".
"type": "node",
// Workspace relative or absolute path to the program.
"program": "bin/storage.js",
// Automatically stop program after launch.
"stopOnEntry": false,
// Command line arguments passed to the program.
"args": [],
// Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.
"cwd": ".",
// Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH.
"runtimeExecutable": null,
// Optional arguments passed to the runtime executable.
"runtimeArgs": ["--nolazy"],
// Environment variables passed to the program.
"env": {
"NODE_ENV": "development"
},
// Use JavaScript source maps (if they exist).
"sourceMaps": false,
// If JavaScript source maps are enabled, the generated code is expected in this directory.
"outDir": null
},
{
"name": "Attach",
"type": "node",
// TCP/IP address. Default is "localhost".
"address": "localhost",
// Port to attach to.
"port": 5858,
"sourceMaps": false
}
]
}

147
API.md

@ -1,147 +0,0 @@
# NoteHub API
**Version 1.4, status: released.**
## Changelog
- **V1.4**: Bugfix: no whitespace elimination from the note text is needed now for the signature computation.
- **V1.3**: New note ID format.
- **V1.2**: Theme & font settings can be specified during the publishing.
- **V1.1**: Fields `publisher` and `title` in the response to the note retrieval.
- **V1.0**: Initial release.
## Prerequisites
The NoteHub API can only be used in combination with a __Publisher ID__ (PID) and __Publisher Secret Key__ (PSK), which can be requested [here](#registration). 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.
All API requests must be issued with one special parameter `version` denoting the expected version of the API as a string, e.g. `1.0` (see examples below). You should always put the version of this document as a `version` parameter.
Once you obtained your PSK, you can test the API [here](/api-test.html).
## <a name="registration"></a>NoteHub API Access Request
To register as a publisher and gain access to NoteHub API, please <a href="mailto:notehub@icloud.com?subject=NoteHub API Access Request&body=Please add [a] desired PID as a 16 char string [b] your contact information, [c] short usage explanation and [d] the URL of the resource or it's website.">send</a> an email with the following information about you: the desired PID, your contact information, a short description of what you want to do and an URL of the resource where the API will be used or its website.
## Note Retrieval
A simple `GET` request to the following URL:
https://www.notehub.org/api/note
with the following parameters:
Parameter | Explanation | Type
--- | --- | ---
`noteID` | Note-ID | **required**
`version` | Used API version | **required**
will return a JSON object containing following self explaining fields: `note`, `title`, `longURL`, `shortURL`, `statistics`, `status`, `publisher`.
Example:
{
note: "markdown source",
title: "Lorem Ipsum.",
longURL: "https://www.notehub.org/2014/1/3/lorem-ipsum",
shortURL: "https://www.notehub.org/0vrcp",
statistics: {
published: "1396250865735",
edited: "1412516289863",
views: 24
},
status: {
success: true,
comment: "some server message"
},
publisher: "Publisher Description"
}
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 title), e.g.: `"2014/1/3/lorem-ipsum"`. This ID will be generated by NoteHub automatically.
## Note Publishing
A note must be created by a `POST` request to the following URL:
https://www.notehub.org/api/note
with the following parameters:
Parameter | Explanation | Type
--- | --- | ---
`note` | Text to publish | **required**
`pid` | Publisher ID | **required**
`signature` | Signature | **required**
`password` | Secret token (plain or hashed) | *optional*
`version` | Used API version | **required**
`theme` | Theme name | *optional*
`text-size` | Text size | *optional*
`header-size`| Header size | *optional*
`text-font` | Text font name | *optional*
`header-font`| Header font name | *optional*
The Signature is the MD5 hash of the following string concatenation:
pid + psk + note
The signature serves as a proof, that the request is authentic and will be issued by the publisher corresponding to the provided PID. Please note, that _all_ of the values used in the signature computation, should be identical to the values passed with the request itself.
Ensure, that your note contains only `\n` symbols as line breaks!
The parameters specifying the theme name and fonts are optional and only impact the URLs returned back.
The response of the server will contain the fields `noteID`, `longURL`, `shortURL`, `status`.
Example:
{
noteID: "2014/1/3/lorem-ipsum",
longURL: "https://www.notehub.org/2014/1/3/lorem-ipsum",
shortURL: "https://www.notehub.org/0vrcp",
status: {
success: true,
comment: "some server message"
}
}
The status object serves the same purpose as in the case of note retrieval.
## Note Update
To update a note, an `PUT` request must be issued to the following URL:
https://www.notehub.org/api/note
with the following parameters:
Parameter | Explanation | Type
--- | --- | ---
`noteID` | Note-ID | **required**
`note` | New text | **required**
`pid` | Publisher ID | **required**
`signature` | Signature | **required**
`password` | Secret token (plain or hashed) | **required**
`version` | Used API version | **required**
The Signature is the MD5 hash of the following string concatenation:
pid + psk + noteID + note + password
Please note, that all of the values used in the signature computation, should be identical to the values passed with the request itself.
Ensure, that your note contains only `\n` symbols as line breaks!
The response of the server will contain the fields `longURL`, `shortURL`, `status`.
Example:
{
longURL: "https://www.notehub.org/2014/1/3/lorem-ipsum",
shortURL: "https://www.notehub.org/0vrcp",
status: {
success: true,
comment: "some server message"
}
}
The status object serves the same purpose as in the case of note retrieval and publishing.

19
LANDING.md

@ -1,19 +0,0 @@
## Features
- **Themes**: specify the color scheme in the URL: [default](/2014/3/31/demo-note), [dark](/2014/3/31/demo-note?theme=dark), [solarized light](/2014/3/31/demo-note?theme=solarized-light), [solarized dark](/2014/3/31/demo-note?theme=solarized-dark).
- **Fonts**: specify a font (e.g., [Google Web Fonts](http://www.google.com/webfonts/)) for headers and body text in the URL like [this](/8m4l9) or [this](/2014/3/31/demo-note?text-font=monospace&header-font=Courier&text-size=0.7&header-size=1.1).
- **Short URLs**: every page (including theme & font options) has its own short url.
- **Editing**: if you set a password during publishing, you can edit your note any time later.
- **Statistics**: page view counter, publishing and editing date.
- **Expiration**: all notes with less than 30 views after the first 30 days will expire.
- **Export**: the original markdown content can be displayed in plain text format.
- **API**: Integrate the publishing functionality into your editor using the official [NoteHub API](/api).
## Changelog
- **2014-09**: text size setting added ([example](/2014/3/31/demo-note?text-font=monospace&header-font=Courier&text-size=0.7&header-size=1.1))
- **2014-07**: deprecated all API versions less than 1.4 &amp; performance improvements.
- **2014-03**: note expiration implemented.
- **2014-02**: a simple JS-client for API testing [added](/api-test.html).
- **2014-01**: [NoteHub API](/api), mobile friendly styling and more.
- **2013-03**: new color themes.
- **2012-07**: password protection for note editing added.
- **2012-06**: NoteHub released as a result of an [experiment](/2012/6/16/how-notehub-is-built).

1
Procfile

@ -1 +0,0 @@
web: lein with-profile production ring server-headless $PORT

8
README.md

@ -2,14 +2,8 @@
[NoteHub](https://www.notehub.org) is a free and hassle-free pastebin for one-off markdown publishing. [NoteHub](https://www.notehub.org) is a free and hassle-free pastebin for one-off markdown publishing.
NoteHub _was_ an one-app-one-language [experiment](https://www.notehub.org/2012/6/16/how-notehub-is-built) and was initially implemented entirely in [Clojure](http://clojure.org) (ClojureScript).
NoteHub's persistence layer is based on the key-value store [redis](http://redis.io).
Currently, NoteHub is hosted for free on [Heroku](http://heroku.com).
NoteHub supports an [API](https://github.com/chmllr/NoteHub/blob/master/API.md) and can be integrated as a publishing platform.
## How to Use? ## How to Use?
First, create [a new page](https://www.notehub.org/new) using the [Markdown syntax](http://daringfireball.net/projects/markdown/). First, create [a new page](https://notehub.org/new) using the [Markdown syntax](http://daringfireball.net/projects/markdown/).
When the note is published, you'll see a subtle panel at the bottom of the screen. When the note is published, you'll see a subtle panel at the bottom of the screen.
From this panel you can go to a rudimentary statistics of the article, or you can export the original markdown, or copy the short url of the note. From this panel you can go to a rudimentary statistics of the article, or you can export the original markdown, or copy the short url of the note.
Besides this, you also can invert the color scheme by appending to the note url: Besides this, you also can invert the color scheme by appending to the note url:

10
jsconfig.json

@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs"
},
"exclude": [
"node_modules",
"bin"
]
}

28
messages

@ -1,28 +0,0 @@
page-title = Free Pastebin for One-Off Markdown Publishing
title = Free and Hassle-free Pastebin for Markdown Pages.
name = NoteHub
new-page = New Page
status-404 = Not Found
status-400 = Bad Request
status-403 = Forbidden
status-500 = Internal Server Error
footer = Source code on [GitHub](https://github.com/chmllr/NoteHub) &middot; Hosted on [Heroku](http://heroku.com) &middot; DB on [RedisLabs](http://redislabs.com) &middot; SSL by [CloudFlare](http://cloudflare.com) <br/> Created by [@chmllr](https://github.com/chmllr)
loading = Loading...
set-passwd = Password for editing
enter-passwd = Password
publish = Publish
update = Save
published = Published
publisher = Publisher
edited = Edited
views = Article Views
statistics = Statistics
stats = statistics
export = export
notehub = &#8962; notehub
edit = edit
short-url = short url
api-title = API

34
package.json

@ -0,0 +1,34 @@
{
"name": "NoteHub",
"version": "3.0.0",
"description": "Free Pastebin for One-Off Markdown Publishing",
"main": "server.js",
"scripts": {
"server": "node server.js",
"devser": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/chmllr/NoteHub.git"
},
"keywords": [
"markdown",
"pastebin"
],
"author": "Christian Müller <notehub@icloud.com> (http://github.com/chmllr)",
"license": "ISC",
"bugs": {
"url": "https://github.com/chmllr/NoteHub/issues"
},
"homepage": "https://github.com/chmllr/NoteHub",
"dependencies": {
"body-parser": "^1.14.1",
"express": "^4.13.3",
"lru-cache": "^2.6.5",
"marked": "^0.3.5",
"md5": "^2.0.0",
"sequelize": "^3.8.0",
"sqlite3": "^3.1.0"
}
}

26
project.clj

@ -1,26 +0,0 @@
(defproject NoteHub "2.0.0"
:description "A free and anonymous hosting for markdown pages."
:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/core.cache "0.6.4"]
[hiccup "1.0.5"]
[zeus "0.1.0"]
[garden "1.2.5"]
[org.pegdown/pegdown "1.4.2"]
[iokv "0.1.1"]
[cheshire "5.3.1"]
[ring "1.3.1"]
[com.taoensso/carmine "2.7.0" :exclusions [org.clojure/clojure]]
[compojure "1.2.0"]]
:main notehub.handler
:min-lein-version "2.0.0"
:plugins [[lein-ring "0.8.12"]]
:ring {:handler notehub.handler/app}
:profiles {:uberjar {:aot :all}
:production {:ring {:auto-reload? false
:auto-refresh? false}}
:dev {:ring {:auto-reload? true
:auto-refresh? true}
:dependencies
[[javax.servlet/servlet-api "2.5"]
[ring-mock "0.1.5"]]}}
:jvm-opts ["-Dfile.encoding=utf-8"])

35
resources/edit.html

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>NoteHub &mdash; New Page</title>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<link href="/style.css" rel="stylesheet" type="text/css" />
<script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/blueimp-md5/1.0.1/js/md5.min.js" type="text/javascript"></script>
<script src="/js/publishing.js" type="text/javascript"></script>
</head>
<body onload="onLoad()">
<article id="preview" style="flex: none; -webkit-flex: none"></article>
<div class="hidden" id="dashed-line"></div>
<div class="central-element helvetica" style="margin-bottom: 3em">
<form action="/note" autocomplete="off" method="POST">
<input id="action" name="action" value="%ACTION%" type="hidden" />
<input id="id" name="id" value="%ID%" type="hidden" />
<input id="password" name="password" type="hidden" />
<input id="session" name="session" type="hidden" value="%SESSION%" />
<input id="signature" name="signature" type="hidden" />
<textarea id="note" name="note">%CONTENT%</textarea>
<fieldset class="hidden" id="input-elems">
<input class="ui-elem" id="plain-password" name="plain-password" placeholder="Password for editing" type="text">&nbsp;
<input class="button ui-elem" id="publish-button" type="submit" value="Publish">
<br>
<br>
</fieldset>
</form>
</div>
</body>
</html>

6
resources/footer.html

@ -0,0 +1,6 @@
<footer>
<a href="/">&#8962; notehub</a> &middot;
<a href="%LINK%/stats">statistics</a> &middot;
<a href="%LINK%/edit">edit</a> &middot;
<a href="%LINK%/export">export</a>
</footer>

121
resources/public/api-test.html

@ -1,121 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/2.3.2/css/bootstrap.min.css" rel="stylesheet">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<title>NoteHub API Testing</title>
<meta charset="UTF-8">
</head>
<body>
<script>
function request(type, data) {
$("#request").text(JSON.stringify(data));
$.ajax({
type: type,
dataType: 'json',
url: "/api/note",
data: data,
success: function(result){
$("#response").text(JSON.stringify(result));
}
});
}
function selectForm(name) {
["#get", "#post", "#update"].forEach(function(elem){
if(name==elem) {
$(elem + "-form").show();
$(elem + "-li").addClass("active");
} else {
$(elem + "-form").hide()
$(elem + "-li").removeClass("active");
}
});
}
function getNote(){
var data = {
noteID: $("#get-noteID").val(),
version: $("#version").val()
};
request("GET", data);
}
function postNote(){
var data = {
note: $("#post-note").val(),
pid: $("#pid").val(),
signature: $("#post-signature").val(),
password: $("#post-password").val(),
version: $("#version").val()
};
request("POST", data);
}
function updateNote(){
var data = {
noteID: $("#update-noteID").val(),
note: $("#update-note").val(),
pid: $("#pid").val(),
signature: $("#update-signature").val(),
password: $("#update-password").val(),
version: $("#version").val()
};
request("PUT", data);
}
</script>
<div class="container">
<div class="page-header">
<h1>NoteHub API Testing</h1>
</div>
<label>PID</label>
<input id="pid" type="text">
<label>PSK</label>
<input id="psk" type="text">
<label>API version</label>
<input id="version" type="text" value="1.4">
<br/>
<ul class="nav nav-tabs">
<li id="get-li" class="active"><a href="javascript:selectForm('#get')">Get Note</a></li>
<li id="post-li"><a href="javascript:selectForm('#post')">Post Note</a></li>
<li id="update-li"><a href="javascript:selectForm('#update')">Update Note</a></li>
</ul>
<form id="get-form">
<fieldset>
<label>noteID</label>
<input id="get-noteID" type="text" value="2014/1/3/lorem-ipsum">
<br/>
<a href="javascript:getNote()" class="btn">Submit</a>
</fieldset>
</form>
<form id="post-form" style="display: none;">
<fieldset>
<label>note</label>
<textarea id="post-note"></textarea>
<label>signature (md5 hash of pid + psk + note)</label>
<input id="post-signature" type="text" value="">
<label>password</label>
<input id="post-password" type="text" value="">
<br/>
<a href="javascript:postNote()" class="btn">Submit</a>
</fieldset>
</form>
<form id="update-form" style="display: none;">
<fieldset>
<label>noteID</label>
<input id="update-noteID" type="text" value="">
<label>note</label>
<textarea id="update-note"></textarea>
<label>password</label>
<input id="update-password" type="text" value="">
<label>signature (md5 of pid + psk + noteID + note + password)</label>
<input id="update-signature" type="text" value="">
<br/>
<a href="javascript:updateNote()" class="btn">Submit</a>
</fieldset>
</form>
<h4>Request</h4>
<pre id="request" class="prettyprint linenums">
</pre>
<h4>Response</h4>
<pre id="response" class="prettyprint linenums">
</pre>
</div>
</body>
</html>

37
resources/public/index.html

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<title>NoteHub &mdash; Free Pastebin for One-Off Markdown Publishing</title>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link href="/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="hero">
<h1>NoteHub</h1>
<h2>Free and Hassle-free Pastebin for Markdown Notes.</h2>
<br>
<a class="landing-button demo" href="2014/3/31/demo-note" style="color: white">See Demo Note</a>
<a class="landing-button" href="/new" style="color: white">New Note</a>
</div>
<div id="dashed-line"></div>
<article class="helvetica bottom-space" style="font-size: 1em">
<h2>Changelog</h2>
<ul>
<li><strong>2015-11</strong>: NoteHub rewritten in Node.js.</li>
<li><strong>2015-11</strong>: NoteHub API and note styling discontinued due to low adoption by the user base.</li>
<li><strong>2014-09</strong>: text size setting added</li>
<li><strong>2014-07</strong>: deprecated all API versions less than 1.4 &amp; performance improvements.</li>
<li><strong>2014-03</strong>: note expiration implemented.</li>
<li><strong>2014-02</strong>: a simple JS-client for API testing added.</li>
<li><strong>2014-01</strong>: NoteHub API, mobile friendly styling and more.</li>
<li><strong>2013-03</strong>: new color themes.</li>
<li><strong>2012-07</strong>: password protection for note editing added.</li>
<li><strong>2012-06</strong>: NoteHub released as a result of an <a href="/2012/6/16/how-notehub-is-built">experiment</a>.</li>
</ul>
</article>
<footer>
Source code @ <a href="https://github.com/chmllr/NoteHub">GitHub</a>
</footer>
</body>
</html>

56
resources/public/js/publishing.js

@ -1,66 +1,46 @@
var $ = function(id){ return document.getElementById(id); } var $ = function(id) {
return document.getElementById(id);
}
var iosDetected = navigator.userAgent.match("(iPad|iPod|iPhone)"); var iosDetected = navigator.userAgent.match("(iPad|iPod|iPhone)");
var timer = null; var timer = null;
var timerDelay = iosDetected ? 800 : 400; var timerDelay = iosDetected ? 800 : 400;
var show = function(elem) { elem.style.display = "block" } var show = function(elem) {
elem.style.display = "block"
}
var $note, $action, $preview, $plain_password, $input_elems, $dashed_line, $proposed_title, updatePreview; var $note, $action, $preview, $plain_password, $input_elems, $dashed_line, $proposed_title, updatePreview;
var firstLines_;
var backendTimer; var backendTimer;
function updateProposedTitle() { function md2html(input) {
clearTimeout(backendTimer);
backendTimer = setTimeout(function () {
var http = new XMLHttpRequest();
var url = "/propose-title";
http.open("POST", url, true);
http.onreadystatechange = function() {
if(http.readyState == 4 && http.status == 200) {
var now = new Date();
$proposed_title.innerHTML =
"Expected URL: https://www.notehub.org/" +
now.getFullYear() + "/" + (now.getMonth()+1) + "/" + now.getDate() + "/" +
http.responseText;
}
}
http.send($note.value);
}, 500);
}
function md2html(input){
return marked(input); return marked(input);
} }
function onLoad () { function onLoad() {
$note = $("note"); $note = $("note");
$action = $("action"); $action = $("action").value;
$preview = $("preview"); $preview = $("preview");
$plain_password = $("plain-password"); $plain_password = $("plain-password");
$proposed_title = $("proposed-title"); $proposed_title = $("proposed-title");
$input_elems = $("input-elems"); $input_elems = $("input-elems");
$dashed_line = $("dashed-line"); $dashed_line = $("dashed-line");
updatePreview = function(){ updatePreview = function() {
clearTimeout(timer); clearTimeout(timer);
var content = $note.value; var content = $note.value;
var delay = Math.min(timerDelay, timerDelay * (content.length / 400)); var delay = Math.min(timerDelay, timerDelay * (content.length / 400));
timer = setTimeout(function(){ timer = setTimeout(function() {
show($dashed_line); show($dashed_line);
show($input_elems); show($input_elems);
$preview.innerHTML = md2html(content); $preview.innerHTML = md2html(content);
var firstLines = content.split("\n", 2);
if(firstLines_ != firstLines) {
firstLines_ = firstLines;
updateProposedTitle();
}
}, delay); }, delay);
}; };
if($action){ if ($action == "UPDATE") updatePreview();
if($action.value == "update") updatePreview(); else $note.value = ""; else $note.value = "";
$note.onkeyup = updatePreview; $note.onkeyup = updatePreview;
$("publish-button").onclick = function(e) { $("publish-button").onclick = function(e) {
if($plain_password.value != "") $("password").value = md5($plain_password.value); if ($plain_password.value != "") $("password").value = md5($plain_password.value);
$plain_password.value = null; $plain_password.value = null;
$("signature").value = md5($("session").value + $note.value); $("signature").value = md5($("session").value +
} $note.value.replace(/[\n\r]/g, ""));
if(iosDetected) $note.className += " ui-border"; else $note.focus();
} }
if (iosDetected) $note.className += " ui-border";
else $note.focus();
} }

284
resources/public/style.css

@ -0,0 +1,284 @@
.ui-border {
border-radius: 3px;
border: 1px solid #333;
}
a {
border-bottom: 1px dotted;
text-decoration: none;
color: #097;
}
a:hover {
color: #0a8;
}
a:visited {
color: #054;
}
#draft {
margin-bottom: 3em;
}
.button {
cursor: pointer;
}
.ui-elem {
background: #fff;
font-size: 1em;
opacity: 0.8;
padding: 0.3em;
border-radius: 3px;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif';
font-weight: 300;
border: 1px solid #333;
}
.landing-button, textarea, fieldset {
border: none;
margin: 1em;
}
.landing-button {
padding: 10px;
border-radius: 3px;
background: #0a6;
font-size: 1.5em;
text-decoration: none;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif';
font-weight: 300;
}
.landing-button.demo {
opacity: 0.9;
background: #ddd;
color: black !important;
margin-right: 1.5em;
}
.landing-button:hover {
background: #0b7;
}
.landing-button.demo:hover {
background: #eee;
}
.helvetica {
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif';
font-weight: 300;
}
footer {
text-align: center;
padding-bottom: 1em;
font-size: 0.8em;
width: 100%;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif';
font-weight: 300;
}
@media screen and (max-width: 767px) {
footer {
font-size: 0.4em;
}
}
footer a {
border: none;
}
html,
body {
padding: 0;
margin: 0;
color: #333;
background: #fff;
}
#hero {
text-align: center;
padding-bottom: 5em;
padding-top: 5em;
}
h1, h2, h3, h4, h5, h6 {
font-family: "PT Serif", Georgia, serif;
font-weight: bolder;
}
h1 {
font-size: 1.8em;
}
h2 {
font-size: 1.6em;
}
h3 {
font-size: 1.4em;
}
h4 {
font-size: 1.2em;
}
h5 {
font-size: 1.1em;
}
h6 {
font-size: 1.0em;
}
#hero h1 {
font-size: 2.5em;
}
#hero h2 {
margin: 2em;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', sans-serif;
font-weight: 300;
}
article {
-webkit-flex: 1;
flex: 1;
text-align: justify;
margin-top: 3em;
font-family: sans-serif;
margin-right: auto;
margin-left: auto;
}
@media screen and (min-width: 1024px) {
article {
width: 800px;
}
}
@media screen and (max-width: 1023px) {
article {
width: 90%;
}
}
.central-element {
margin-right: auto;
margin-left: auto;
}
@media screen and (min-width: 1024px) {
.central-element {
width: 800px;
}
}
@media screen and (max-width: 1023px) {
.central-element {
width: 90%;
}
}
article img {
max-width: 100%;
}
article p {
line-height: 140%;
font-size: 1.05em;
}
blockquote {
font-family: "PT Serif", Georgia, serif;
padding-left: 1em;
border-left: 4px solid #097;
}
article > h1:first-child {
margin: 2em;
font-size: 2.0em;
text-align: center;
}
.centered {
text-align: center;
}
.bottom-space {
margin-bottom: 7em;
}
code,
pre {
font-size: 1.05em;
background: #efefef;
font-family: monospace;
}
pre {
border: 1px solid #aaa;
padding: 0.5em;
border-radius: 3px;
}
*:focus {
outline: 0px none transparent;
}
@media screen and (min-width: 1024px) {
textarea {
width: 800px;
}
}
textarea {
height: 500px;
font-size: 1em;
font-family: Courier;
border-radius: 5px;
}
.hidden {
display: none;
}
#dashed-line {
margin-bottom: 3em;
margin-top: 3em;
border-bottom: 1px dashed#888;
}
table {
border-collapse: collapse;
width: 100%;
}
th {
background-color: #efefef;
line-height: 2.5em;
padding: 0.3em;
}
td {
line-height: 2.5em;
padding: 0.3em;
border-top: 1px solid #aaa;
}
.middot {
padding: 0.5em;
}
body {
display: -webkit-flex;
}
body {
-webkit-flex-direction: column;
flex-direction: column;
display: flex;
min-height: 100vh;
}

15
resources/template.html

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>NoteHub &mdash; %TITLE%</title>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<link href="/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<article>
%CONTENT%
</article>
%FOOTER%
</body>
</html>

10
scripts/expose.sh

@ -1,10 +0,0 @@
#!/bin/bash
unset DEVMODE
if ! pgrep "redis-server" > /dev/null; then
redis-server &
fi
lein uberjar
java -jar target/NoteHub-2.0.0-standalone.jar

9
scripts/run.sh

@ -1,9 +0,0 @@
#!/bin/bash
export DEVMODE=1
if ! pgrep "redis-server" > /dev/null; then
redis-server &
fi
lein ring server-headless 8080

112
server.js

@ -0,0 +1,112 @@
var express = require('express');
var view = require('./src/view');
var storage = require('./src/storage');
var md5 = require('md5');
var LRU = require("lru-cache")
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.urlencoded({ extended: true }));
var MODELS = {};
var CACHE = new LRU({
max: 50,
dispose: key => {
MODELS[key].save();
delete MODELS[key];
}
});
var getTimeStamp = () => {
var timestamp = new Date().getTime();
timestamp = Math.floor(timestamp / 10000000);
return (timestamp).toString(16)
}
app.use(express.static(__dirname + '/resources/public'));
app.get('/new', function (req, res) {
res.send(view.newNotePage(getTimeStamp() + md5(Math.random())));
});
app.post('/note', function (req, res) {
var body = req.body,
session = body.session,
note = body.note,
password = md5(body.password);
var goToNote = note => res.redirect("/" + note.id);
if (session.indexOf(getTimeStamp()) != 0)
return sendResponse(res, 400, "Session expired");
var expectedSignature = md5(session + note.replace(/[\n\r]/g, ""));
if (expectedSignature != body.signature)
return sendResponse(res, 400, "Signature mismatch");
console.log(body)
if (body.action == "POST")
storage.addNote(note, password).then(goToNote);
else
storage.updateNote(body.id, password, note).then(note => {
CACHE.del(note.id);
goToNote(note);
},
error => sendResponse(res, 403, error.message))
});
app.get("/:year/:month/:day/:title", function (req, res) {
var P = req.params;
storage.getNoteId(P.year + "/" + P.month + "/" + P.day + "/" + P.title)
.then(id => res.redirect("/" + id));
});
app.get(/\/([a-z0-9]+\/edit)/, function (req, res) {
var link = req.params["0"].replace("/edit", "");
storage.getNote(link).then(note => res.send(note
? view.editNotePage(getTimeStamp() + md5(Math.random()), note)
: notFound(res)));
});
app.get(/\/([a-z0-9]+\/export)/, function (req, res) {
var link = req.params["0"].replace("/export", "");
res.set({ 'Content-Type': 'text/plain', 'Charset': 'utf-8' });
storage.getNote(link).then(note => note
? res.send(note.text)
: notFound(res));
});
app.get(/\/([a-z0-9]+\/stats)/, function (req, res) {
var link = req.params["0"].replace("/stats", "");
var promise = link in MODELS
? new Promise(resolve => resolve(MODELS[link]))
: storage.getNote(link);
promise.then(note => note
? res.send(view.renderStats(note))
: notFound(res));
});
app.get(/\/([a-z0-9]+)/, function (req, res) {
var link = req.params["0"];
if (CACHE.has(link)) {
(link in MODELS) && MODELS[link].views++;
res.send(CACHE.get(link));
} else storage.getNote(link).then(note => {
if (!note) return notFound(res);
var content = view.renderNote(note);
CACHE.set(link, content);
MODELS[link] = note;
res.send(content);
});
});
var sendResponse = (res, code, message) =>
res.status(code).send(view.renderPage(message, "<h1>" + message + "</h1>", ""));
var notFound = res => sendResponse(res, 404, "Not found");
var server = app.listen(3000, function () {
console.log('NoteHub server listening on port %s', server.address().port);
});
setInterval(() => {
console.log("saving stats...");
Object.keys(MODELS).forEach(id => MODELS[id].save())
}, 60 * 5 * 1000);

68
src/migrate.js

@ -0,0 +1,68 @@
var redis = require("redis"),
client = redis.createClient();
var Sequelize = require('sequelize');
var sequelize = new Sequelize('database', null, null, {
dialect: 'sqlite',
pool: {
max: 5,
min: 0,
idle: 10000
},
storage: 'database.sqlite'
});
var Note = sequelize.define('Note', {
id: { type: Sequelize.STRING(6), unique: true, primaryKey: true },
deprecatedId: Sequelize.TEXT,
text: Sequelize.TEXT,
published: { type: Sequelize.DATE, defaultValue: Sequelize.NOW },
edited: { type: Sequelize.DATE, allowNull: true, defaultValue: null },
password: Sequelize.STRING(16),
views: Sequelize.INTEGER,
});
sequelize.sync().then(function() {
client.hgetall("note", function(err, notes) {
console.log("notes retrieved:", Object.keys(notes).length);
client.hgetall("published", function(err, published) {
console.log("published retrieved:", Object.keys(published).length);
client.hgetall("password", function(err, password) {
console.log("password retrieved:", Object.keys(password).length);
client.hgetall("views", function(err, views) {
console.log("views retrieved:", Object.keys(views).length);
client.hgetall("edited", function(err, edited) {
console.log("edited retrieved:", Object.keys(edited).length);
Object.keys(notes).forEach(function(id) {
client.smembers(id + ":urls", function(err, links) {
var createLink = LinkId => {
Note.create({
id: LinkId,
deprecatedId: id,
text: notes[id],
published: published[id] && new Date(published[id] * 1000) || new Date(),
password: password[id] && password[id].length == 32 && password[id],
edited: !isNaN(edited[id]) && edited[id] && new Date(edited[id] * 1000) || null,
views: views[id],
})
};
if (links.length == 0) {
var tmp = id.split("/");
var paramString = '{:day "' + tmp[2] +
'", :month "' + tmp[1] + '", :title "' + tmp[3] + '", :year "' + tmp[0] + '"}';
client.hget("short-url", paramString, function(err, result) {
if (!result) throw ("oops:" + paramString + ":" + id);
createLink(result);
});
} else createLink(links[links.length - 1]);
})
});
});
});
});
});
});
})

126
src/notehub/api.clj

@ -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))})))

260
src/notehub/css.clj

@ -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"
}]
)))

169
src/notehub/handler.clj

@ -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)}))

155
src/notehub/storage.clj

@ -1,155 +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 decomp []
(let [ids (map first (partition 2 (redis :hgetall :note)))]
(doseq [id ids]
(do
(println id)
(redis :hset :note id (get-note id))
))))
(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"))))

124
src/notehub/views.clj

@ -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) "&mdash;" 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) "&nbsp"
(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 "&middot;"] links)])))

58
src/storage.js

@ -0,0 +1,58 @@
var Sequelize = require('sequelize');
var sequelize = new Sequelize('database', null, null, {
dialect: 'sqlite',
pool: {
max: 5,
min: 0,
idle: 10000
},
storage: 'database.sqlite'
});
var Note = sequelize.define('Note', {
id: { type: Sequelize.STRING(6), unique: true, primaryKey: true },
deprecatedId: Sequelize.TEXT,
text: Sequelize.TEXT,
published: { type: Sequelize.DATE, defaultValue: Sequelize.NOW },
edited: { type: Sequelize.DATE, allowNull: true, defaultValue: null },
password: Sequelize.STRING(16),
views: { type: Sequelize.INTEGER, defaultValue: 0 }
});
module.exports.getNote = id => {
console.log("resolving note", id);
return Note.findById(id);
}
module.exports.getNoteId = deprecatedId => {
console.log("resolving deprecated Id", deprecatedId);
return Note.findOne({
where: { deprecatedId: deprecatedId }
}).then(note => note.id);
}
var generateId = () => [1, 1, 1, 1, 1]
.map(() => {
var code = Math.floor(Math.random() * 36);
return String.fromCharCode(code + (code < 10 ? 48 : 87));
})
.join("");
var getFreeId = () => {
var id = generateId();
return Note.findById(id).then(result => result ? getFreeId() : id);
};
module.exports.addNote = (note, password) => getFreeId().then(id => Note.create({
id: id,
text: note,
password: password
}));
module.exports.updateNote = (id, password, text) => Note.findById(id).then(note => {
if (!note || note.password !== password) return new Promise((resolve, reject) => {
reject({ message: "Password is wrong" });
});
note.text = text;
return note.save();
});

41
src/view.js

@ -0,0 +1,41 @@
var marked = require("marked");
var fs = require("fs");
var pageTemplate = fs.readFileSync("resources/template.html", "utf-8");
var footerTemplate = fs.readFileSync("resources/footer.html", "utf-8");
var editTemplate = fs.readFileSync("resources/edit.html", "utf-8");
var deriveTitle = text => text
.split(/[\n\r]/)[0].slice(0,25)
.replace(/[^a-zA-Z0-9\s]/g, "");
var renderPage = (title, content, footer) => pageTemplate
.replace("%TITLE%", title)
.replace("%CONTENT%", content)
.replace("%FOOTER%", footer);
module.exports.renderPage = renderPage;
module.exports.renderStats = note => renderPage(deriveTitle(note.text),
`<h2>Statistics</h2>
<table>
<tr><td>Published</td><td>${note.published}</td></tr>
<tr><td>Edited</td><td>${note.edited || "N/A"}</td></tr>
<tr><td>Views</td><td>${note.views}</td></tr>
</table>`,
"");
module.exports.renderNote = note => renderPage(deriveTitle(note.text),
marked(note.text),
footerTemplate.replace(/%LINK%/g, note.id));
module.exports.newNotePage = session => editTemplate
.replace("%ACTION%", "POST")
.replace("%SESSION%", session)
.replace("%CONTENT%", "Loading...");
module.exports.editNotePage = (session, note) => editTemplate
.replace("%ACTION%", "UPDATE")
.replace("%SESSION%", session)
.replace("%ID%", note.id)
.replace("%CONTENT%", note.text);

1
system.properties

@ -1 +0,0 @@
java.runtime.version=1.8

214
test/notehub/test/api.clj

@ -1,214 +0,0 @@
(ns notehub.test.api
(:require
[cheshire.core :refer :all]
[notehub.storage :as storage])
(:use [notehub.api]
[notehub.handler]
[clojure.test]))
(def note "hello world!\nThis is a _test_ note!")
(def pid "somePlugin")
(def pid2 "somePlugin2")
(def note-title (let [[y m d] (get-date)]
(apply str (interpose "/" [y m d "hello-world"]))))
(def note-url (str (apply str "/" (interpose "/" (get-date))) "/hello-world"))
(defn substring? [a b] (not (= nil (re-matches (re-pattern (str "(?s).*" a ".*")) b))))
(defmacro isnt [arg] `(is (not ~arg)))
(defn send-request
([resource] (send-request resource {}))
([resource params]
(let [[method url] (if (vector? resource) resource [:get resource])]
(app-routes {:request-method method :uri url :params params}))))
(defn has-status [input status]
(= status (:status input)))
(defn register-publisher-fixture [f]
(def psk (storage/register-publisher pid))
(f)
(storage/revoke-publisher pid)
(storage/revoke-publisher pid2)
(storage/delete-note note-title))
(use-fixtures :each register-publisher-fixture)
(deftest api
(testing "API"
(testing "publisher registration"
(is (storage/valid-publisher? pid))
(isnt (storage/valid-publisher? pid2))
(let [psk2 (storage/register-publisher pid2)]
(is (= psk2 (storage/get-psk pid2)))
(is (storage/valid-publisher? pid2))
(is (storage/revoke-publisher pid2))
(isnt (storage/valid-publisher? "any_PID"))
(isnt (storage/valid-publisher? pid2))))
(testing "note publishing & retrieval"
(isnt (:success (:status (get-note {:noteID "some note id"}))))
(is (= "note is empty" (:message (:status (post-note {:note "" :pid pid :signature (storage/sign pid psk "")})))))
(let [post-response (post-note {:note note :pid pid :signature (storage/sign pid psk note)})
get-response (get-note post-response)]
(is (:success (:status post-response)))
(is (:success (:status get-response)))
(is (= note (:note get-response)))
(is (= (:longURL post-response) (:longURL get-response) note-url))
(is (= (:shortURL post-response) (:shortURL get-response)))
(is (storage/note-exists? (:noteID post-response)))
(let [su (last (clojure.string/split (:shortURL get-response) #"/"))]
(is (= su (storage/create-short-url (:noteID post-response) (storage/resolve-url su)))))
(let [resp (send-request (:shortURL get-response))
resp (send-request ((:headers resp) "Location"))]
(is (substring? "hello world"(:body resp))))
(is (= (:publisher get-response) pid))
(is (= (:title get-response) (derive-title note)))
(is (= "1" (get-in get-response [:statistics :views])))
(isnt (get-in get-response [:statistics :edited]))
(is (= "noteID 'randomString' unknown"
(get-in
(parse-string
(:body (send-request "/api/note" {:version "1.4" :noteID "randomString"})))
["status" "message"])))
(is (= "3" (get-in (get-note post-response) [:statistics :views])))))
(testing "creation with wrong signature"
(let [response (post-note {:note note :pid pid :signature (storage/sign pid2 psk note)})]
(isnt (:success (:status response)))
(is (= "signature invalid" (:message (:status response)))))
(let [response (post-note {:note note :pid pid :signature (storage/sign pid2 psk "any note")})]
(isnt (:success (:status response)))
(is (= "signature invalid" (:message (:status response)))))
(isnt (:success (:status (post-note {:note note :pid pid :signature (storage/sign pid "random_psk" note)}))))
(is (:success (:status (post-note {:note note :pid pid :signature (storage/sign pid psk note)}))))
(let [randomPID "randomPID"
psk2 (storage/register-publisher randomPID)
_ (storage/revoke-publisher randomPID)
response (post-note {:note note :pid randomPID :signature (storage/sign randomPID psk2 note)})]
(isnt (:success (:status response)))
(is (= (:message (:status response)) "pid invalid"))))
(testing "note update"
(let [post-response (post-note {:note note :pid pid :signature (storage/sign pid psk note) :password "passwd"})
note-id (:noteID post-response)
new-note "a new note!"]
(is (:success (:status post-response)))
(is (:success (:status (get-note {:noteID note-id}))))
(is (= note (:note (get-note {:noteID note-id}))))
(let [update-response (update-note {:noteID note-id :note new-note :pid pid
:signature (storage/sign pid psk new-note) :password "passwd"})]
(isnt (:success (:status update-response)))
(is (= "signature invalid" (:message (:status update-response)))))
(is (= note (:note (get-note {:noteID note-id}))))
(let [update-response (update-note {:noteID note-id :note new-note :pid pid
:signature (storage/sign pid psk note-id new-note "passwd")
:password "passwd"})]
(is (= { :success true } (:status update-response)))
(isnt (= nil (get-in (get-note {:noteID note-id}) [:statistics :edited])))
(is (= new-note (:note (get-note {:noteID note-id})))))
(let [update-response (update-note {:noteID note-id :note "aaa" :pid pid
:signature (storage/sign pid psk note-id "aaa" "pass")
:password "pass"})]
(isnt (:success (:status update-response)))
(is (= "password invalid" (:message (:status update-response)))))
(is (= new-note (:note (get-note {:noteID note-id}))))
(is (= new-note (:note (get-note {:noteID note-id}))))))))
(deftest api-note-creation
(testing "Note creation"
(let [response (send-request [:post "/api/note"]
{:note note
:pid pid
:signature (storage/sign pid psk note)
:version "1.4"})
body (parse-string (:body response))
noteID (body "noteID")]
(is (has-status response 200))
(is (get-in body ["status" "success"]))
(is (= note ((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID}))) "note")))
(is (= "Deprecated API version" (get-in (parse-string
(:body (send-request [:get "/api/note"] {:version "1.3" :noteID noteID}))) ["status" "message"])))
(is (= "API version expected" (get-in (parse-string
(:body (send-request [:get "/api/note"] {:noteID noteID}))) ["status" "message"])))
(is (= note ((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID}))) "note")))
(isnt (= note ((parse-string
(:body (send-request [:get "/api/note"] {:version "1.3" :noteID noteID}))) "note")))
(is (do
(storage/delete-note noteID)
(not (storage/note-exists? noteID)))))))
(deftest api-note-creation-with-params
(testing "Note creation with params"
(let [response (send-request [:post "/api/note"]
{:note note
:pid pid
:signature (storage/sign pid psk note)
:version "1.4"
:theme "dark"
:text-size 1.1
:text-font "Helvetica"})
body (parse-string (:body response))
noteID (body "noteID")]
(let [url ((:headers
(send-request
(str "/"
(last (clojure.string/split
((parse-string (:body response)) "shortURL") #"/"))))) "Location")]
(= url ((parse-string (:body response)) "longURL"))
(is (substring? "theme=dark" url))
(is (substring? "text-size=1.1" url))
(is (substring? "text-font=Helvetica" url)))
(is (do
(storage/delete-note noteID)
(not (storage/note-exists? noteID)))))))
(deftest api-note-update
(let [response (send-request [:post "/api/note"]
{:note note
:pid pid
:signature (storage/sign pid psk note)
:version "1.4"
:password "qwerty"})
body (parse-string (:body response))
noteID (body "noteID")]
(testing "Note update"
(is (has-status response 200))
(is (get-in body ["status" "success"]))
(is (storage/note-exists? noteID))
(is (substring? "_test_ note"
((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID}))) "note")))
(let [response (send-request [:put "/api/note"]
{:noteID noteID
:note "WRONG pass"
:pid pid
:signature (storage/sign pid psk noteID "WRONG pass" "qwerty1")
:password "qwerty1"
:version "1.4"})
body (parse-string (:body response))]
(is (has-status response 200))
(isnt (get-in body ["status" "success"]))
(is (= "password invalid" (get-in body ["status" "message"])))
(isnt (get-in body ["statistics" "edited"]))
(is (substring? "_test_ note"
((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID}))) "note"))))
(is (get-in (parse-string
(:body (send-request [:put "/api/note"]
{:noteID noteID
:note "UPDATED CONTENT"
:pid pid
:signature (storage/sign pid psk noteID "UPDATED CONTENT" "qwerty")
:password "qwerty"
:version "1.4"}))) ["status" "success"]))
(isnt (= nil (((parse-string
(:body (send-request [:get "/api/note"] {:version "1.4" :noteID noteID})))
"statistics") "edited")))
(let [resp (send-request [:get "/api/note"] {:version "1.4" :noteID noteID})]
(is (substring? "UPDATED CONTENT"
((parse-string
(:body resp)) "note"))))
(is (do
(storage/delete-note noteID)
(not (storage/note-exists? noteID)))))))

129
test/notehub/test/handler.clj

@ -1,129 +0,0 @@
(ns notehub.test.handler
(:use clojure.test
[notehub.api :only [get-date url]]
notehub.storage
ring.mock.request
notehub.test.api
notehub.handler))
(defn build-key [[y m d] t] (notehub.api/build-key y m d t))
(def date [2012 6 3])
(def test-title "some-title")
(def test-note "# This is a test note.\nHello _world_. Motörhead, тест.")
(def session-key (str (quot (System/currentTimeMillis) 100000000) "somemd5hash"))
(defn create-testnote-fixture [f]
(add-note (build-key date test-title) test-note "testPID")
(f)
(delete-note (build-key date test-title)))
(use-fixtures :each create-testnote-fixture)
(is (= (url 2010 05 06 "test-title" "export") "/2010/5/6/test-title/export"))
(deftest testing-fixture
(testing "Was a not created?"
(is (= (get-note (build-key date test-title)) test-note))
(is (note-exists? (build-key date test-title)))))
(deftest export-test
(testing "Markdown export"
(is (= (:body (send-request (url 2012 6 3 "some-title" "export"))) test-note))))
(deftest note-creation
(let [date (get-date)
title "this-is-a-test-note"
[year month day] date]
(testing "Note creation"
(let [resp (send-request
[:post "/post-note"]
{:session session-key
:note test-note
:signature (sign session-key test-note)})]
(is (has-status resp 302))
(is (note-exists? (build-key date title)))
(is (substring? "Hello <em>world</em>"
((send-request (url year month day title)) :body)))
(is (do
(delete-note (build-key date title))
(not (note-exists? (build-key date title)))))))))
(deftest note-creation-utf
(let [date (get-date)
title "радуга"
note "# Радуга\nкаждый охотник желает знать, где сидят фазаны."
[year month day] date]
(testing "Note creation with UTF8 symbols"
(is (has-status
(send-request
[:post "/post-note"]
{:session session-key
:note note
:signature (sign session-key note)}) 302))
(is (note-exists? (build-key date title)))
(is (substring? "знать" ((send-request (url year month day title)) :body)))
(is (do
(delete-note (build-key date title))
(not (note-exists? (build-key date title))))))))
(deftest note-update
(let [date (get-date)
title "this-is-a-test-note"
[year month day] date
hash (sign session-key test-note)]
(testing "Note update"
(is (has-status
(send-request
[:post "/post-note"]
{:session session-key
:note test-note
:password "qwerty"
:signature hash}) 302))
(is (note-exists? (build-key date title)))
(is (substring? "test note" ((send-request (url year month day title)) :body)))
(is (has-status
(send-request
[:post "/update-note"]
{:noteID (build-key [year month day] title)
:note "WRONG pass"
:password "qwerty1" }) 403))
(is (substring? "test note" ((send-request (url year month day title)) :body)))
(is (has-status
(send-request
[:post "/update-note"]
{:noteID (build-key [year month day] title)
:note "UPDATED CONTENT 123"
:password "qwerty" }) 302))
(is (substring? "UPDATED CONTENT" ((send-request (url year month day title)) :body)))
(is (do
(delete-note (build-key date title))
(not (note-exists? (build-key date title))))))))
(deftest requests
(testing "HTTP Status"
(testing "of a wrong access"
(is (has-status (send-request "/wrong-page") 404))
(is (has-status (send-request (url 2012 6 3 "lol" "stat")) 404))
(is (has-status (send-request (url 2012 6 3 "lol" "export")) 404))
(is (has-status (send-request (url 2012 6 3 "lol")) 404))
(is (has-status (send-request (url 2012 6 4 "wrong-title")) 404)))
(testing "of corrupt note-post"
(is (has-status (send-request [:post "/post-note"]) 400)))
(testing "valid accesses"
;(is (has-status (send-request "/new") 200) "accessing /new")
(is (has-status (send-request "/api") 200) "accessing API")
(is (has-status (send-request (url 2012 6 3 "some-title")) 200) "accessing test note")
(is (has-status (send-request (url 2012 6 3 "some-title" "export")) 200) "accessing test note's export")
(is (has-status (send-request (url 2012 6 3 "some-title" "stats")) 200) "accessing test note's stats")
(is (has-status (send-request "/") 200) "accessing landing page"))))
(deftest test-app
(testing "main route"
(let [response (app (request :get "/"))]
(is (= (:status response) 200))
(is (substring? "Free and Hassle-free" (:body response)))))
(testing "not-found route"
(let [response (app (request :get "/invalid"))]
(is (= (:status response) 404)))))

70
test/notehub/test/storage.clj

@ -1,70 +0,0 @@
(ns notehub.test.storage
(:use [notehub.storage]
[clojure.test])
(:require [taoensso.carmine :as car :refer (wcar)]))
(defn build-key [[y m d] t] (notehub.api/build-key y m d t))
(def date [2012 06 03])
(def test-title "Some title.")
(def test-note "This is a test note.")
(def metadata {:year 2012,
:month 6,
:day 23,
:title test-title,
:theme "dark",
:header-font "Anton"})
(def test-short-url "")
(deftest signature
(is (= "07e1c0d9533b5168e18a99f4540448af" (sign "wwq123456"))))
(deftest storage
(testing "Storage"
(testing "of short-url mechanism"
(let [fakeID (build-key date test-title)
url (create-short-url fakeID metadata)
url2 (create-short-url fakeID metadata)]
(is (= 1 (redis :scard (str fakeID :urls))))
(def test-short-url (create-short-url fakeID (assoc metadata :a :b)))
(is (= 2 (redis :scard (str fakeID :urls))))
(is (short-url-exists? url))
(is (= url url2))
(is (= metadata (resolve-url url)))
(delete-short-url url)
(is (not (short-url-exists? url))))))
(testing "of correct note creation"
(is (= (let [note-id (build-key date test-title)]
(add-note note-id test-note "testPID")
(is (= 2 (redis :scard (str note-id :urls))))
(create-short-url note-id metadata)
(is (= 3 (redis :scard (str note-id :urls))))
(increment-note-view note-id)
(get-note note-id))
test-note))
(is (= "1" (get-note-views (build-key date test-title))))
(is (= (do
(increment-note-view (build-key date test-title))
(get-note-views (build-key date test-title)))
"2")))
(testing "of note update"
(is (= (do
(add-note (build-key date test-title) test-note "testPID" "12345qwert")
(get-note (build-key date test-title)))
test-note))
(is (valid-password? (build-key date test-title) "12345qwert"))
(is (= (do
(edit-note (build-key date test-title) "update")
(get-note (build-key date test-title)))
"update")))
(testing "of the note access"
(is (not= (get-note (build-key date test-title)) "any text")))
(testing "of note existence"
(is (note-exists? (build-key date test-title)))
(is (short-url-exists? test-short-url))
(is (= 3 (redis :scard (str (build-key date test-title) :urls))))
(delete-note (build-key date test-title))
(is (not (short-url-exists? test-short-url)))
(is (not (note-exists? (build-key date test-title))))
(is (= 0 (redis :scard (str (build-key date test-title) :urls))))
(is (not (note-exists? (build-key [2013 06 03] test-title))))
(is (not (note-exists? (build-key date "some title"))))))
Loading…
Cancel
Save