preparing for 3.0
This commit is contained in:
@@ -1,28 +1,12 @@
|
||||
# README
|
||||
|
||||
> "Make every detail perfect and limit the number of details to perfect."
|
||||
> — _Jack Dorsey_
|
||||
|
||||
## About
|
||||
|
||||
[NoteHub](https://www.notehub.org) is a free and hassle-free pastebin for one-off markdown publishing and was implemented as a one-app-one-language [experiment](https://notehub.org/ikqz8).
|
||||
|
||||
## Implementation
|
||||
|
||||
NoteHub's implementation aims for ultimate simplicity and performance.
|
||||
|
||||
### Design
|
||||
|
||||
1. At a request, the server first checks, if a note is already rendered and is present in the LRU cache.
|
||||
(a) If _yes_, it return the rendered HTML code and increases the views counter.
|
||||
(b) Otherwise, the note is retrieved from the DB, rendered and put into the LRU cache; the views counter will be increased.
|
||||
2. The rendering of note pages: there are HTML file tempates with placeholders, which will be trivially filled with replacements.
|
||||
3. The LRU cache holds the rendered HTML for the most popular notes, which makes their access a static O(1) operation without any DB I/O.
|
||||
4. The server keeps corresponding models for all notes in the cache for statistics updates. These models are persisted every 5 minutes to the DB.
|
||||
Dead simple hosting for markdown notes.
|
||||
|
||||
## Installation & Deployment
|
||||
|
||||
To run NoteHub as a standalone version, execute:
|
||||
|
||||
1. `git clone https://github.com/chmllr/NoteHub.git`
|
||||
2. `npm install`
|
||||
3. `npm start`
|
||||
|
||||
This starts a NoteHub instance on port 3000.
|
||||
TBD
|
||||
|
||||
-234
@@ -1,234 +0,0 @@
|
||||
var frisby = require('frisby');
|
||||
var md5 = require('md5');
|
||||
|
||||
frisby.create('Landing page')
|
||||
.get('http://localhost:3000/')
|
||||
.expectStatus(200)
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.expectBodyContains('Hassle-free')
|
||||
.toss();
|
||||
|
||||
frisby.create('Open note page')
|
||||
.get('http://localhost:3000/new')
|
||||
.expectStatus(200)
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.expectBodyContains('Terms of Service')
|
||||
.toss();
|
||||
|
||||
frisby.create('Open TOS')
|
||||
.get('http://localhost:3000/TOS')
|
||||
.expectStatus(200)
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.expectBodyContains('Site Terms of Use Modifications')
|
||||
.toss();
|
||||
|
||||
frisby.create('Incurrect URL')
|
||||
.get('http://localhost:3000/abcdef')
|
||||
.expectStatus(404)
|
||||
.expectBodyContains('Not found')
|
||||
.toss();
|
||||
|
||||
frisby.create('Invalid posting')
|
||||
.post('http://localhost:3000/note')
|
||||
.expectStatus(400)
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.expectBodyContains('Bad request')
|
||||
.toss();
|
||||
|
||||
let testNote = 'This is a test note';
|
||||
|
||||
frisby.create('Invalid posting 2')
|
||||
.post('http://localhost:3000/note', {
|
||||
action: 'POST',
|
||||
note: testNote
|
||||
})
|
||||
.expectStatus(400)
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.expectBodyContains('Bad request')
|
||||
.toss();
|
||||
|
||||
frisby.create('Invalid posting 3')
|
||||
.post('http://localhost:3000/note', {
|
||||
action: 'POST',
|
||||
session: md5('new'),
|
||||
signature: 'assdss',
|
||||
note: testNote
|
||||
})
|
||||
.expectStatus(400)
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.expectBodyContains('Signature mismatch')
|
||||
.toss();
|
||||
|
||||
frisby.create('Valid posting')
|
||||
.post('http://localhost:3000/note', {
|
||||
action: 'POST',
|
||||
session: md5('new'),
|
||||
signature: md5(md5('new') + testNote),
|
||||
password: '',
|
||||
note: testNote
|
||||
})
|
||||
.expectStatus(302)
|
||||
.expectBodyContains('Found. Redirecting to')
|
||||
.expectHeaderContains('content-type', 'text/plain; charset=utf-8')
|
||||
.after(function(err, res, body) {
|
||||
let noteId = body.replace('Found. Redirecting to /', '');
|
||||
frisby.create('Read posted note')
|
||||
.get('http://localhost:3000/' + noteId)
|
||||
.expectStatus(200)
|
||||
.expectBodyContains(testNote)
|
||||
.after((err, res, body) => {
|
||||
frisby.create('Illegal note editing attempt with empty password')
|
||||
.post('http://localhost:3000/note', {
|
||||
id: noteId,
|
||||
action: 'UPDATE',
|
||||
session: md5('new'),
|
||||
signature: md5(md5('new') + testNote+'!!!'),
|
||||
note: testNote + '!!!',
|
||||
password: ''
|
||||
})
|
||||
.expectStatus(400)
|
||||
.expectBodyContains('Password is wrong')
|
||||
.toss();
|
||||
})
|
||||
.after((err, res, body) => {
|
||||
frisby.create('Illegal note editing attempt')
|
||||
.post('http://localhost:3000/note', {
|
||||
id: noteId,
|
||||
action: 'UPDATE',
|
||||
session: md5('new'),
|
||||
signature: md5(md5('new') + testNote+'!!!'),
|
||||
note: testNote + '!!!',
|
||||
password: 'aaabbb'
|
||||
})
|
||||
.expectStatus(400)
|
||||
.expectBodyContains('Password is wrong')
|
||||
.toss();
|
||||
|
||||
})
|
||||
.toss();
|
||||
})
|
||||
.toss();
|
||||
|
||||
frisby.create('Valid posting, editing and removal')
|
||||
.post('http://localhost:3000/note', {
|
||||
action: 'POST',
|
||||
session: md5('new'),
|
||||
signature: md5(md5('new') + testNote),
|
||||
password: 'aabbcc',
|
||||
note: testNote
|
||||
})
|
||||
.expectStatus(302)
|
||||
.expectBodyContains('Found. Redirecting to')
|
||||
.expectHeaderContains('content-type', 'text/plain; charset=utf-8')
|
||||
.after(function(err, res, body) {
|
||||
var noteId = body.replace('Found. Redirecting to /', '');
|
||||
frisby.create('Export posted note')
|
||||
.get('http://localhost:3000/' + noteId + '/export')
|
||||
.expectStatus(200)
|
||||
.expectHeaderContains('content-type', 'text/plain; charset=utf-8')
|
||||
.expectBodyContains(testNote)
|
||||
.toss();
|
||||
frisby.create('Read posted note')
|
||||
.get('http://localhost:3000/' + noteId)
|
||||
.expectStatus(200)
|
||||
.expectBodyContains(testNote)
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.after((err, res, body) => {
|
||||
frisby.create('Unauthorized note editing attempt')
|
||||
.post('http://localhost:3000/note', {
|
||||
id: noteId,
|
||||
action: 'UPDATE',
|
||||
session: md5('new'),
|
||||
signature: md5(md5('new') + testNote+'!!!'),
|
||||
note: testNote + '!!!',
|
||||
password: 'abbcc'
|
||||
})
|
||||
.expectStatus(400)
|
||||
.expectBodyContains('Password is wrong')
|
||||
.toss();
|
||||
})
|
||||
.after((err, res, body) => {
|
||||
frisby.create('Valid note editing attempt')
|
||||
.post('http://localhost:3000/note', {
|
||||
id: noteId,
|
||||
action: 'UPDATE',
|
||||
session: md5('new'),
|
||||
signature: md5(md5('new') + 'Changed!'),
|
||||
note: 'Changed!',
|
||||
password: 'aabbcc'
|
||||
})
|
||||
.expectStatus(302)
|
||||
.after((err, res, body) => {
|
||||
frisby.create('Read changed note')
|
||||
.get('http://localhost:3000/' + noteId)
|
||||
.expectStatus(200)
|
||||
.expectBodyContains('Changed!')
|
||||
.toss();
|
||||
})
|
||||
.after((err, res, body) => {
|
||||
frisby.create('Delete posted note')
|
||||
.post('http://localhost:3000/note',{
|
||||
id: noteId,
|
||||
button: 'Delete',
|
||||
action: 'UPDATE',
|
||||
session: md5('new'),
|
||||
signature: md5(md5('new') + 'Changed!'),
|
||||
note: 'Changed!',
|
||||
password: 'aabbcc'
|
||||
})
|
||||
.expectStatus(200)
|
||||
.expectBodyContains('Note deleted')
|
||||
.toss();
|
||||
})
|
||||
.toss();
|
||||
})
|
||||
.toss();
|
||||
frisby.create('Read stats of posted note')
|
||||
.expectStatus(200)
|
||||
.get('http://localhost:3000/' + noteId).toss();
|
||||
frisby.create('Read stats of posted note')
|
||||
.expectStatus(200)
|
||||
.get('http://localhost:3000/' + noteId).toss();
|
||||
frisby.create('Read stats of posted note')
|
||||
.expectStatus(200)
|
||||
.get('http://localhost:3000/' + noteId).toss();
|
||||
frisby.create('Read stats of posted note')
|
||||
.get('http://localhost:3000/' + noteId + '/stats')
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.expectStatus(200)
|
||||
.expectBodyContains('Statistics')
|
||||
.expectBodyContains('<tr><td>Views</td><td>4</td></tr>')
|
||||
.toss();
|
||||
})
|
||||
.toss();
|
||||
|
||||
var tooLongNote = 'ABCD';
|
||||
|
||||
while (tooLongNote.length < 1024*200) tooLongNote += tooLongNote;
|
||||
|
||||
frisby.create('Invalid posting of too long note')
|
||||
.post('http://localhost:3000/note', {
|
||||
action: 'POST',
|
||||
session: md5('new'),
|
||||
signature: md5(md5('new') + testNote),
|
||||
password: 'aabbcc',
|
||||
note: tooLongNote
|
||||
})
|
||||
.expectStatus(400)
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.expectBodyContains('too large')
|
||||
.toss();
|
||||
|
||||
frisby.create('Invalid update without id')
|
||||
.post('http://localhost:3000/note', {
|
||||
action: 'UPDATE',
|
||||
session: md5('new'),
|
||||
signature: md5(md5('new') + 'Any note'),
|
||||
password: 'aabbcc',
|
||||
note: 'Any note'
|
||||
})
|
||||
.expectStatus(400)
|
||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
|
||||
.expectBodyContains('Wrong note ID')
|
||||
.toss();
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
<a href="%LINK%/stats">statistics</a> ·
|
||||
<a href="%LINK%/edit">edit</a> ·
|
||||
<a href="%LINK%/export">export</a> ·
|
||||
<a href="/TOS">terms of service</a>
|
||||
</footer>
|
||||
<a href="/TOS.md">terms of service</a>
|
||||
</footer>
|
||||
@@ -0,0 +1,36 @@
|
||||
# Demo Note
|
||||
|
||||
## Text Formatting
|
||||
|
||||
This is a _short_ note demonstrating the **capabilities** of Markdown. [Markdown](http://en.wikipedia.org/wiki/Markdown) is a _markup language_ with plain text
|
||||
formatting syntax. But you also can use <u>standard HTML</u> tags.
|
||||
|
||||
## Backquotes, Lists & Code
|
||||
|
||||
This is a backquote:
|
||||
|
||||
> _"Our greatest glory is not in never falling but in rising every time we fall."_
|
||||
> — Confucius
|
||||
|
||||
To create simple lists, just enumerate all items using a dash in the prefix:
|
||||
|
||||
- Alpha
|
||||
- Beta
|
||||
- Gamma
|
||||
|
||||
Also you can either mark some special `words` or write entire `code` blocks:
|
||||
|
||||
(defn fact [n]
|
||||
(if (< n 2) 1
|
||||
(* n (fact (- n 1)))))
|
||||
|
||||
## Tables
|
||||
|
||||
Also simple tables is a piece of cake:
|
||||
|
||||
Column 1 | Column 2 | Column 3
|
||||
--- | --- | ---
|
||||
Text 1 | Text 3 | <s>Text 5</s>
|
||||
Text 2 | Text 4 | <mark>Text 6</mark>
|
||||
|
||||
Take a look at the [source code](/Demo.md/export) of this page, to see how it works.
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -11,13 +11,14 @@
|
||||
<h1>NoteHub</h1>
|
||||
<h2>Free and Hassle-free Pastebin for Markdown Notes</h2>
|
||||
<br>
|
||||
<a class="landing-button demo" href="/demo" style="color: white">See Demo Note</a>
|
||||
<a class="landing-button demo" href="/Demo.md" 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="bottom-space">
|
||||
<h2>Changelog</h2>
|
||||
<ul>
|
||||
<li><strong>2017-08</strong>: NoteHub 3.0 released.</li>
|
||||
<li><strong>2016-08</strong>: Syntax highlighting for code blocks added (thanks to <a href="https://github.com/maciejsmolinski">Maciej</a>).</li>
|
||||
<li><strong>2016-03</strong>: Note deletion feature added.</li>
|
||||
<li><strong>2015-10</strong>: NoteHub rewritten in Node.js.</li>
|
||||
@@ -34,7 +35,7 @@
|
||||
</article>
|
||||
<footer>
|
||||
<a href="https://github.com/chmllr/NoteHub">source code</a> ·
|
||||
<a href="/TOS">terms of service</a>
|
||||
<a href="/TOS.md">terms of service</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "NoteHub",
|
||||
"version": "3.0.0",
|
||||
"description": "Free Pastebin for One-Off Markdown Publishing",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "./node_modules/nodemon/bin/nodemon.js server.js",
|
||||
"test": "jasmine-node ."
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chmllr/NoteHub.git"
|
||||
},
|
||||
"keywords": [
|
||||
"markdown",
|
||||
"pastebin"
|
||||
],
|
||||
"author": "Christian Müller <notehub@icloud.com> (http://twitter.com/drmllr)",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/chmllr/NoteHub/issues"
|
||||
},
|
||||
"homepage": "https://github.com/chmllr/NoteHub",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.15.0",
|
||||
"express": "^4.13.4",
|
||||
"highlight.js": "^9.5.0",
|
||||
"lru-cache": "^4.0.0",
|
||||
"marked": "^0.3.5",
|
||||
"md5": "^2.1.0",
|
||||
"sequelize": "^3.19.3",
|
||||
"sqlite3": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"frisby": "^0.8.5",
|
||||
"nodemon": "^1.9.1"
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html style="height: 100%;">
|
||||
|
||||
<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="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/highlight.min.js" type="text/javascript"></script>
|
||||
<script src="/js/publishing.js" type="text/javascript"></script>
|
||||
<base target="_blank">
|
||||
</head>
|
||||
|
||||
<body onload="onLoad()" style="height: 100%">
|
||||
<div id="editContainer">
|
||||
<form id="editPane" action="/note" autocomplete="off" method="POST" target="_self">
|
||||
<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 id="input-elems">
|
||||
<input class="ui-elem" id="plain-password" name="plain-password"
|
||||
placeholder="Password for editing" type="text" autocomplete="off">
|
||||
<label style="margin-right: 1em">
|
||||
<input id="tos" type="checkbox" onClick="enableButton()">
|
||||
Accept <a href="/TOS">Terms of Service</a>
|
||||
</label>
|
||||
<input class="button ui-elem" disabled id="publish-button" name="button" type="submit" value="Publish">
|
||||
<input class="button ui-elem" id="delete-button" name="button" type="submit" value="Delete">
|
||||
<span id="tableau">
|
||||
0 words
|
||||
</span>
|
||||
<br>
|
||||
<br>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div id="previewPane"><article id="draft"></article></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,73 +0,0 @@
|
||||
var $ = function(id) {
|
||||
return document.getElementById(id);
|
||||
};
|
||||
var iosDetected = navigator.userAgent.match('(iPad|iPod|iPhone)');
|
||||
var timer = null;
|
||||
var timerDelay = iosDetected ? 800 : 400;
|
||||
var $note, $action, $preview, $plain_password, $tableau;
|
||||
var backendTimer;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
marked.setOptions({
|
||||
langPrefix: 'hljs lang-',
|
||||
highlight: function (code) {
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
function md2html(input) {
|
||||
return marked(input);
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
if ($action == 'UPDATE') return;
|
||||
console.log('draft autosave...');
|
||||
$tableau.innerHTML = 'Draft autosaved.';
|
||||
localStorage.setItem('draft', $note.value);
|
||||
}
|
||||
|
||||
function enableButton() {
|
||||
var checkbox = $('tos');
|
||||
var button = $('publish-button');
|
||||
button.disabled = !checkbox.checked;
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
$note = $('note');
|
||||
$action = $('action').value;
|
||||
$preview = $('draft');
|
||||
$tableau = $('tableau');
|
||||
$plain_password = $('plain-password');
|
||||
var updatePreview = function() {
|
||||
clearTimeout(timer);
|
||||
var content = $note.value;
|
||||
var delay = Math.min(timerDelay, timerDelay * (content.length / 400));
|
||||
timer = setTimeout(function() {
|
||||
$preview.innerHTML = md2html(content);
|
||||
$tableau.innerHTML = content.split(/\s+/).length + ' words';
|
||||
}, delay);
|
||||
};
|
||||
if ($action == 'UPDATE') updatePreview();
|
||||
else {
|
||||
$('delete-button').style.display = 'none';
|
||||
$note.value = '';
|
||||
var draft = localStorage.getItem('draft');
|
||||
if (draft) {
|
||||
$note.value = draft;
|
||||
updatePreview();
|
||||
}
|
||||
}
|
||||
$note.onkeyup = updatePreview;
|
||||
$('delete-button').onclick = $('publish-button').onclick = function(e) {
|
||||
localStorage.removeItem('draft');
|
||||
self.onbeforeunload = null;
|
||||
if ($plain_password.value !== '') $('password').value = md5($plain_password.value);
|
||||
$plain_password.value = null;
|
||||
$('signature').value = md5($('session').value + $note.value.replace(/[\n\r]/g, ''));
|
||||
};
|
||||
if (iosDetected) $note.className += ' ui-border';
|
||||
else $note.focus();
|
||||
self.onbeforeunload = saveDraft;
|
||||
setInterval(saveDraft, 60 * 1000);
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
var express = require('express');
|
||||
var view = require('./view');
|
||||
var storage = require('./storage');
|
||||
var md5 = require('md5');
|
||||
var LRU = require('lru-cache');
|
||||
var bodyParser = require('body-parser');
|
||||
var fs = require('fs');
|
||||
var blackList = new Set();
|
||||
|
||||
var app = express();
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true, limit: '200kb' }));
|
||||
app.use(express.static(__dirname + '/resources/public'));
|
||||
app.use(function (error, req, res, next) {
|
||||
if (error) {
|
||||
sendResponse(res, 400, 'Bad request', error.message);
|
||||
log('REQUEST ERROR:', error);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
var MODELS = {};
|
||||
var CACHE = new LRU({
|
||||
max: 50,
|
||||
dispose: key => {
|
||||
log('disposing', key, 'from cache');
|
||||
var model = MODELS[key];
|
||||
if (model) model.save();
|
||||
delete MODELS[key];
|
||||
}
|
||||
});
|
||||
|
||||
var log = function() {
|
||||
var date = new Date();
|
||||
var timestamp = date.getDate() + '/' + date.getMonth() + ' ' + date.getHours() + ':' +
|
||||
date.getMinutes() + ':' + date.getSeconds() + '.' + date.getMilliseconds();
|
||||
var message = Array.prototype.slice.call(arguments);
|
||||
message.unshift('--');
|
||||
message.unshift(timestamp);
|
||||
console.log.apply(console, message);
|
||||
};
|
||||
|
||||
app.get('/TOS', (req, res) => res.send(view.renderTOS()));
|
||||
|
||||
app.get('/new', (req, res) => {
|
||||
log(req.ip, 'opens /new');
|
||||
res.send(view.newNotePage(md5('new')));
|
||||
});
|
||||
|
||||
app.post('/note', (req, res) => {
|
||||
var body = req.body,
|
||||
session = body.session,
|
||||
note = body.note,
|
||||
password = body.password,
|
||||
action = body.action,
|
||||
id = body.id;
|
||||
log(req.ip, 'calls /note to', action, id);
|
||||
var goToNote = note => res.redirect('/' + note.id);
|
||||
if (!note || !session || session.indexOf(md5('edit/' + id)) !== 0 && session.indexOf(md5('new')) !== 0)
|
||||
return sendResponse(res, 400, 'Bad request');
|
||||
if (body.signature != md5(session + note.replace(/[\n\r]/g, '')))
|
||||
return sendResponse(res, 400, 'Signature mismatch');
|
||||
if (action == 'POST')
|
||||
storage.addNote(note, password).then(goToNote);
|
||||
else {
|
||||
if (!id) return sendResponse(res, 400, 'Wrong note ID');
|
||||
CACHE.del(id);
|
||||
if (body.button == 'Delete') {
|
||||
log('deleting note', id);
|
||||
storage.deleteNote(id, password).then(
|
||||
() => sendResponse(res, 200, 'Note deleted'),
|
||||
error => sendResponse(res, 400, 'Bad request', error.message));
|
||||
} else {
|
||||
log('updating note', id);
|
||||
storage.updateNote(id, password, note).then(goToNote,
|
||||
error => sendResponse(res, 400, 'Bad request', error.message));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/:year/:month/:day/:title', (req, res) => {
|
||||
var P = req.params, url = P.year + '/' + P.month + '/' + P.day + '/' + P.title;
|
||||
log(req.ip, 'resolves deprecated id', url);
|
||||
if (CACHE.has(url)) {
|
||||
log(url, 'is cached!');
|
||||
var id = CACHE.get(url);
|
||||
if (id) res.redirect('/' + id);
|
||||
else notFound(res);
|
||||
} else storage.getNoteId(url).then(note => {
|
||||
log(url, 'is not cached, resolving...');
|
||||
if (note) {
|
||||
CACHE.set(url, note.id);
|
||||
res.redirect('/' + note.id);
|
||||
} else {
|
||||
CACHE.set(url, null);
|
||||
notFound(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get(/\/([a-z0-9]+)\/edit/, (req, res) => {
|
||||
var id = req.params['0'];
|
||||
log(req.ip, 'calls /edit on', id);
|
||||
storage.getNote(id).then(note => res.send(note ?
|
||||
view.editNotePage(md5('edit/' + id), note) :
|
||||
notFound(res)));
|
||||
});
|
||||
|
||||
app.get(/\/([a-z0-9]+)\/export/, (req, res) => {
|
||||
var id = req.params['0'];
|
||||
log(req.ip, 'calls /export on', id);
|
||||
res.set({ 'Content-Type': 'text/plain', 'Charset': 'utf-8' });
|
||||
storage.getNote(id).then(note => note ?
|
||||
res.send(note.text) :
|
||||
notFound(res));
|
||||
});
|
||||
|
||||
app.get(/\/([a-z0-9]+)\/stats/, (req, res) => {
|
||||
var id = req.params['0'];
|
||||
log(req.ip, 'calls /stats on', id);
|
||||
var promise = id in MODELS ?
|
||||
new Promise(resolve => resolve(MODELS[id])) :
|
||||
storage.getNote(id);
|
||||
promise.then(note => note ?
|
||||
res.send(view.renderStats(note)) :
|
||||
notFound(res));
|
||||
});
|
||||
|
||||
app.get(/\/([a-z0-9]+)/, (req, res) => {
|
||||
var id = req.params['0'];
|
||||
log(req.ip, 'open note', id, 'from', req.get('Referer'));
|
||||
if (CACHE.has(id)) {
|
||||
log(id, 'is cached!');
|
||||
var obj = MODELS[id];
|
||||
if (!obj) return notFound(res);
|
||||
obj.views++;
|
||||
res.send(CACHE.get(id));
|
||||
} else storage.getNote(id).then(note => {
|
||||
log(id, 'is not cached, resolving...');
|
||||
if (!note) {
|
||||
CACHE.set(id, null);
|
||||
return notFound(res);
|
||||
}
|
||||
var content = view.renderNote(note, blackList);
|
||||
CACHE.set(id, content);
|
||||
MODELS[id] = note;
|
||||
note.views++;
|
||||
res.send(content);
|
||||
});
|
||||
});
|
||||
|
||||
var sendResponse = (res, code, message, details) => {
|
||||
log('sending response', code, message);
|
||||
res.status(code).send(view.renderPage(null, message,
|
||||
`<h1>${message}</h1><br/>` +
|
||||
`<center>${details || '¯\\_(ツ)_/¯'}</center>`, ''));
|
||||
};
|
||||
|
||||
var notFound = res => sendResponse(res, 404, 'Not found');
|
||||
|
||||
var server = app.listen(process.env.PORT || 3000,
|
||||
() => log('NoteHub server listening on port', server.address().port));
|
||||
|
||||
setInterval(() => {
|
||||
var keys = Object.keys(MODELS);
|
||||
log('saving stats for', keys.length, 'models...');
|
||||
keys.forEach(id => MODELS[id].save());
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
var updateBlackList = () => {
|
||||
var ids = fs.readFileSync(process.env.BLACK_LIST || '/dev/null', 'utf-8').split(/\n+/).filter(Boolean);
|
||||
ids.filter(id => !blackList.has(id)).forEach(id => CACHE.del(id));
|
||||
blackList = new Set(ids);
|
||||
log('black list updated, entries:', blackList.size);
|
||||
};
|
||||
|
||||
setInterval(updateBlackList, 60 * 60 * 1000);
|
||||
|
||||
updateBlackList();
|
||||
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
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 }
|
||||
});
|
||||
|
||||
sequelize.sync();
|
||||
|
||||
module.exports.getNote = id => Note.findById(id);
|
||||
|
||||
module.exports.getNoteId = deprecatedId => Note.findOne({
|
||||
where: { deprecatedId: deprecatedId }
|
||||
});
|
||||
|
||||
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
|
||||
}));
|
||||
|
||||
var passwordCheck = (note, password, callback) =>
|
||||
(!note || note.password.length === 0 || note.password !== password) ?
|
||||
new Promise((resolve, reject) => reject({ message: 'Password is wrong' })) :
|
||||
callback();
|
||||
|
||||
module.exports.updateNote = (id, password, text) =>
|
||||
Note.findById(id).then(note =>
|
||||
passwordCheck(note, password, () => {
|
||||
note.text = text;
|
||||
note.edited = new Date();
|
||||
return note.save();
|
||||
}));
|
||||
|
||||
module.exports.deleteNote = (id, password) =>
|
||||
Note.findById(id).then(note =>
|
||||
passwordCheck(note, password, () => note.destroy()));
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
var marked = require('marked');
|
||||
var fs = require('fs');
|
||||
var hljs = require('highlight.js');
|
||||
|
||||
var TOS = fs.readFileSync('resources/TOS.md', 'utf-8');
|
||||
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 header = fs.readFileSync(process.env.HEADER || '/dev/null', 'utf-8');
|
||||
|
||||
var deriveTitle = text => text
|
||||
.split(/[\n\r]/)[0].slice(0,25)
|
||||
.replace(/[`~!@#\$%^&\*_|\+=\?;:'",.<>\{\}\\\/]/g, '');
|
||||
|
||||
var renderPage = (id, title, content, footer, blackList) => pageTemplate
|
||||
.replace('%HEADER%', blackList && blackList.has(id) ? header : '')
|
||||
.replace('%TITLE%', title)
|
||||
.replace('%CONTENT%', content.replace(/<meta.*?>/gi, '').replace(/<script[\s\S.]*?\/script>/gi, ''))
|
||||
.replace('%FOOTER%', footer || '');
|
||||
|
||||
marked.setOptions({
|
||||
langPrefix: 'hljs lang-',
|
||||
highlight: code => hljs.highlightAuto(code).value,
|
||||
});
|
||||
|
||||
module.exports.renderPage = renderPage;
|
||||
|
||||
module.exports.renderStats = note => renderPage(note.id, 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.renderTOS = () => renderPage('tos', 'Terms of Service', marked(TOS));
|
||||
|
||||
module.exports.renderNote = (note, blackList) => renderPage(note.id,
|
||||
deriveTitle(note.text),
|
||||
marked(note.text),
|
||||
footerTemplate.replace(/%LINK%/g, note.id),
|
||||
blackList);
|
||||
|
||||
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%', escape$(note.text));
|
||||
|
||||
var escape$ = s => s.split('').map(chr => chr == '$' ? '$$' : chr).join('');
|
||||
Reference in New Issue
Block a user