Merge branch 'node'
This commit is contained in:
+5
-13
@@ -1,14 +1,6 @@
|
||||
Makefile
|
||||
PrivateMakefile
|
||||
dump.rdb
|
||||
resources/public/cljs
|
||||
pom.xml
|
||||
*jar
|
||||
/lib/
|
||||
/classes/
|
||||
.lein-*
|
||||
.crossover-cljs
|
||||
target/
|
||||
.nrepl-port
|
||||
.idea/
|
||||
NoteHub.iml
|
||||
bin/
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
database.sqlite
|
||||
database.sqlite-journal
|
||||
|
||||
Vendored
+42
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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 & 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).
|
||||
@@ -2,14 +2,8 @@
|
||||
|
||||
[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?
|
||||
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.
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "commonjs"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"bin"
|
||||
]
|
||||
}
|
||||
@@ -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) · Hosted on [Heroku](http://heroku.com) · DB on [RedisLabs](http://redislabs.com) · 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 = ⌂ notehub
|
||||
edit = edit
|
||||
short-url = short url
|
||||
api-title = API
|
||||
@@ -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
@@ -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"])
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>NoteHub — 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">
|
||||
<input class="button ui-elem" id="publish-button" type="submit" value="Publish">
|
||||
<br>
|
||||
<br>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
<footer>
|
||||
<a href="/">⌂ notehub</a> ·
|
||||
<a href="%LINK%/stats">statistics</a> ·
|
||||
<a href="%LINK%/edit">edit</a> ·
|
||||
<a href="%LINK%/export">export</a>
|
||||
</footer>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>NoteHub — 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 & 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>
|
||||
@@ -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 timer = null;
|
||||
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 firstLines_;
|
||||
var backendTimer;
|
||||
|
||||
function updateProposedTitle() {
|
||||
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){
|
||||
function md2html(input) {
|
||||
return marked(input);
|
||||
}
|
||||
|
||||
function onLoad () {
|
||||
function onLoad() {
|
||||
$note = $("note");
|
||||
$action = $("action");
|
||||
$action = $("action").value;
|
||||
$preview = $("preview");
|
||||
$plain_password = $("plain-password");
|
||||
$proposed_title = $("proposed-title");
|
||||
$input_elems = $("input-elems");
|
||||
$dashed_line = $("dashed-line");
|
||||
updatePreview = function(){
|
||||
updatePreview = function() {
|
||||
clearTimeout(timer);
|
||||
var content = $note.value;
|
||||
var delay = Math.min(timerDelay, timerDelay * (content.length / 400));
|
||||
timer = setTimeout(function(){
|
||||
timer = setTimeout(function() {
|
||||
show($dashed_line);
|
||||
show($input_elems);
|
||||
$preview.innerHTML = md2html(content);
|
||||
var firstLines = content.split("\n", 2);
|
||||
if(firstLines_ != firstLines) {
|
||||
firstLines_ = firstLines;
|
||||
updateProposedTitle();
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
if($action){
|
||||
if($action.value == "update") updatePreview(); else $note.value = "";
|
||||
if ($action == "UPDATE") updatePreview();
|
||||
else $note.value = "";
|
||||
$note.onkeyup = updatePreview;
|
||||
$("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;
|
||||
$("signature").value = md5($("session").value + $note.value);
|
||||
}
|
||||
if(iosDetected) $note.className += " ui-border"; else $note.focus();
|
||||
$("signature").value = md5($("session").value +
|
||||
$note.value.replace(/[\n\r]/g, ""));
|
||||
}
|
||||
if (iosDetected) $note.className += " ui-border";
|
||||
else $note.focus();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>NoteHub — %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>
|
||||
@@ -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
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export DEVMODE=1
|
||||
|
||||
if ! pgrep "redis-server" > /dev/null; then
|
||||
redis-server &
|
||||
fi
|
||||
|
||||
lein ring server-headless 8080
|
||||
@@ -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);
|
||||
@@ -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]);
|
||||
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -1,126 +0,0 @@
|
||||
(ns notehub.api
|
||||
(:import
|
||||
[java.util Calendar])
|
||||
(:use
|
||||
[iokv.core]
|
||||
[ring.util.codec :only [url-encode]]
|
||||
[clojure.string :rename {replace sreplace}
|
||||
:only [replace blank? trim lower-case split-lines split]])
|
||||
(:require
|
||||
[ring.util.codec]
|
||||
[hiccup.util :as util]
|
||||
[notehub.storage :as storage]))
|
||||
|
||||
(def version "1.4")
|
||||
|
||||
(defn log
|
||||
"Logs args to the server stdout"
|
||||
[string & args]
|
||||
(apply printf (str "%s:" string) (str (storage/get-current-date) ":LOG") args)
|
||||
(println))
|
||||
|
||||
(defn url
|
||||
"Creates a local url from the given substrings"
|
||||
[& args]
|
||||
(apply str (interpose "/" (cons "" (map url-encode args)))))
|
||||
|
||||
; Concatenates all fields to a string
|
||||
(defn build-key
|
||||
"Returns a storage-key for the given note coordinates"
|
||||
[year month day title]
|
||||
(apply str (interpose "/" [year month day title])))
|
||||
|
||||
(defn derive-title [md-text]
|
||||
(sreplace (first (split-lines md-text))
|
||||
#"(#+|_|\*+|<.*?>)" ""))
|
||||
|
||||
(defn get-date
|
||||
"Returns today's date"
|
||||
[]
|
||||
(map #(+ (second %) (.get (Calendar/getInstance) (first %)))
|
||||
{Calendar/YEAR 0, Calendar/MONTH 1, Calendar/DAY_OF_MONTH 0}))
|
||||
|
||||
(defn- create-response
|
||||
([success] {:success success})
|
||||
([success message & params]
|
||||
(assoc (create-response success) :message (apply format message params))))
|
||||
|
||||
(defn- get-path [host-url token & [description]]
|
||||
(if (= :url description)
|
||||
(str host-url "/" token)
|
||||
(let [[year month day title] (split token #"/")]
|
||||
(if description
|
||||
(str host-url "/" (storage/create-short-url token {:year year :month month :day day :title title}))
|
||||
(str host-url (url year month day title))))))
|
||||
|
||||
(defn version-manager [f params]
|
||||
(if-let [req-version (:version params)]
|
||||
(let [req-version (Double/parseDouble req-version)
|
||||
version (Double/parseDouble version)]
|
||||
(if (< req-version version)
|
||||
{:status (create-response false "Deprecated API version")}
|
||||
(f params)))
|
||||
{:status (create-response false "API version expected")}))
|
||||
|
||||
(defn get-note [{:keys [noteID hostURL]}]
|
||||
(if (storage/note-exists? noteID)
|
||||
(let [note (storage/get-note noteID)]
|
||||
(storage/increment-note-view noteID)
|
||||
{:note note
|
||||
:title (derive-title note)
|
||||
:longURL (get-path hostURL noteID)
|
||||
:shortURL (get-path hostURL noteID :id)
|
||||
:statistics (storage/get-note-statistics noteID)
|
||||
:status (create-response true)
|
||||
:publisher (storage/get-publisher noteID)})
|
||||
{:status (create-response false "noteID '%s' unknown" noteID)}))
|
||||
|
||||
(defn propose-title [note]
|
||||
(let [raw-title (filter #(or (= \- %) (Character/isLetterOrDigit %))
|
||||
(-> note derive-title trim (sreplace " " "-") lower-case))
|
||||
max-length (get-setting :max-title-length #(Integer/parseInt %) 80)]
|
||||
(apply str (take max-length raw-title))))
|
||||
|
||||
(defn post-note
|
||||
[{:keys [note pid signature password hostURL] :as params}]
|
||||
;(log "post-note: %s" {:pid pid :signature signature :password password :note note})
|
||||
(let [errors (filter identity
|
||||
[(when-not (storage/valid-publisher? pid) "pid invalid")
|
||||
(when-not (= signature (storage/sign pid (storage/get-psk pid) note))
|
||||
"signature invalid")
|
||||
(when (blank? note) "note is empty")])]
|
||||
(if (empty? errors)
|
||||
(let [[year month day] (map str (get-date))
|
||||
params (select-keys params [:text-size :header-size :text-font :header-font :theme])
|
||||
proposed-title (propose-title note)
|
||||
title (first (drop-while #(storage/note-exists? (build-key year month day %))
|
||||
(cons proposed-title
|
||||
(map #(str proposed-title "-" (+ 2 %)) (range)))))
|
||||
noteID (build-key year month day title)
|
||||
new-params (assoc params :year year :month month :day day :title title)
|
||||
short-url (get-path hostURL (storage/create-short-url noteID new-params) :url)
|
||||
long-url (get-path hostURL noteID)]
|
||||
(do
|
||||
(storage/add-note noteID note pid password)
|
||||
{:noteID noteID
|
||||
:longURL (if (empty? params) long-url (str (util/url long-url params)))
|
||||
:shortURL short-url
|
||||
:status (create-response true)}))
|
||||
{:status (create-response false (first errors))})))
|
||||
|
||||
|
||||
(defn update-note [{:keys [noteID note pid signature password hostURL]}]
|
||||
;(log "update-note: %s" {:pid pid :noteID noteID :signature signature :password password :note note})
|
||||
(let [errors (filter identity
|
||||
[(when-not (storage/valid-publisher? pid) "pid invalid")
|
||||
(when-not (= signature (storage/sign pid (storage/get-psk pid) noteID note password))
|
||||
"signature invalid")
|
||||
(when (blank? note) "note is empty")
|
||||
(when-not (storage/valid-password? noteID password) "password invalid")])]
|
||||
(if (empty? errors)
|
||||
(do
|
||||
(storage/edit-note noteID note)
|
||||
{:longURL (get-path hostURL noteID)
|
||||
:shortURL (get-path hostURL noteID :id)
|
||||
:status (create-response true)})
|
||||
{:status (create-response false (first errors))})))
|
||||
@@ -1,260 +0,0 @@
|
||||
(ns notehub.css
|
||||
(:require [garden.core :refer [css]]
|
||||
[garden.stylesheet :refer [at-media]]
|
||||
[garden.units :as u :refer [px pt em]]))
|
||||
|
||||
(def themes
|
||||
{
|
||||
"dark"
|
||||
{
|
||||
:background {
|
||||
:normal "#333",
|
||||
:halftone "#444"
|
||||
},
|
||||
:foreground {
|
||||
:normal "#ccc",
|
||||
:halftone "#bbb"
|
||||
},
|
||||
:link {
|
||||
:fresh "#6b8",
|
||||
:visited "#496",
|
||||
:hover "#7c9"
|
||||
}
|
||||
},
|
||||
"solarized-light"
|
||||
{
|
||||
:background {
|
||||
:normal "#fdf6e3",
|
||||
:halftone "#eee8d5"
|
||||
},
|
||||
:foreground {
|
||||
:normal "#657b83",
|
||||
:halftone "#839496"
|
||||
},
|
||||
:link {
|
||||
:fresh "#b58900",
|
||||
:visited "#cb4b16",
|
||||
:hover "#dc322f"
|
||||
}
|
||||
},
|
||||
"solarized-dark"
|
||||
{
|
||||
:background {
|
||||
:normal "#073642",
|
||||
:halftone "#002b36"
|
||||
},
|
||||
:foreground {
|
||||
:normal "#93a1a1",
|
||||
:halftone "#839191"
|
||||
},
|
||||
:link {
|
||||
:fresh "#cb4b16",
|
||||
:visited "#b58900",
|
||||
:hover "#dc322f"
|
||||
}
|
||||
},
|
||||
"default"
|
||||
{
|
||||
:background {
|
||||
:normal "#fff",
|
||||
:halftone "#efefef"
|
||||
},
|
||||
:foreground {
|
||||
:normal "#333",
|
||||
:halftone "#888"
|
||||
},
|
||||
:link {
|
||||
:fresh "#097",
|
||||
:visited "#054",
|
||||
:hover "#0a8"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
(defn generate [params]
|
||||
(let [theme (themes (params "theme" "default"))
|
||||
; VARIABLES
|
||||
background (get-in theme [:background :normal])
|
||||
foreground (get-in theme [:foreground :normal])
|
||||
background-halftone (get-in theme [:background :halftone])
|
||||
foreground-halftone (get-in theme [:foreground :halftone])
|
||||
link-fresh (get-in theme [:link :fresh])
|
||||
link-visited (get-in theme [:link :visited])
|
||||
link-hover (get-in theme [:link :hover])
|
||||
width (px 800)
|
||||
header-font (or (params "header-font") "Noticia Text")
|
||||
text-font (or (params "text-font") "Georgia")
|
||||
header-size-factor (Float/parseFloat (or (params "header-size") "1"))
|
||||
text-size-factor (Float/parseFloat (or (params "text-size") "1"))
|
||||
|
||||
; MIXINS
|
||||
helvetica {
|
||||
:font-weight 300
|
||||
:font-family "'Helvetica Neue','Helvetica','Arial','Lucida Grande','sans-serif'"
|
||||
}
|
||||
central-element {
|
||||
:margin-left "auto"
|
||||
:margin-right "auto"
|
||||
}
|
||||
thin-border {
|
||||
:border (print-str "1px solid" foreground)
|
||||
}]
|
||||
(css
|
||||
[:.ui-border { :border-radius (px 3) } thin-border]
|
||||
|
||||
[:a {
|
||||
:color link-fresh
|
||||
:text-decoration "none"
|
||||
:border-bottom "1px dotted"
|
||||
}]
|
||||
[:a:hover { :color link-hover }]
|
||||
[:a:visited { :color link-visited }]
|
||||
[:#draft {
|
||||
:margin-bottom (em 3)
|
||||
}]
|
||||
[:.button {
|
||||
:cursor "pointer"
|
||||
}]
|
||||
[:.ui-elem {
|
||||
:border-radius (px 3)
|
||||
:padding (em 0.3)
|
||||
:opacity 0.8
|
||||
:font-size (em 1)
|
||||
:background background
|
||||
}
|
||||
helvetica thin-border]
|
||||
[:.landing-button, :textarea, :fieldset { :border "none" }]
|
||||
[:.landing-button {
|
||||
:box-shadow "0 2px 5px #aaa"
|
||||
:text-decoration "none"
|
||||
:font-size (em 1.5)
|
||||
:background "#0a2"
|
||||
:border-radius (px 10)
|
||||
:padding (px 10)
|
||||
}
|
||||
helvetica]
|
||||
[:.landing-button:hover { :background "#0b2" }]
|
||||
|
||||
[:.helvetica helvetica]
|
||||
|
||||
[:#footer {
|
||||
:width "100%"
|
||||
:font-size (em 0.8)
|
||||
:padding-bottom (em 1)
|
||||
:text-align "center"
|
||||
}
|
||||
helvetica]
|
||||
(at-media {:screen true :max-width (px 767)} [:#footer {:font-size (em 0.4)}])
|
||||
["#footer a" { :border "none" }]
|
||||
|
||||
[:html, :body {
|
||||
:background background
|
||||
:color foreground
|
||||
:margin 0
|
||||
:padding 0
|
||||
}]
|
||||
[:#hero {
|
||||
:padding-top (em 5)
|
||||
:padding-bottom (em 5)
|
||||
:text-align "center"
|
||||
}]
|
||||
[:h1, :h2, :h3, :h4, :h5, :h6 {
|
||||
:font-weight "bold"
|
||||
:font-family (str header-font ",'Noticia Text','PT Serif','Georgia'")
|
||||
}]
|
||||
[:h1 { :font-size (em (* 1.8 header-size-factor)) }]
|
||||
[:h2 { :font-size (em (* 1.6 header-size-factor)) }]
|
||||
[:h3 { :font-size (em (* 1.4 header-size-factor)) }]
|
||||
[:h4 { :font-size (em (* 1.2 header-size-factor)) }]
|
||||
[:h5 { :font-size (em (* 1.1 header-size-factor)) }]
|
||||
[:h6 { :font-size (em (* 1 header-size-factor)) }]
|
||||
|
||||
["#hero h1" { :font-size (em 2.5) }]
|
||||
["#hero h2" { :margin (em 2) } helvetica ]
|
||||
|
||||
[:article {
|
||||
:font-family (str text-font ", 'Georgia'")
|
||||
:margin-top (em 5)
|
||||
:text-align "justify"
|
||||
:flex 1
|
||||
:-webkit-flex 1
|
||||
}
|
||||
central-element]
|
||||
|
||||
(at-media {:screen true :min-width (px 1024)} [:article {:width width}])
|
||||
(at-media {:screen true :max-width (px 1023)} [:article {:width "90%"}])
|
||||
|
||||
[:.central-element central-element]
|
||||
|
||||
(at-media {:screen true :min-width (px 1024)} [:.central-element {:width width}])
|
||||
(at-media {:screen true :max-width (px 1023)} [:.central-element {:width "90%"}])
|
||||
|
||||
["article img" { :max-width "100%" }]
|
||||
|
||||
["article p" {
|
||||
:font-size (em (* 1.2 text-size-factor))
|
||||
:line-height "140%"
|
||||
}]
|
||||
|
||||
["article > h1:first-child" {
|
||||
:text-align "center"
|
||||
:font-size (em (* 2 header-size-factor))
|
||||
:margin (em 2)
|
||||
}]
|
||||
|
||||
[:.centered { :text-align "center" }]
|
||||
[:.bottom-space { :margin-bottom (em 7) }]
|
||||
[:code, :pre {
|
||||
:font-family "monospace"
|
||||
:background background-halftone
|
||||
:font-size (em (* 1.2 text-size-factor))
|
||||
}]
|
||||
|
||||
[:pre {
|
||||
:border-radius (px 3)
|
||||
:padding (em 0.5)
|
||||
:border (str "1px dotted" foreground-halftone)
|
||||
}]
|
||||
|
||||
["*:focus" { :outline "0px none transparent" }]
|
||||
(at-media {:screen true :min-width (px 1024)} [:textarea {:width width}])
|
||||
|
||||
[:textarea {
|
||||
:border-radius (px 5)
|
||||
:font-family "Courier"
|
||||
:font-size (em 1)
|
||||
:height (px 500)
|
||||
}]
|
||||
[:.hidden { :display "none" }]
|
||||
[:#dashed-line {
|
||||
:border-bottom (str "1px dashed" foreground-halftone)
|
||||
:margin-top (em 3)
|
||||
:margin-bottom (em 3)
|
||||
}]
|
||||
[:table {
|
||||
:width "100%"
|
||||
:border-collapse "collapse"
|
||||
}
|
||||
helvetica]
|
||||
[:th {
|
||||
:padding (em 0.3)
|
||||
:line-height (em 2.5)
|
||||
:background-color background-halftone
|
||||
}]
|
||||
[:td {
|
||||
:border-top (str "1px dotted" foreground-halftone)
|
||||
:padding (em 0.3)
|
||||
:line-height (em 2.5)
|
||||
}]
|
||||
[:.middot { :padding (em 0.5) }]
|
||||
|
||||
[:body { :display "-webkit-flex" }]
|
||||
|
||||
[:body {
|
||||
:display "flex"
|
||||
:min-height "100vh"
|
||||
:flex-direction "column"
|
||||
:-webkit-flex-direction "column"
|
||||
}]
|
||||
)))
|
||||
@@ -1,169 +0,0 @@
|
||||
(ns notehub.handler
|
||||
(:use
|
||||
compojure.core
|
||||
iokv.core
|
||||
notehub.views
|
||||
[clojure.string :rename {replace sreplace} :only [replace]])
|
||||
(:require
|
||||
[ring.adapter.jetty :as jetty]
|
||||
[clojure.core.cache :as cache]
|
||||
[hiccup.util :as util]
|
||||
[compojure.handler :as handler]
|
||||
[compojure.route :as route]
|
||||
[notehub.api :as api]
|
||||
[notehub.storage :as storage]
|
||||
[cheshire.core :refer :all])
|
||||
(:gen-class))
|
||||
|
||||
(defn current-timestamp []
|
||||
(quot (System/currentTimeMillis) 100000000))
|
||||
|
||||
; note page cache
|
||||
(def page-cache (atom (cache/lru-cache-factory {})))
|
||||
|
||||
; TODO: make sure the status is really set to the response!!!!
|
||||
(defn- response
|
||||
"Sets a custom message for each needed HTTP status.
|
||||
The message to be assigned is extracted with a dynamically generated key"
|
||||
[code]
|
||||
{:status code
|
||||
:body (let [message (get-message (keyword (str "status-" code)))]
|
||||
(layout :no-js {} message
|
||||
[:article [:h1 message]]))})
|
||||
|
||||
(defn redirect [url]
|
||||
{:status 302
|
||||
:headers {"Location" (str url)}
|
||||
:body ""})
|
||||
|
||||
(defn return-content-type [ctype content]
|
||||
{:headers {"Content-Type" ctype}
|
||||
:body content})
|
||||
|
||||
(defroutes api-routes
|
||||
(GET "/note" {params :params}
|
||||
(generate-string (api/version-manager api/get-note params)))
|
||||
|
||||
(POST "/note" {params :params}
|
||||
(generate-string (api/version-manager api/post-note params)))
|
||||
|
||||
(PUT "/note" {params :params}
|
||||
(generate-string (api/version-manager api/update-note params))))
|
||||
|
||||
(defroutes app-routes
|
||||
(GET "/api" [] (layout :no-js {} (get-message :api-title)
|
||||
[:article (md-to-html (slurp "API.md"))]))
|
||||
|
||||
(context "/api" []
|
||||
#(ring.util.response/content-type (api-routes %) "application/json"))
|
||||
|
||||
(GET "/" [] landing-page)
|
||||
|
||||
(POST "/propose-title" {body :body}
|
||||
(let [note (slurp body)]
|
||||
(return-content-type
|
||||
"text/plain; charset=utf-8"
|
||||
(api/propose-title note))))
|
||||
|
||||
(GET "/:year/:month/:day/:title/export" [year month day title]
|
||||
(when-let [md-text (:note (api/get-note {:noteID (api/build-key year month day title)}))]
|
||||
(return-content-type "text/plain; charset=utf-8" md-text)))
|
||||
|
||||
(GET "/:year/:month/:day/:title/stats" [year month day title]
|
||||
(let [note-id (api/build-key year month day title)]
|
||||
(statistics-page (api/derive-title (storage/get-note note-id))
|
||||
(storage/get-note-statistics note-id)
|
||||
(storage/get-publisher note-id))))
|
||||
|
||||
(GET "/:year/:month/:day/:title/edit" [year month day title]
|
||||
(let [note-id (api/build-key year month day title)]
|
||||
(note-update-page
|
||||
note-id
|
||||
(:note (api/get-note {:noteID note-id})))))
|
||||
|
||||
(GET "/new" [] (new-note-page
|
||||
(str
|
||||
(current-timestamp)
|
||||
(storage/sign (rand-int Integer/MAX_VALUE)))))
|
||||
|
||||
(GET "/:year/:month/:day/:title" [year month day title :as params]
|
||||
(let [params (assoc (:query-params params)
|
||||
:year year :month month :day day :title title)
|
||||
note-id (api/build-key year month day title)
|
||||
short-url (storage/create-short-url note-id params)]
|
||||
(when (storage/note-exists? note-id)
|
||||
(if (cache/has? @page-cache short-url)
|
||||
(do
|
||||
(swap! page-cache cache/hit short-url)
|
||||
(storage/increment-note-view note-id))
|
||||
(swap! page-cache cache/miss short-url
|
||||
(note-page (api/get-note {:noteID note-id})
|
||||
(api/url short-url)
|
||||
params)))
|
||||
(cache/lookup @page-cache short-url))))
|
||||
|
||||
(GET "/:short-url" [short-url]
|
||||
(when-let [params (storage/resolve-url short-url)]
|
||||
(let [{:keys [year month day title]} params
|
||||
rest-params (dissoc params :year :month :day :title)
|
||||
core-url (api/url year month day title)
|
||||
long-url (if (empty? rest-params) core-url (util/url core-url rest-params))]
|
||||
(redirect long-url))))
|
||||
|
||||
(POST "/post-note" [session note signature password]
|
||||
(if (and session
|
||||
(.startsWith session
|
||||
(str (current-timestamp)))
|
||||
(= signature (storage/sign session note)))
|
||||
(let [pid "NoteHub"
|
||||
psk (storage/get-psk pid)
|
||||
params {:note note
|
||||
:pid pid
|
||||
:signature (storage/sign pid psk note)
|
||||
:password password}]
|
||||
(if (storage/valid-publisher? pid)
|
||||
(let [resp (api/post-note params)]
|
||||
(if (get-in resp [:status :success])
|
||||
(redirect (:longURL resp))
|
||||
(response 400)))
|
||||
(response 500)))
|
||||
(response 400)))
|
||||
|
||||
(POST "/update-note" [noteID note password]
|
||||
(let [pid "NoteHub"
|
||||
psk (storage/get-psk pid)
|
||||
params {:noteID noteID :note note :password password :pid pid}]
|
||||
(if (storage/valid-publisher? pid)
|
||||
(let [resp (api/update-note (assoc params
|
||||
:signature
|
||||
(storage/sign pid psk noteID note password)))]
|
||||
(if (get-in resp [:status :success])
|
||||
(do
|
||||
(doseq [url (storage/get-short-urls noteID)]
|
||||
(swap! page-cache cache/evict url))
|
||||
(redirect (:longURL resp)))
|
||||
(response 403)))
|
||||
(response 500))))
|
||||
|
||||
(route/resources "/")
|
||||
(route/not-found (response 404)))
|
||||
|
||||
(def app
|
||||
(let [handler (handler/site app-routes)]
|
||||
(fn [request]
|
||||
(let [{:keys [server-name server-port]} request
|
||||
hostURL (str "https://" server-name
|
||||
(when (not= 80 server-port) (str ":" server-port)))
|
||||
request (assoc-in request [:params :hostURL] hostURL)]
|
||||
(if (get-setting :dev-mode)
|
||||
(handler request)
|
||||
(try (handler request)
|
||||
(catch Exception e
|
||||
(do
|
||||
;TODO (log e)
|
||||
(response 500)))))))))
|
||||
|
||||
(defn -main [& [port]]
|
||||
(jetty/run-jetty #'app
|
||||
{:port (if port (Integer/parseInt port) 8080)}))
|
||||
|
||||
@@ -1,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"))))
|
||||
@@ -1,124 +0,0 @@
|
||||
(ns notehub.views
|
||||
(:use
|
||||
iokv.core
|
||||
[clojure.string :rename {replace sreplace} :only [replace]]
|
||||
[hiccup.form]
|
||||
[hiccup.core]
|
||||
[hiccup.element]
|
||||
[hiccup.util :only [escape-html]]
|
||||
[hiccup.page :only [include-js html5]])
|
||||
(:require [notehub.css :as css])
|
||||
(:import (org.pegdown PegDownProcessor Extensions)))
|
||||
|
||||
(def get-message (get-map "messages"))
|
||||
|
||||
(def md-processor
|
||||
(PegDownProcessor. (int (bit-and-not Extensions/ALL Extensions/HARDWRAPS))))
|
||||
|
||||
(defn md-to-html [md-text]
|
||||
(.markdownToHtml md-processor md-text))
|
||||
|
||||
; Creates the main html layout
|
||||
(defn layout
|
||||
[js? style title & content]
|
||||
(html5
|
||||
[:head
|
||||
[:title (print-str (get-message :name) "—" title)]
|
||||
[:meta {:charset "UTF-8"}]
|
||||
[:meta {:name "viewport" :content "width=device-width, initial-scale=1.0"}]
|
||||
[:link {:rel "stylesheet" :type "text/css"
|
||||
:href
|
||||
(format "https://fonts.googleapis.com/css?family=PT+Serif:700|Noticia+Text:700%s&subset=latin,cyrillic"
|
||||
(reduce (fn [acc e]
|
||||
(if-let [font (style e)]
|
||||
(str acc "|" (sreplace font #" " "+"))
|
||||
acc)) "" ["text-font" "header-font"]))}]
|
||||
[:style (css/generate style)]
|
||||
(if (= :js js?)
|
||||
(html
|
||||
(include-js "//cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js")
|
||||
(include-js "//cdnjs.cloudflare.com/ajax/libs/blueimp-md5/1.0.1/js/md5.min.js")
|
||||
(include-js "/js/publishing.js")
|
||||
[:body {:onload "onLoad()"} content])
|
||||
[:body content])]))
|
||||
|
||||
(defn- sanitize
|
||||
"Breakes all usages of <script> & <iframe>"
|
||||
[input]
|
||||
(sreplace input #"(</?(iframe|script).*?>|javascript:)" ""))
|
||||
|
||||
; input form for the markdown text with a preview area
|
||||
(defn- input-form [form-url command fields content passwd-msg]
|
||||
(let [css-class (when (= :publish command) :hidden)]
|
||||
(layout :js {} (get-message :new-page)
|
||||
[:article#preview {:style "flex: none; -webkit-flex: none"} ""]
|
||||
[:div#dashed-line {:class css-class}]
|
||||
[:div.central-element.helvetica {:style "margin-bottom: 3em"}
|
||||
(form-to {:autocomplete :off} [:post form-url]
|
||||
(hidden-field :action command)
|
||||
(hidden-field :password)
|
||||
fields
|
||||
(text-area :note content)
|
||||
[:fieldset#input-elems {:class css-class}
|
||||
(text-field {:class "ui-elem"
|
||||
:placeholder (get-message passwd-msg)}
|
||||
:plain-password) " "
|
||||
(submit-button {:class "button ui-elem"
|
||||
:id :publish-button} (get-message command))
|
||||
[:br]
|
||||
[:br]
|
||||
[:div#proposed-title {:style "color: #aaa; font-size: 0.8em"}]])])))
|
||||
|
||||
(def landing-page
|
||||
(layout :no-js {} (get-message :page-title)
|
||||
[:div#hero
|
||||
[:h1 (get-message :name)]
|
||||
[:h2 (get-message :title)]
|
||||
[:br]
|
||||
[:a.landing-button {:href "/new" :style "color: white"} (get-message :new-page)]]
|
||||
[:div#dashed-line]
|
||||
[:article.helvetica.bottom-space
|
||||
{:style "font-size: 1em"}
|
||||
(md-to-html (slurp "LANDING.md"))]
|
||||
[:div#footer (md-to-html (get-message :footer))]))
|
||||
|
||||
|
||||
(defn statistics-page [note-title stats publisher]
|
||||
(let [page-title (get-message :statistics)
|
||||
info (assoc stats :publisher publisher)]
|
||||
(layout :no-js {} page-title
|
||||
[:div.central-element.helvetica {:style "width: 80%"}
|
||||
[:h2 note-title]
|
||||
[:h3.helvetica page-title]
|
||||
[:table#stats
|
||||
(map
|
||||
#(when-let [v (% info)]
|
||||
[:tr
|
||||
[:td (str (get-message %) ":")]
|
||||
[:td (if (or (= % :published) (= % :edited))
|
||||
(str (java.util.Date. (Long/parseLong v))) v)]])
|
||||
[:published :edited :publisher :views])]])))
|
||||
|
||||
(defn note-update-page [note-id note]
|
||||
(input-form "/update-note"
|
||||
:update
|
||||
(html (hidden-field :noteID note-id))
|
||||
note
|
||||
:enter-passwd))
|
||||
|
||||
(defn new-note-page [session]
|
||||
(input-form "/post-note" :publish
|
||||
(html (hidden-field :session session)
|
||||
(hidden-field {:id :signature} :signature))
|
||||
(get-message :loading) :set-passwd))
|
||||
|
||||
(defn note-page [note short-url css-params]
|
||||
(layout :no-js css-params (:title note)
|
||||
[:article.bottom-space (md-to-html (sanitize (:note note)))]
|
||||
(let [urls {:short-url short-url
|
||||
:notehub "/"}
|
||||
links (map #(link-to
|
||||
(urls % (str (:longURL note) "/" (name %)))
|
||||
(get-message %))
|
||||
[:notehub :stats :edit :export :short-url])]
|
||||
[:div#footer (interpose [:span.middot "·"] links)])))
|
||||
@@ -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
@@ -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 +0,0 @@
|
||||
java.runtime.version=1.8
|
||||
@@ -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)))))))
|
||||
@@ -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)))))
|
||||
@@ -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"))))))
|
||||
Reference in New Issue
Block a user