32 changed files with 769 additions and 1670 deletions
@ -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 |
|
||||||
|
|||||||
@ -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,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). |
|
||||||
@ -1 +0,0 @@ |
|||||||
web: lein with-profile production ring server-headless $PORT |
|
||||||
@ -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" |
||||||
|
} |
||||||
|
} |
||||||
@ -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 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(); |
||||||
} |
} |
||||||
|
|||||||
@ -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(); |
||||||
|
}); |
||||||
@ -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,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")))))) |
|
||||||
Loading…
Reference in new issue