diff --git a/.gitignore b/.gitignore index e653c91..ed50308 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,6 @@ -Makefile -PrivateMakefile dump.rdb -resources/public/cljs -pom.xml -*jar -/lib/ -/classes/ -.lein-* -.crossover-cljs -target/ -.nrepl-port -.idea/ -NoteHub.iml +bin/ +node_modules/ +npm-debug.log +database.sqlite +database.sqlite-journal diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c71297c --- /dev/null +++ b/.vscode/launch.json @@ -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 + } + ] +} diff --git a/API.md b/API.md deleted file mode 100644 index fad729a..0000000 --- a/API.md +++ /dev/null @@ -1,147 +0,0 @@ -# NoteHub API - -**Version 1.4, status: released.** - -## Changelog - -- **V1.4**: Bugfix: no whitespace elimination from the note text is needed now for the signature computation. -- **V1.3**: New note ID format. -- **V1.2**: Theme & font settings can be specified during the publishing. -- **V1.1**: Fields `publisher` and `title` in the response to the note retrieval. -- **V1.0**: Initial release. - -## Prerequisites - -The NoteHub API can only be used in combination with a __Publisher ID__ (PID) and __Publisher Secret Key__ (PSK), which can be requested [here](#registration). The PSK can be revoked at any moment in case of an API abuse. - -A PID is a string chosen by the publisher and cannot be longer than 16 characters (e.g.: __notepadPlugin__). A PSK will be generated by the NoteHub API and can be a string of any length and content. - -All API requests must be issued with one special parameter `version` denoting the expected version of the API as a string, e.g. `1.0` (see examples below). You should always put the version of this document as a `version` parameter. - -Once you obtained your PSK, you can test the API [here](/api-test.html). - -## NoteHub API Access Request -To register as a publisher and gain access to NoteHub API, please send an email with the following information about you: the desired PID, your contact information, a short description of what you want to do and an URL of the resource where the API will be used or its website. - -## Note Retrieval - -A simple `GET` request to the following URL: - - https://www.notehub.org/api/note - -with the following parameters: - -Parameter | Explanation | Type ---- | --- | --- -`noteID` | Note-ID | **required** -`version` | Used API version | **required** - -will return a JSON object containing following self explaining fields: `note`, `title`, `longURL`, `shortURL`, `statistics`, `status`, `publisher`. - -Example: - - { - note: "markdown source", - title: "Lorem Ipsum.", - longURL: "https://www.notehub.org/2014/1/3/lorem-ipsum", - shortURL: "https://www.notehub.org/0vrcp", - statistics: { - published: "1396250865735", - edited: "1412516289863", - views: 24 - }, - status: { - success: true, - comment: "some server message" - }, - publisher: "Publisher Description" - } - -Hence, the status of the request can be evaluated by reading of the property `status.success`. The field `status.comment`might contain an error message, a warning or any other comments from the server. - -The note ID is a string, containing the date of publishing and a few first words of the note (usually the title), e.g.: `"2014/1/3/lorem-ipsum"`. This ID will be generated by NoteHub automatically. - -## Note Publishing - -A note must be created by a `POST` request to the following URL: - - https://www.notehub.org/api/note - -with the following parameters: - -Parameter | Explanation | Type ---- | --- | --- -`note` | Text to publish | **required** -`pid` | Publisher ID | **required** -`signature` | Signature | **required** -`password` | Secret token (plain or hashed) | *optional* -`version` | Used API version | **required** -`theme` | Theme name | *optional* -`text-size` | Text size | *optional* -`header-size`| Header size | *optional* -`text-font` | Text font name | *optional* -`header-font`| Header font name | *optional* - -The Signature is the MD5 hash of the following string concatenation: - - pid + psk + note - -The signature serves as a proof, that the request is authentic and will be issued by the publisher corresponding to the provided PID. Please note, that _all_ of the values used in the signature computation, should be identical to the values passed with the request itself. -Ensure, that your note contains only `\n` symbols as line breaks! - -The parameters specifying the theme name and fonts are optional and only impact the URLs returned back. - -The response of the server will contain the fields `noteID`, `longURL`, `shortURL`, `status`. - -Example: - - { - noteID: "2014/1/3/lorem-ipsum", - longURL: "https://www.notehub.org/2014/1/3/lorem-ipsum", - shortURL: "https://www.notehub.org/0vrcp", - status: { - success: true, - comment: "some server message" - } - } - -The status object serves the same purpose as in the case of note retrieval. - -## Note Update - -To update a note, an `PUT` request must be issued to the following URL: - - https://www.notehub.org/api/note - -with the following parameters: - -Parameter | Explanation | Type ---- | --- | --- -`noteID` | Note-ID | **required** -`note` | New text | **required** -`pid` | Publisher ID | **required** -`signature` | Signature | **required** -`password` | Secret token (plain or hashed) | **required** -`version` | Used API version | **required** - -The Signature is the MD5 hash of the following string concatenation: - - pid + psk + noteID + note + password - -Please note, that all of the values used in the signature computation, should be identical to the values passed with the request itself. -Ensure, that your note contains only `\n` symbols as line breaks! - -The response of the server will contain the fields `longURL`, `shortURL`, `status`. - -Example: - - { - longURL: "https://www.notehub.org/2014/1/3/lorem-ipsum", - shortURL: "https://www.notehub.org/0vrcp", - status: { - success: true, - comment: "some server message" - } - } - -The status object serves the same purpose as in the case of note retrieval and publishing. diff --git a/LANDING.md b/LANDING.md deleted file mode 100644 index 7a148a5..0000000 --- a/LANDING.md +++ /dev/null @@ -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). diff --git a/Procfile b/Procfile deleted file mode 100644 index 0fef382..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: lein with-profile production ring server-headless $PORT diff --git a/README.md b/README.md index 1f80f77..d3a9c06 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,8 @@ [NoteHub](https://www.notehub.org) is a free and hassle-free pastebin for one-off markdown publishing. -NoteHub _was_ an one-app-one-language [experiment](https://www.notehub.org/2012/6/16/how-notehub-is-built) and was initially implemented entirely in [Clojure](http://clojure.org) (ClojureScript). -NoteHub's persistence layer is based on the key-value store [redis](http://redis.io). -Currently, NoteHub is hosted for free on [Heroku](http://heroku.com). - -NoteHub supports an [API](https://github.com/chmllr/NoteHub/blob/master/API.md) and can be integrated as a publishing platform. - ## How to Use? -First, create [a new page](https://www.notehub.org/new) using the [Markdown syntax](http://daringfireball.net/projects/markdown/). +First, create [a new page](https://notehub.org/new) using the [Markdown syntax](http://daringfireball.net/projects/markdown/). When the note is published, you'll see a subtle panel at the bottom of the screen. From this panel you can go to a rudimentary statistics of the article, or you can export the original markdown, or copy the short url of the note. Besides this, you also can invert the color scheme by appending to the note url: diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..ca5b5e3 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs" + }, + "exclude": [ + "node_modules", + "bin" + ] +} diff --git a/messages b/messages deleted file mode 100644 index 30b460b..0000000 --- a/messages +++ /dev/null @@ -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)
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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..4e582e4 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "NoteHub", + "version": "3.0.0", + "description": "Free Pastebin for One-Off Markdown Publishing", + "main": "server.js", + "scripts": { + "server": "node server.js", + "devser": "nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/chmllr/NoteHub.git" + }, + "keywords": [ + "markdown", + "pastebin" + ], + "author": "Christian Müller (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" + } +} diff --git a/project.clj b/project.clj deleted file mode 100644 index 42b4412..0000000 --- a/project.clj +++ /dev/null @@ -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"]) diff --git a/resources/edit.html b/resources/edit.html new file mode 100644 index 0000000..37a049d --- /dev/null +++ b/resources/edit.html @@ -0,0 +1,35 @@ + + + + + NoteHub — New Page + + + + + + + + + +
+ +
+
+ + + + + + + +
+
+ + + diff --git a/resources/footer.html b/resources/footer.html new file mode 100644 index 0000000..b98b195 --- /dev/null +++ b/resources/footer.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/resources/public/api-test.html b/resources/public/api-test.html deleted file mode 100644 index d87222d..0000000 --- a/resources/public/api-test.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - -NoteHub API Testing - - - - -
- - - - - - - -
- -
-
- - -
- Submit -
-
- - -

Request

-
-    
-

Response

-
-    
-
- - diff --git a/resources/public/index.html b/resources/public/index.html new file mode 100644 index 0000000..cb4ab8e --- /dev/null +++ b/resources/public/index.html @@ -0,0 +1,37 @@ + + + + NoteHub — Free Pastebin for One-Off Markdown Publishing + + + + + +
+

NoteHub

+

Free and Hassle-free Pastebin for Markdown Notes.

+
+ See Demo Note + New Note +
+
+
+

Changelog

+ +
+ + + diff --git a/resources/public/js/publishing.js b/resources/public/js/publishing.js index a65aab5..64953e8 100644 --- a/resources/public/js/publishing.js +++ b/resources/public/js/publishing.js @@ -1,66 +1,46 @@ -var $ = function(id){ return document.getElementById(id); } +var $ = function(id) { + return document.getElementById(id); +} var iosDetected = navigator.userAgent.match("(iPad|iPod|iPhone)"); var timer = null; var timerDelay = iosDetected ? 800 : 400; -var show = function(elem) { elem.style.display = "block" } +var show = function(elem) { + elem.style.display = "block" +} var $note, $action, $preview, $plain_password, $input_elems, $dashed_line, $proposed_title, updatePreview; -var firstLines_; var backendTimer; -function updateProposedTitle() { - clearTimeout(backendTimer); - backendTimer = setTimeout(function () { - var http = new XMLHttpRequest(); - var url = "/propose-title"; - http.open("POST", url, true); - http.onreadystatechange = function() { - if(http.readyState == 4 && http.status == 200) { - var now = new Date(); - $proposed_title.innerHTML = - "Expected URL: https://www.notehub.org/" + - now.getFullYear() + "/" + (now.getMonth()+1) + "/" + now.getDate() + "/" + - http.responseText; - } - } - http.send($note.value); - }, 500); -} - -function md2html(input){ +function md2html(input) { return marked(input); } -function onLoad () { +function onLoad() { $note = $("note"); - $action = $("action"); + $action = $("action").value; $preview = $("preview"); $plain_password = $("plain-password"); $proposed_title = $("proposed-title"); $input_elems = $("input-elems"); $dashed_line = $("dashed-line"); - updatePreview = function(){ + updatePreview = function() { clearTimeout(timer); var content = $note.value; var delay = Math.min(timerDelay, timerDelay * (content.length / 400)); - timer = setTimeout(function(){ + timer = setTimeout(function() { show($dashed_line); show($input_elems); $preview.innerHTML = md2html(content); - var firstLines = content.split("\n", 2); - if(firstLines_ != firstLines) { - firstLines_ = firstLines; - updateProposedTitle(); - } }, delay); }; - if($action){ - if($action.value == "update") updatePreview(); else $note.value = ""; - $note.onkeyup = updatePreview; - $("publish-button").onclick = function(e) { - if($plain_password.value != "") $("password").value = md5($plain_password.value); - $plain_password.value = null; - $("signature").value = md5($("session").value + $note.value); - } - if(iosDetected) $note.className += " ui-border"; else $note.focus(); + if ($action == "UPDATE") updatePreview(); + else $note.value = ""; + $note.onkeyup = updatePreview; + $("publish-button").onclick = function(e) { + if ($plain_password.value != "") $("password").value = md5($plain_password.value); + $plain_password.value = null; + $("signature").value = md5($("session").value + + $note.value.replace(/[\n\r]/g, "")); } + if (iosDetected) $note.className += " ui-border"; + else $note.focus(); } diff --git a/resources/public/style.css b/resources/public/style.css new file mode 100644 index 0000000..a1659e8 --- /dev/null +++ b/resources/public/style.css @@ -0,0 +1,284 @@ +.ui-border { + border-radius: 3px; + border: 1px solid #333; +} + +a { + border-bottom: 1px dotted; + text-decoration: none; + color: #097; +} + +a:hover { + color: #0a8; +} + +a:visited { + color: #054; +} + +#draft { + margin-bottom: 3em; +} + +.button { + cursor: pointer; +} + +.ui-elem { + background: #fff; + font-size: 1em; + opacity: 0.8; + padding: 0.3em; + border-radius: 3px; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif'; + font-weight: 300; + border: 1px solid #333; +} + +.landing-button, textarea, fieldset { + border: none; + margin: 1em; +} + +.landing-button { + padding: 10px; + border-radius: 3px; + background: #0a6; + font-size: 1.5em; + text-decoration: none; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif'; + font-weight: 300; +} + +.landing-button.demo { + opacity: 0.9; + background: #ddd; + color: black !important; + margin-right: 1.5em; +} + +.landing-button:hover { + background: #0b7; +} + +.landing-button.demo:hover { + background: #eee; +} + +.helvetica { + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif'; + font-weight: 300; +} + +footer { + text-align: center; + padding-bottom: 1em; + font-size: 0.8em; + width: 100%; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif'; + font-weight: 300; +} + +@media screen and (max-width: 767px) { + footer { + font-size: 0.4em; + } +} + +footer a { + border: none; +} + +html, +body { + padding: 0; + margin: 0; + color: #333; + background: #fff; +} + +#hero { + text-align: center; + padding-bottom: 5em; + padding-top: 5em; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "PT Serif", Georgia, serif; + font-weight: bolder; +} + +h1 { + font-size: 1.8em; +} + +h2 { + font-size: 1.6em; +} + +h3 { + font-size: 1.4em; +} + +h4 { + font-size: 1.2em; +} + +h5 { + font-size: 1.1em; +} + +h6 { + font-size: 1.0em; +} + +#hero h1 { + font-size: 2.5em; +} + +#hero h2 { + margin: 2em; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', sans-serif; + font-weight: 300; +} + +article { + -webkit-flex: 1; + flex: 1; + text-align: justify; + margin-top: 3em; + font-family: sans-serif; + margin-right: auto; + margin-left: auto; +} + +@media screen and (min-width: 1024px) { + article { + width: 800px; + } +} + +@media screen and (max-width: 1023px) { + article { + width: 90%; + } +} + +.central-element { + margin-right: auto; + margin-left: auto; +} + +@media screen and (min-width: 1024px) { + .central-element { + width: 800px; + } +} + +@media screen and (max-width: 1023px) { + .central-element { + width: 90%; + } +} + +article img { + max-width: 100%; +} + +article p { + line-height: 140%; + font-size: 1.05em; +} + +blockquote { + font-family: "PT Serif", Georgia, serif; + padding-left: 1em; + border-left: 4px solid #097; +} + +article > h1:first-child { + margin: 2em; + font-size: 2.0em; + text-align: center; +} + +.centered { + text-align: center; +} + +.bottom-space { + margin-bottom: 7em; +} + +code, +pre { + font-size: 1.05em; + background: #efefef; + font-family: monospace; +} + +pre { + border: 1px solid #aaa; + padding: 0.5em; + border-radius: 3px; +} + +*:focus { + outline: 0px none transparent; +} + +@media screen and (min-width: 1024px) { + textarea { + width: 800px; + } +} + +textarea { + height: 500px; + font-size: 1em; + font-family: Courier; + border-radius: 5px; +} + +.hidden { + display: none; +} + +#dashed-line { + margin-bottom: 3em; + margin-top: 3em; + border-bottom: 1px dashed#888; +} + +table { + border-collapse: collapse; + width: 100%; +} + +th { + background-color: #efefef; + line-height: 2.5em; + padding: 0.3em; +} + +td { + line-height: 2.5em; + padding: 0.3em; + border-top: 1px solid #aaa; +} + +.middot { + padding: 0.5em; +} + +body { + display: -webkit-flex; +} + +body { + -webkit-flex-direction: column; + flex-direction: column; + display: flex; + min-height: 100vh; +} diff --git a/resources/template.html b/resources/template.html new file mode 100644 index 0000000..fad3326 --- /dev/null +++ b/resources/template.html @@ -0,0 +1,15 @@ + + + + NoteHub — %TITLE% + + + + + +
+ %CONTENT% +
+ %FOOTER% + + diff --git a/scripts/expose.sh b/scripts/expose.sh deleted file mode 100755 index f70f4fc..0000000 --- a/scripts/expose.sh +++ /dev/null @@ -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 diff --git a/scripts/run.sh b/scripts/run.sh deleted file mode 100755 index e62edac..0000000 --- a/scripts/run.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -export DEVMODE=1 - -if ! pgrep "redis-server" > /dev/null; then - redis-server & -fi - -lein ring server-headless 8080 diff --git a/server.js b/server.js new file mode 100644 index 0000000..80ac174 --- /dev/null +++ b/server.js @@ -0,0 +1,112 @@ +var express = require('express'); +var view = require('./src/view'); +var storage = require('./src/storage'); +var md5 = require('md5'); +var LRU = require("lru-cache") +var bodyParser = require('body-parser'); + +var app = express(); + +app.use(bodyParser.urlencoded({ extended: true })); + +var MODELS = {}; +var CACHE = new LRU({ + max: 50, + dispose: key => { + MODELS[key].save(); + delete MODELS[key]; + } +}); + +var getTimeStamp = () => { + var timestamp = new Date().getTime(); + timestamp = Math.floor(timestamp / 10000000); + return (timestamp).toString(16) +} + +app.use(express.static(__dirname + '/resources/public')); + +app.get('/new', function (req, res) { + res.send(view.newNotePage(getTimeStamp() + md5(Math.random()))); +}); + +app.post('/note', function (req, res) { + var body = req.body, + session = body.session, + note = body.note, + password = md5(body.password); + var goToNote = note => res.redirect("/" + note.id); + if (session.indexOf(getTimeStamp()) != 0) + return sendResponse(res, 400, "Session expired"); + var expectedSignature = md5(session + note.replace(/[\n\r]/g, "")); + if (expectedSignature != body.signature) + return sendResponse(res, 400, "Signature mismatch"); + console.log(body) + if (body.action == "POST") + storage.addNote(note, password).then(goToNote); + else + storage.updateNote(body.id, password, note).then(note => { + CACHE.del(note.id); + goToNote(note); + }, + error => sendResponse(res, 403, error.message)) +}); + +app.get("/:year/:month/:day/:title", function (req, res) { + var P = req.params; + storage.getNoteId(P.year + "/" + P.month + "/" + P.day + "/" + P.title) + .then(id => res.redirect("/" + id)); +}); + +app.get(/\/([a-z0-9]+\/edit)/, function (req, res) { + var link = req.params["0"].replace("/edit", ""); + storage.getNote(link).then(note => res.send(note + ? view.editNotePage(getTimeStamp() + md5(Math.random()), note) + : notFound(res))); +}); + +app.get(/\/([a-z0-9]+\/export)/, function (req, res) { + var link = req.params["0"].replace("/export", ""); + res.set({ 'Content-Type': 'text/plain', 'Charset': 'utf-8' }); + storage.getNote(link).then(note => note + ? res.send(note.text) + : notFound(res)); +}); + +app.get(/\/([a-z0-9]+\/stats)/, function (req, res) { + var link = req.params["0"].replace("/stats", ""); + var promise = link in MODELS + ? new Promise(resolve => resolve(MODELS[link])) + : storage.getNote(link); + promise.then(note => note + ? res.send(view.renderStats(note)) + : notFound(res)); +}); + +app.get(/\/([a-z0-9]+)/, function (req, res) { + var link = req.params["0"]; + if (CACHE.has(link)) { + (link in MODELS) && MODELS[link].views++; + res.send(CACHE.get(link)); + } else storage.getNote(link).then(note => { + if (!note) return notFound(res); + var content = view.renderNote(note); + CACHE.set(link, content); + MODELS[link] = note; + res.send(content); + }); +}); + +var sendResponse = (res, code, message) => + res.status(code).send(view.renderPage(message, "

" + message + "

", "")); + +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); \ No newline at end of file diff --git a/src/migrate.js b/src/migrate.js new file mode 100644 index 0000000..e480ac3 --- /dev/null +++ b/src/migrate.js @@ -0,0 +1,68 @@ +var redis = require("redis"), + client = redis.createClient(); + +var Sequelize = require('sequelize'); +var sequelize = new Sequelize('database', null, null, { + dialect: 'sqlite', + pool: { + max: 5, + min: 0, + idle: 10000 + }, + storage: 'database.sqlite' +}); + +var Note = sequelize.define('Note', { + id: { type: Sequelize.STRING(6), unique: true, primaryKey: true }, + deprecatedId: Sequelize.TEXT, + text: Sequelize.TEXT, + published: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }, + edited: { type: Sequelize.DATE, allowNull: true, defaultValue: null }, + password: Sequelize.STRING(16), + views: Sequelize.INTEGER, +}); + +sequelize.sync().then(function() { + client.hgetall("note", function(err, notes) { + console.log("notes retrieved:", Object.keys(notes).length); + client.hgetall("published", function(err, published) { + console.log("published retrieved:", Object.keys(published).length); + client.hgetall("password", function(err, password) { + console.log("password retrieved:", Object.keys(password).length); + client.hgetall("views", function(err, views) { + console.log("views retrieved:", Object.keys(views).length); + client.hgetall("edited", function(err, edited) { + console.log("edited retrieved:", Object.keys(edited).length); + Object.keys(notes).forEach(function(id) { + client.smembers(id + ":urls", function(err, links) { + + var createLink = LinkId => { + Note.create({ + id: LinkId, + deprecatedId: id, + text: notes[id], + published: published[id] && new Date(published[id] * 1000) || new Date(), + password: password[id] && password[id].length == 32 && password[id], + edited: !isNaN(edited[id]) && edited[id] && new Date(edited[id] * 1000) || null, + views: views[id], + }) + }; + + if (links.length == 0) { + var tmp = id.split("/"); + var paramString = '{:day "' + tmp[2] + + '", :month "' + tmp[1] + '", :title "' + tmp[3] + '", :year "' + tmp[0] + '"}'; + client.hget("short-url", paramString, function(err, result) { + if (!result) throw ("oops:" + paramString + ":" + id); + createLink(result); + }); + } else createLink(links[links.length - 1]); + + }) + }); + }); + }); + }); + }); + }); +}) diff --git a/src/notehub/api.clj b/src/notehub/api.clj deleted file mode 100644 index 2a920f5..0000000 --- a/src/notehub/api.clj +++ /dev/null @@ -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))}))) diff --git a/src/notehub/css.clj b/src/notehub/css.clj deleted file mode 100644 index 4597010..0000000 --- a/src/notehub/css.clj +++ /dev/null @@ -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" - }] - ))) diff --git a/src/notehub/handler.clj b/src/notehub/handler.clj deleted file mode 100644 index 0f30409..0000000 --- a/src/notehub/handler.clj +++ /dev/null @@ -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)})) - diff --git a/src/notehub/storage.clj b/src/notehub/storage.clj deleted file mode 100644 index 8184cc1..0000000 --- a/src/notehub/storage.clj +++ /dev/null @@ -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")))) diff --git a/src/notehub/views.clj b/src/notehub/views.clj deleted file mode 100644 index 391e75a..0000000 --- a/src/notehub/views.clj +++ /dev/null @@ -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