├── README.md ├── backend ├── index.js ├── lock.js └── sql │ └── 001-initial.sql ├── bin.js ├── frontend ├── css │ └── styles.css ├── html │ ├── footer.html │ ├── header.html │ ├── page-home.html │ ├── page-notfound.html │ ├── page-thread.html │ ├── page-user.html │ └── page-users.html └── js │ └── app.js ├── package-lock.json ├── package.json ├── screenshot.png └── test-run.js /README.md: -------------------------------------------------------------------------------- 1 | # { http + dat } Web forum 2 | 3 | A Web forum built on Dat and HTTP and run in node.js. This application demonstrates a "dat + http hybrid architecture." Involves two roles: 4 | 5 | - **Users**. Must use a browser that supports [Dat](https://datproject.org/). Each user has a "dat archive." They write their posts to `/posts/${site-hostname}/*.json`, and the data is synced to the server. 6 | - **Server**. An HTTP server. Reads post-files from user dats and stores them in a SQLite DB. Visitors can browse the site to see the content. To participate, they create a Dat archive and submit it to the server. 7 | 8 | ## Why is this cool? 9 | 10 | This forum is federated. Users are identified with global URLs that can easily be reused at other forum instances. User data is signed and saved on the user's computer, so they have full control over it. 11 | 12 | --- 13 | 14 | This is part of an ongoing project series to demonstrate 3 different architectures: 15 | 16 | - [http + dat](https://github.com/pfrazee/node-http-dat-forum). Users visit an http server to see the app & get latest data. 17 | - dat (nodejs) (**todo**). Users visit a dat archive to see the app & get latest data. 18 | - dat (SPA) (**todo**). Users visit a dat archive to see the app & get latest data. Run 100% in the browser. 19 | 20 | --- 21 | 22 | ## Screenshot 23 | 24 | This screenshot is of an instance with 4 users. The 5 visible threads were read from the posts folders of those 4 users' dats. 25 | 26 | ![screenshot.png](screenshot.png) -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const express = require('express') 3 | const a = require('co-express') 4 | const ejs = require('ejs') 5 | const Dat = require('@beaker/dat-node') 6 | const sqlite = require('sqlite') 7 | const mkdirp = require('mkdirp') 8 | const {join} = require('path') 9 | const lock = require('./lock') 10 | const app = express() 11 | 12 | const DEBUG_FORUM_NAME = 'Paul\'s Dat Forum' 13 | 14 | exports.setup = async function setup (hostname, dataPath) { 15 | // setup the data folder 16 | mkdirp.sync(dataPath) 17 | const dat = Dat.createNode({path: dataPath}) // initiate dat 18 | const db = await sqlite.open(join(dataPath, 'db.sqlite'), {Promise}) 19 | await db.migrate({migrationsPath: join(__dirname, 'sql')}) 20 | 21 | var server = { 22 | hostname, 23 | dat, 24 | db, 25 | users: null, 26 | addUser (userData) { 27 | return addUser(server, userData) 28 | } 29 | } 30 | 31 | // swarm all users 32 | var users = server.users = await db.all('SELECT * FROM User') 33 | console.log('Swarming', users.length, 'users') 34 | for (let user of users) { 35 | await indexUser(db, dat, user) 36 | } 37 | 38 | // configure app 39 | app.use(express.json()) 40 | app.engine('html', ejs.renderFile) 41 | app.engine('ejs', ejs.renderFile) 42 | app.set('view engine', 'html') 43 | app.set('views', join(__dirname, '..', 'frontend/html')) 44 | app.locals = { 45 | users, 46 | siteInfo: { 47 | name: DEBUG_FORUM_NAME, 48 | hostname: hostname, 49 | port: 3000 50 | } 51 | } 52 | app.use('/css', express.static(join(__dirname, '..', 'frontend', 'css'))) 53 | app.use('/js', express.static(join(__dirname, '..', 'frontend', 'js'))) 54 | 55 | // GET / 56 | app.get('/', a(async (req, res) => { 57 | var posts = await db.all(` 58 | SELECT Post.*, User.name as authorName 59 | FROM Post 60 | INNER JOIN User ON Post.authorUrl = User.url 61 | WHERE threadRootUrl IS NULL 62 | ORDER BY firstIndexedAt DESC 63 | LIMIT 20 64 | `) 65 | res.format({ 66 | 'application/json': () => res.send({posts}), 67 | 'text/html': () => res.render('page-home', {posts}) 68 | }) 69 | })) 70 | 71 | // GET /thread/:id 72 | app.get('/thread/:threadId', a(async (req, res) => { 73 | var rootPost = await db.get('SELECT Post.*, User.name as authorName FROM Post INNER JOIN User ON Post.authorUrl = User.url WHERE id=?', [req.params.threadId]) 74 | if (!rootPost) { 75 | return res.status(404).render('page-notfound') 76 | } 77 | var replies = await db.all('SELECT Post.*, User.name as authorName FROM Post INNER JOIN User ON Post.authorUrl = User.url WHERE threadRootUrl=?', rootPost.url) 78 | 79 | res.format({ 80 | 'application/json': () => res.send({rootPost, replies}), 81 | 'text/html': () => res.render('page-thread', {rootPost, replies}) 82 | }) 83 | })) 84 | 85 | // GET /users 86 | app.get('/users', (req, res) => { 87 | res.format({ 88 | 'application/json': () => res.send({users}), 89 | 'text/html': () => res.render('page-users', {users}) 90 | }) 91 | }) 92 | 93 | // GET /user 94 | // ?name=... 95 | // ?url=... 96 | app.get('/user', (req, res) => { 97 | var user 98 | if (req.query.name) { 99 | let name = req.query.name.toLowerCase() 100 | user = users.find(user => user.name === name) 101 | } else if (req.query.url) { 102 | let url = req.query.url.toLowerCase() 103 | user = users.find(user => user.url === url) 104 | } 105 | if (!user) { 106 | return res.status(404).render('page-notfound') 107 | } 108 | res.format({ 109 | 'application/json': () => res.send({user}), 110 | 'text/html': () => res.render('page-user', {user}) 111 | }) 112 | }) 113 | 114 | // POST /users 115 | app.post('/users', a(async (req, res) => { 116 | try { 117 | await addUser(server, req.body) 118 | res.send({success: true}) 119 | } catch (e) { 120 | res.status(400).send({error: e.toString()}) 121 | } 122 | })) 123 | 124 | // listen 125 | app.listen(3000, () => console.log(DEBUG_FORUM_NAME, 'listening on port 3000')) 126 | 127 | return server 128 | } 129 | 130 | async function addUser (server, userData) { 131 | assert(userData.name && typeof userData.name === 'string', 'name is required') 132 | assert(userData.url && typeof userData.url === 'string', 'url is required') 133 | 134 | var {db, users} = server 135 | var release = await lock('users') // we need a transaction to correctly reserve the username 136 | try { 137 | var {name, url} = userData 138 | 139 | // check if the name is available 140 | var existingUser = await db.get(`SELECT * FROM User WHERE name=?`, [name]) 141 | if (existingUser) throw new Error('Username is already in use') 142 | 143 | // add to db 144 | var values = {url, name, isAdmin: 0, createdAt: Date.now()} 145 | db.run(`INSERT INTO User (${NAMES(values)}) VALUES (${QS(values)})`, Object.values(values)) 146 | users.push(values) 147 | 148 | // index 149 | await indexUser(server, userData) 150 | } finally { 151 | release() 152 | } 153 | } 154 | 155 | async function indexUser (server, user) { 156 | var {dat, hostname} = server 157 | user.archive = await dat.getArchive(user.url) 158 | // index current posts 159 | var files = await user.archive.readdir('/posts/' + hostname) 160 | for (let file of files) { 161 | await indexPost(server, user, '/posts/' + hostname + '/' + file) 162 | } 163 | // watch for subsequent updates 164 | user.archive.watch('/posts/' + hostname + '/*.json', e => indexPost(server, user, e.path)) 165 | } 166 | 167 | async function indexPost (server, user, path) { 168 | var {db} = server 169 | var url = user.url + path 170 | var release = await lock(user.url) // index one post on a user at a time 171 | try { 172 | // read post 173 | var indexedAt = Date.now() 174 | var post = JSON.parse(await user.archive.readFile(path, 'utf8')) 175 | 176 | // validate 177 | assert(post && typeof post === 'object', 'Post must be an object') 178 | assert(!post.title || typeof post.title === 'string', 'Post .title must be a string') 179 | assert(typeof post.body === 'string' && post.body, 'Post .body is required and must be a string') 180 | assert(!post.threadRootUrl || typeof post.threadRootUrl === 'string', 'Post .threadRootUrl must be a string') 181 | assert(!post.threadParentUrl || typeof post.threadParentUrl === 'string', 'Post .threadParentUrl must be a string') 182 | 183 | // store 184 | let values = { 185 | url, 186 | authorUrl: user.url, 187 | threadRootUrl: post.threadRootUrl, 188 | threadParentUrl: post.threadParentUrl, 189 | 190 | title: post.title, 191 | body: post.body, 192 | 193 | firstIndexedAt: indexedAt, 194 | lastIndexedAt: indexedAt 195 | } 196 | var postRecord = await db.get('SELECT * FROM Post WHERE url=?', [url]) 197 | if (postRecord) { 198 | values.firstIndexedAt = postRecord.firstIndexedAt 199 | await db.run(`UPDATE Post ${SET(values)} WHERE url=?`, [...Object.values(values), url]) 200 | } else { 201 | await db.run(`INSERT OR IGNORE INTO Post (${NAMES(values)}) VALUES (${QS(values)})`, Object.values(values)) 202 | } 203 | } catch (e) { 204 | console.error('Failed to index post by', user.name) 205 | console.error('Post URL:', url) 206 | console.error(e) 207 | } finally { 208 | release() 209 | } 210 | } 211 | 212 | function SET (obj) { 213 | var valuesClause = Object.keys(obj).map(key => `${key}=?`) 214 | return `SET ${valuesClause.join(', ')}` 215 | } 216 | 217 | function NAMES (obj) { 218 | return Object.keys(obj).join(', ') 219 | } 220 | 221 | function QS (obj) { 222 | return Object.keys(obj).map(_ => '?').join(', ') 223 | } 224 | -------------------------------------------------------------------------------- /backend/lock.js: -------------------------------------------------------------------------------- 1 | var AwaitLock = require('await-lock') 2 | 3 | // wraps await-lock in a simpler interface, with many possible locks 4 | // usage: 5 | /* 6 | var lock = require('./lock') 7 | async function foo () { 8 | var release = await lock('bar') 9 | // ... 10 | release() 11 | } 12 | */ 13 | 14 | var locks = {} 15 | module.exports = async function (key) { 16 | if (!(key in locks)) locks[key] = new AwaitLock() 17 | 18 | var lock = locks[key] 19 | await lock.acquireAsync() 20 | return lock.release.bind(lock) 21 | } 22 | -------------------------------------------------------------------------------- /backend/sql/001-initial.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- Up 3 | -------------------------------------------------------------------------------- 4 | 5 | CREATE TABLE User ( 6 | url TEXT PRIMARY KEY, 7 | 8 | name TEXT NOT NULL, 9 | isAdmin NUMERIC, 10 | 11 | createdAt INTEGER 12 | 13 | CONSTRAINT User_ck_isAdmin CHECK (isAdmin IN (0, 1)) 14 | ); 15 | CREATE INDEX User_ix_name ON User (name); 16 | 17 | CREATE TABLE Post ( 18 | id INTEGER PRIMARY KEY, 19 | url TEXT UNIQUE, 20 | authorUrl TEXT NOT NULL, 21 | threadRootUrl TEXT, 22 | threadParentUrl TEXT, 23 | 24 | title TEXT NOT NULL, 25 | body TEXT NOT NULL, 26 | 27 | firstIndexedAt INTEGER, 28 | lastIndexedAt INTEGER, 29 | 30 | FOREIGN KEY (threadRootUrl) REFERENCES Post (url), 31 | FOREIGN KEY (threadParentUrl) REFERENCES Post (url) 32 | ); 33 | CREATE INDEX Post_ix_url ON Post (url); 34 | CREATE INDEX Post_ix_threadRootUrl ON Post (threadRootUrl); 35 | CREATE INDEX Post_ix_firstIndexedAt ON Post (firstIndexedAt); 36 | 37 | 38 | -------------------------------------------------------------------------------- 39 | -- Down 40 | -------------------------------------------------------------------------------- 41 | 42 | DROP INDEX Post_ix_firstIndexedAt; 43 | DROP TABLE Post; 44 | DROP INDEX User_ix_name; 45 | DROP TABLE User; -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | require('./backend').setup('localhost', path(__dirname, 'data')) -------------------------------------------------------------------------------- /frontend/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | position: relative; 3 | max-width: 840px; 4 | margin: 0 auto; 5 | } 6 | 7 | .header-tools { 8 | position: absolute; 9 | right: 0; 10 | text-align: right; 11 | } 12 | 13 | .create-user { 14 | border: 1px solid #ccc; 15 | padding: 20px 14px; 16 | box-shadow: 0 2px 2px rgba(0,0,0,.15); 17 | margin-top: 5px; 18 | } 19 | 20 | .composer-container { 21 | position: fixed; 22 | bottom: 0; 23 | right: 20px; 24 | } 25 | 26 | .composer { 27 | border: 1px solid #ccc; 28 | padding: 20px 14px; 29 | width: 500px; 30 | height: 480px; 31 | background: #fff; 32 | } 33 | 34 | .composer #title-input { 35 | display: block; 36 | width: calc(100% - 18px); 37 | padding: 5px; 38 | margin: 0 0 10px; 39 | font-size: 14px; 40 | } 41 | 42 | .composer #body-input { 43 | width: calc(100% - 18px); 44 | height: 400px; 45 | border-color: #ccc; 46 | padding: 6px; 47 | font-size: 13px; 48 | } 49 | 50 | .composer .actions { 51 | display: flex; 52 | justify-content: space-between; 53 | } 54 | 55 | .threads .thread { 56 | padding: 5px 10px; 57 | margin: 1em 0; 58 | } 59 | 60 | .threads .thread .thread-title { 61 | margin: 0; 62 | font-size: 24px; 63 | } 64 | 65 | .thread { 66 | margin: 1em 0; 67 | } 68 | 69 | .thread-title { 70 | margin: 0; 71 | } 72 | 73 | .post { 74 | margin-bottom: 1em; 75 | border-bottom: 1px solid #ccc; 76 | } 77 | 78 | .post-title { 79 | padding: 5px 10px; 80 | } 81 | 82 | .post-meta { 83 | padding: 0px 10px 5px; 84 | } 85 | 86 | .post-body { 87 | padding: 1.5em; 88 | white-space: pre; 89 | } 90 | 91 | .reply-composer { 92 | display: none; 93 | } 94 | .reply-composer.visible { 95 | display: block; 96 | } 97 | 98 | .reply-composer #title-input { 99 | display: block; 100 | width: 500px; 101 | padding: 5px; 102 | margin: 0 0 10px; 103 | font-size: 14px; 104 | } 105 | 106 | .reply-composer #body-input { 107 | width: 500px; 108 | height: 200px; 109 | border-color: #ccc; 110 | padding: 6px; 111 | font-size: 13px; 112 | } 113 | 114 | .beaker-notice { 115 | width: 300px; 116 | font-size: 14px; 117 | color: gray; 118 | } -------------------------------------------------------------------------------- /frontend/html/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/html/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pauls Dat Forum 5 | 6 | 7 | 8 |
9 |
-------------------------------------------------------------------------------- /frontend/html/page-home.html: -------------------------------------------------------------------------------- 1 | <%- include('header.html') -%> 2 | 3 |

<%= siteInfo.name %>

4 |

Threads

5 | 6 |
7 | <% for (let post of posts) { %> 8 |
9 |

<%= post.title %>

10 |
by <%= post.authorName %> - <%= (new Date(post.firstIndexedAt)).toLocaleString() %>
11 |
12 | <% } %> 13 |
14 | 15 |

Users

16 | 17 | 22 | 23 | <%- include('footer.html') -%> -------------------------------------------------------------------------------- /frontend/html/page-notfound.html: -------------------------------------------------------------------------------- 1 | <%- include('header.html') -%> 2 | 3 |

404 not found

4 | 5 | <%- include('footer.html') -%> -------------------------------------------------------------------------------- /frontend/html/page-thread.html: -------------------------------------------------------------------------------- 1 | <%- include('header.html') -%> 2 | 3 |

<%= siteInfo.name %>

4 | 5 | « back 6 | 7 |
8 |
9 |

<%= rootPost.title %>

10 | 11 |
<%= rootPost.body %>
12 |
13 | 14 |
15 | <% for (let reply of replies) { %> 16 |
17 |
<%= reply.title %>
18 | 19 |
<%= reply.body %>
20 |
21 | <% } %> 22 |
23 | 24 |
25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 | 33 | <%- include('footer.html') -%> -------------------------------------------------------------------------------- /frontend/html/page-user.html: -------------------------------------------------------------------------------- 1 | <%- include('header.html') -%> 2 | 3 |

<%= siteInfo.name %>

4 | 5 | « back 6 | 7 |

User: <%= user.name %>

8 | 9 | 15 | 16 | <%- include('footer.html') -%> -------------------------------------------------------------------------------- /frontend/html/page-users.html: -------------------------------------------------------------------------------- 1 | <%- include('header.html') -%> 2 | 3 |

<%= siteInfo.name %>

4 |

Users

5 | 6 | 11 | 12 | <%- include('footer.html') -%> -------------------------------------------------------------------------------- /frontend/js/app.js: -------------------------------------------------------------------------------- 1 | import {$, $$, render, safe} from 'dat://pauls-uikit.hashbase.io/js/dom.js' 2 | 3 | var userName = localStorage.userName 4 | var userUrl = localStorage.userUrl 5 | var isCreatingUser = false 6 | var isComposing = false 7 | var userArchive 8 | 9 | main() 10 | 11 | async function main () { 12 | if (userUrl && userName) { 13 | userArchive = new DatArchive(userUrl) 14 | try { 15 | var replyComposer = $('.reply-composer') 16 | replyComposer.classList.add('visible') 17 | replyComposer.addEventListener('submit', onSubmitReply) 18 | } catch (e) {} 19 | } 20 | renderHeaderTools() 21 | } 22 | 23 | function renderHeaderTools () { 24 | var el = $('.header-tools') 25 | el.innerHTML = '' 26 | 27 | if (typeof DatArchive === 'undefined') { 28 | el.append(render(` 29 |
Read-only mode.
Visit with a dat-based browser to participate.
30 | `)) 31 | return 32 | } 33 | 34 | if (userArchive && userName) { 35 | el.append(render(` 36 |
new thread | ${safe(userName)}
37 | `)) 38 | el.querySelector('.new-thread-btn').addEventListener('click', onNewThread) 39 | } else { 40 | el.append(render(` 41 |
new user
42 | `)) 43 | el.querySelector('.create-user-btn').addEventListener('click', onCreateUser) 44 | } 45 | 46 | if (isCreatingUser) { 47 | el.append(render(` 48 |
49 |
50 | 51 | 52 | 53 | cancel 54 |
55 |
56 | `)) 57 | el.querySelector('.create-user').addEventListener('submit', onSubmitUser) 58 | el.querySelector('.create-user-cancel-btn').addEventListener('click', onCancelCreateUser) 59 | } 60 | } 61 | 62 | function renderComposer () { 63 | var el = $('.composer-container') 64 | el.innerHTML = '' 65 | 66 | if (!isComposing) { 67 | return 68 | } 69 | 70 | el.append(render(` 71 |
72 |
73 | 74 |
75 |
76 | 77 |
78 |
79 | 80 | cancel 81 |
82 |
83 | `)) 84 | 85 | el.querySelector('.composer').addEventListener('submit', onSubmitComposer) 86 | el.querySelector('.composer-cancel-btn').addEventListener('click', onCancelComposer) 87 | } 88 | 89 | async function onSubmitComposer (e) { 90 | e.preventDefault() 91 | 92 | var title = safe($('.composer #title-input').value) 93 | var body = safe($('.composer #body-input').value) 94 | 95 | // write post 96 | await userArchive.writeFile(`/posts/localhost/${Date.now()}.json`, JSON.stringify({title, body})) 97 | 98 | // refresh 99 | if (window.location.pathname === '/') { 100 | window.location.reload() 101 | } else { 102 | isComposing = false 103 | renderComposer() 104 | } 105 | } 106 | 107 | async function onSubmitReply (e) { 108 | e.preventDefault() 109 | 110 | var threadRootUrl = safe($('.reply-composer #thread-root-url-input').value) 111 | var threadParentUrl = safe($('.reply-composer #thread-parent-url-input').value) 112 | var title = safe($('.reply-composer #title-input').value) 113 | var body = safe($('.reply-composer #body-input').value) 114 | 115 | // write post 116 | await userArchive.writeFile(`/posts/localhost/${Date.now()}.json`, JSON.stringify({title, body, threadRootUrl, threadParentUrl})) 117 | 118 | // refresh 119 | window.location.reload() 120 | } 121 | 122 | async function onSubmitUser (e) { 123 | e.preventDefault() 124 | 125 | var name = safe($('#user-name-input').value) 126 | 127 | // create the archive if needed 128 | var title = `${name} (Pauls Dat Forum User)` 129 | if (!userArchive) { 130 | userArchive = await DatArchive.create({ 131 | title, 132 | description: 'User created by pauls-dat-forum', 133 | type: 'user-profile' 134 | }) 135 | } else { 136 | userArchive.configure({title}) 137 | } 138 | try { 139 | await userArchive.mkdir('/posts') 140 | await userArchive.mkdir('/posts/localhost') 141 | } catch (e) {} 142 | 143 | // add to the forum 144 | var reqBody = {name, url: userArchive.url} 145 | var res = await fetch('/users', {method: 'POST', headers: {"Content-Type": "application/json; charset=utf-8"}, body: JSON.stringify(reqBody)}) 146 | var resBody = await res.json() 147 | 148 | // handle error 149 | if (resBody.error) { 150 | try { $('.create-user .error').remove() } catch (e) {} 151 | $('.create-user').append(render(`
${safe(resBody.error)}
`)) 152 | return 153 | } 154 | 155 | // save on success 156 | localStorage.userName = name 157 | localStorage.userUrl = userArchive.url 158 | window.location.reload() 159 | } 160 | 161 | function onCreateUser (e) { 162 | e.preventDefault() 163 | isCreatingUser = true 164 | renderHeaderTools() 165 | } 166 | 167 | function onCancelCreateUser (e) { 168 | e.preventDefault() 169 | isCreatingUser = false 170 | renderHeaderTools() 171 | } 172 | 173 | function onNewThread (e) { 174 | e.preventDefault() 175 | isComposing = true 176 | renderComposer() 177 | } 178 | 179 | function onCancelComposer (e) { 180 | e.preventDefault() 181 | isComposing = false 182 | renderComposer() 183 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pauls-dat-forum", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "await-lock": { 22 | "version": "1.1.3", 23 | "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-1.1.3.tgz", 24 | "integrity": "sha512-e0jRB8X/VVxulahjW16cM1dHsO7xjyZBP8p2AnVmg2Vn3q5xJ5sTUAybmkp96+s+QcrtidSJqpCGfWhVOX7NGg==" 25 | }, 26 | "body-parser": { 27 | "version": "1.18.2", 28 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 29 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 30 | "requires": { 31 | "bytes": "3.0.0", 32 | "content-type": "1.0.4", 33 | "debug": "2.6.9", 34 | "depd": "1.1.2", 35 | "http-errors": "1.6.3", 36 | "iconv-lite": "0.4.19", 37 | "on-finished": "2.3.0", 38 | "qs": "6.5.1", 39 | "raw-body": "2.3.2", 40 | "type-is": "1.6.16" 41 | } 42 | }, 43 | "bytes": { 44 | "version": "3.0.0", 45 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 46 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 47 | }, 48 | "co": { 49 | "version": "4.6.0", 50 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 51 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", 52 | "optional": true 53 | }, 54 | "co-express": { 55 | "version": "2.0.0", 56 | "resolved": "https://registry.npmjs.org/co-express/-/co-express-2.0.0.tgz", 57 | "integrity": "sha1-z04P+org9ex8kgJcYt0atKeYcVQ=", 58 | "requires": { 59 | "co": "4.6.0" 60 | } 61 | }, 62 | "content-disposition": { 63 | "version": "0.5.2", 64 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 65 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 66 | }, 67 | "content-type": { 68 | "version": "1.0.4", 69 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 70 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 71 | }, 72 | "cookie": { 73 | "version": "0.3.1", 74 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 75 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 76 | }, 77 | "cookie-signature": { 78 | "version": "1.0.6", 79 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 80 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 81 | }, 82 | "crypto-random-string": { 83 | "version": "1.0.0", 84 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", 85 | "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", 86 | "dev": true 87 | }, 88 | "debug": { 89 | "version": "2.6.9", 90 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 91 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 92 | "requires": { 93 | "ms": "2.0.0" 94 | } 95 | }, 96 | "depd": { 97 | "version": "1.1.2", 98 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 99 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 100 | }, 101 | "destroy": { 102 | "version": "1.0.4", 103 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 104 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 105 | }, 106 | "ee-first": { 107 | "version": "1.1.1", 108 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 109 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 110 | }, 111 | "ejs": { 112 | "version": "2.6.1", 113 | "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", 114 | "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" 115 | }, 116 | "encodeurl": { 117 | "version": "1.0.2", 118 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 119 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 120 | }, 121 | "escape-html": { 122 | "version": "1.0.3", 123 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 124 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 125 | }, 126 | "etag": { 127 | "version": "1.8.1", 128 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 129 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 130 | }, 131 | "express": { 132 | "version": "4.16.3", 133 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 134 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 135 | "requires": { 136 | "accepts": "1.3.5", 137 | "array-flatten": "1.1.1", 138 | "body-parser": "1.18.2", 139 | "content-disposition": "0.5.2", 140 | "content-type": "1.0.4", 141 | "cookie": "0.3.1", 142 | "cookie-signature": "1.0.6", 143 | "debug": "2.6.9", 144 | "depd": "1.1.2", 145 | "encodeurl": "1.0.2", 146 | "escape-html": "1.0.3", 147 | "etag": "1.8.1", 148 | "finalhandler": "1.1.1", 149 | "fresh": "0.5.2", 150 | "merge-descriptors": "1.0.1", 151 | "methods": "1.1.2", 152 | "on-finished": "2.3.0", 153 | "parseurl": "1.3.2", 154 | "path-to-regexp": "0.1.7", 155 | "proxy-addr": "2.0.3", 156 | "qs": "6.5.1", 157 | "range-parser": "1.2.0", 158 | "safe-buffer": "5.1.1", 159 | "send": "0.16.2", 160 | "serve-static": "1.13.2", 161 | "setprototypeof": "1.1.0", 162 | "statuses": "1.4.0", 163 | "type-is": "1.6.16", 164 | "utils-merge": "1.0.1", 165 | "vary": "1.1.2" 166 | } 167 | }, 168 | "finalhandler": { 169 | "version": "1.1.1", 170 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 171 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 172 | "requires": { 173 | "debug": "2.6.9", 174 | "encodeurl": "1.0.2", 175 | "escape-html": "1.0.3", 176 | "on-finished": "2.3.0", 177 | "parseurl": "1.3.2", 178 | "statuses": "1.4.0", 179 | "unpipe": "1.0.0" 180 | } 181 | }, 182 | "forwarded": { 183 | "version": "0.1.2", 184 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 185 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 186 | }, 187 | "fresh": { 188 | "version": "0.5.2", 189 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 190 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 191 | }, 192 | "http-errors": { 193 | "version": "1.6.3", 194 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 195 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 196 | "requires": { 197 | "depd": "1.1.2", 198 | "inherits": "2.0.3", 199 | "setprototypeof": "1.1.0", 200 | "statuses": "1.4.0" 201 | } 202 | }, 203 | "iconv-lite": { 204 | "version": "0.4.19", 205 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 206 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 207 | }, 208 | "inherits": { 209 | "version": "2.0.3", 210 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 211 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 212 | }, 213 | "ipaddr.js": { 214 | "version": "1.6.0", 215 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", 216 | "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" 217 | }, 218 | "media-typer": { 219 | "version": "0.3.0", 220 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 221 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 222 | }, 223 | "merge-descriptors": { 224 | "version": "1.0.1", 225 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 226 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 227 | }, 228 | "methods": { 229 | "version": "1.1.2", 230 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 231 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 232 | }, 233 | "mime": { 234 | "version": "1.4.1", 235 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 236 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 237 | }, 238 | "mime-db": { 239 | "version": "1.33.0", 240 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 241 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 242 | }, 243 | "mime-types": { 244 | "version": "2.1.18", 245 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 246 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 247 | "requires": { 248 | "mime-db": "1.33.0" 249 | } 250 | }, 251 | "minimist": { 252 | "version": "0.0.8", 253 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 254 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 255 | }, 256 | "mkdirp": { 257 | "version": "0.5.1", 258 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 259 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 260 | "requires": { 261 | "minimist": "0.0.8" 262 | } 263 | }, 264 | "ms": { 265 | "version": "2.0.0", 266 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 267 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 268 | }, 269 | "nan": { 270 | "version": "2.9.2", 271 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.9.2.tgz", 272 | "integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw==" 273 | }, 274 | "negotiator": { 275 | "version": "0.6.1", 276 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 277 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 278 | }, 279 | "on-finished": { 280 | "version": "2.3.0", 281 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 282 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 283 | "requires": { 284 | "ee-first": "1.1.1" 285 | } 286 | }, 287 | "parseurl": { 288 | "version": "1.3.2", 289 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 290 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 291 | }, 292 | "path-to-regexp": { 293 | "version": "0.1.7", 294 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 295 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 296 | }, 297 | "proxy-addr": { 298 | "version": "2.0.3", 299 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", 300 | "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", 301 | "requires": { 302 | "forwarded": "0.1.2", 303 | "ipaddr.js": "1.6.0" 304 | } 305 | }, 306 | "qs": { 307 | "version": "6.5.1", 308 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 309 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 310 | }, 311 | "range-parser": { 312 | "version": "1.2.0", 313 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 314 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 315 | }, 316 | "raw-body": { 317 | "version": "2.3.2", 318 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 319 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 320 | "requires": { 321 | "bytes": "3.0.0", 322 | "http-errors": "1.6.2", 323 | "iconv-lite": "0.4.19", 324 | "unpipe": "1.0.0" 325 | }, 326 | "dependencies": { 327 | "depd": { 328 | "version": "1.1.1", 329 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 330 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 331 | }, 332 | "http-errors": { 333 | "version": "1.6.2", 334 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 335 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 336 | "requires": { 337 | "depd": "1.1.1", 338 | "inherits": "2.0.3", 339 | "setprototypeof": "1.0.3", 340 | "statuses": "1.4.0" 341 | } 342 | }, 343 | "setprototypeof": { 344 | "version": "1.0.3", 345 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 346 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 347 | } 348 | } 349 | }, 350 | "safe-buffer": { 351 | "version": "5.1.1", 352 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 353 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 354 | }, 355 | "send": { 356 | "version": "0.16.2", 357 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 358 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 359 | "requires": { 360 | "debug": "2.6.9", 361 | "depd": "1.1.2", 362 | "destroy": "1.0.4", 363 | "encodeurl": "1.0.2", 364 | "escape-html": "1.0.3", 365 | "etag": "1.8.1", 366 | "fresh": "0.5.2", 367 | "http-errors": "1.6.3", 368 | "mime": "1.4.1", 369 | "ms": "2.0.0", 370 | "on-finished": "2.3.0", 371 | "range-parser": "1.2.0", 372 | "statuses": "1.4.0" 373 | } 374 | }, 375 | "serve-static": { 376 | "version": "1.13.2", 377 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 378 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 379 | "requires": { 380 | "encodeurl": "1.0.2", 381 | "escape-html": "1.0.3", 382 | "parseurl": "1.3.2", 383 | "send": "0.16.2" 384 | } 385 | }, 386 | "setprototypeof": { 387 | "version": "1.1.0", 388 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 389 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 390 | }, 391 | "sqlite": { 392 | "version": "2.9.2", 393 | "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-2.9.2.tgz", 394 | "integrity": "sha512-6rlm1AimZksjZAstmqqnUsmFQvk0LKNaedi9iTSd5e9kWLzN5JRljHFlARLLUudQklmc9nNtVm/vjo3svCKQTw==", 395 | "requires": { 396 | "sqlite3": "4.0.0" 397 | } 398 | }, 399 | "sqlite3": { 400 | "version": "4.0.0", 401 | "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.0.tgz", 402 | "integrity": "sha512-6OlcAQNGaRSBLK1CuaRbKwlMFBb9DEhzmZyQP+fltNRF6XcIMpVIfXCBEcXPe1d4v9LnhkQUYkknDbA5JReqJg==", 403 | "requires": { 404 | "nan": "2.9.2", 405 | "node-pre-gyp": "0.9.0" 406 | }, 407 | "dependencies": { 408 | "abbrev": { 409 | "version": "1.1.1", 410 | "bundled": true 411 | }, 412 | "ansi-regex": { 413 | "version": "2.1.1", 414 | "bundled": true 415 | }, 416 | "aproba": { 417 | "version": "1.2.0", 418 | "bundled": true 419 | }, 420 | "are-we-there-yet": { 421 | "version": "1.1.4", 422 | "bundled": true, 423 | "requires": { 424 | "delegates": "1.0.0", 425 | "readable-stream": "2.3.5" 426 | } 427 | }, 428 | "balanced-match": { 429 | "version": "1.0.0", 430 | "bundled": true 431 | }, 432 | "brace-expansion": { 433 | "version": "1.1.11", 434 | "bundled": true, 435 | "requires": { 436 | "balanced-match": "1.0.0", 437 | "concat-map": "0.0.1" 438 | } 439 | }, 440 | "chownr": { 441 | "version": "1.0.1", 442 | "bundled": true 443 | }, 444 | "code-point-at": { 445 | "version": "1.1.0", 446 | "bundled": true 447 | }, 448 | "concat-map": { 449 | "version": "0.0.1", 450 | "bundled": true 451 | }, 452 | "console-control-strings": { 453 | "version": "1.1.0", 454 | "bundled": true 455 | }, 456 | "core-util-is": { 457 | "version": "1.0.2", 458 | "bundled": true 459 | }, 460 | "debug": { 461 | "version": "2.6.9", 462 | "bundled": true, 463 | "requires": { 464 | "ms": "2.0.0" 465 | } 466 | }, 467 | "deep-extend": { 468 | "version": "0.4.2", 469 | "bundled": true 470 | }, 471 | "delegates": { 472 | "version": "1.0.0", 473 | "bundled": true 474 | }, 475 | "detect-libc": { 476 | "version": "1.0.3", 477 | "bundled": true 478 | }, 479 | "fs-minipass": { 480 | "version": "1.2.5", 481 | "bundled": true, 482 | "requires": { 483 | "minipass": "2.2.1" 484 | } 485 | }, 486 | "fs.realpath": { 487 | "version": "1.0.0", 488 | "bundled": true 489 | }, 490 | "gauge": { 491 | "version": "2.7.4", 492 | "bundled": true, 493 | "requires": { 494 | "aproba": "1.2.0", 495 | "console-control-strings": "1.1.0", 496 | "has-unicode": "2.0.1", 497 | "object-assign": "4.1.1", 498 | "signal-exit": "3.0.2", 499 | "string-width": "1.0.2", 500 | "strip-ansi": "3.0.1", 501 | "wide-align": "1.1.2" 502 | } 503 | }, 504 | "glob": { 505 | "version": "7.1.2", 506 | "bundled": true, 507 | "requires": { 508 | "fs.realpath": "1.0.0", 509 | "inflight": "1.0.6", 510 | "inherits": "2.0.3", 511 | "minimatch": "3.0.4", 512 | "once": "1.4.0", 513 | "path-is-absolute": "1.0.1" 514 | } 515 | }, 516 | "has-unicode": { 517 | "version": "2.0.1", 518 | "bundled": true 519 | }, 520 | "iconv-lite": { 521 | "version": "0.4.19", 522 | "bundled": true 523 | }, 524 | "ignore-walk": { 525 | "version": "3.0.1", 526 | "bundled": true, 527 | "requires": { 528 | "minimatch": "3.0.4" 529 | } 530 | }, 531 | "inflight": { 532 | "version": "1.0.6", 533 | "bundled": true, 534 | "requires": { 535 | "once": "1.4.0", 536 | "wrappy": "1.0.2" 537 | } 538 | }, 539 | "inherits": { 540 | "version": "2.0.3", 541 | "bundled": true 542 | }, 543 | "ini": { 544 | "version": "1.3.5", 545 | "bundled": true 546 | }, 547 | "is-fullwidth-code-point": { 548 | "version": "1.0.0", 549 | "bundled": true, 550 | "requires": { 551 | "number-is-nan": "1.0.1" 552 | } 553 | }, 554 | "isarray": { 555 | "version": "1.0.0", 556 | "bundled": true 557 | }, 558 | "minimatch": { 559 | "version": "3.0.4", 560 | "bundled": true, 561 | "requires": { 562 | "brace-expansion": "1.1.11" 563 | } 564 | }, 565 | "minimist": { 566 | "version": "0.0.8", 567 | "bundled": true 568 | }, 569 | "minipass": { 570 | "version": "2.2.1", 571 | "bundled": true, 572 | "requires": { 573 | "yallist": "3.0.2" 574 | } 575 | }, 576 | "minizlib": { 577 | "version": "1.1.0", 578 | "bundled": true, 579 | "requires": { 580 | "minipass": "2.2.1" 581 | } 582 | }, 583 | "mkdirp": { 584 | "version": "0.5.1", 585 | "bundled": true, 586 | "requires": { 587 | "minimist": "0.0.8" 588 | } 589 | }, 590 | "ms": { 591 | "version": "2.0.0", 592 | "bundled": true 593 | }, 594 | "needle": { 595 | "version": "2.2.0", 596 | "bundled": true, 597 | "requires": { 598 | "debug": "2.6.9", 599 | "iconv-lite": "0.4.19", 600 | "sax": "1.2.4" 601 | } 602 | }, 603 | "node-pre-gyp": { 604 | "version": "0.9.0", 605 | "bundled": true, 606 | "requires": { 607 | "detect-libc": "1.0.3", 608 | "mkdirp": "0.5.1", 609 | "needle": "2.2.0", 610 | "nopt": "4.0.1", 611 | "npm-packlist": "1.1.10", 612 | "npmlog": "4.1.2", 613 | "rc": "1.2.6", 614 | "rimraf": "2.6.2", 615 | "semver": "5.5.0", 616 | "tar": "4.4.0" 617 | } 618 | }, 619 | "nopt": { 620 | "version": "4.0.1", 621 | "bundled": true, 622 | "requires": { 623 | "abbrev": "1.1.1", 624 | "osenv": "0.1.5" 625 | } 626 | }, 627 | "npm-bundled": { 628 | "version": "1.0.3", 629 | "bundled": true 630 | }, 631 | "npm-packlist": { 632 | "version": "1.1.10", 633 | "bundled": true, 634 | "requires": { 635 | "ignore-walk": "3.0.1", 636 | "npm-bundled": "1.0.3" 637 | } 638 | }, 639 | "npmlog": { 640 | "version": "4.1.2", 641 | "bundled": true, 642 | "requires": { 643 | "are-we-there-yet": "1.1.4", 644 | "console-control-strings": "1.1.0", 645 | "gauge": "2.7.4", 646 | "set-blocking": "2.0.0" 647 | } 648 | }, 649 | "number-is-nan": { 650 | "version": "1.0.1", 651 | "bundled": true 652 | }, 653 | "object-assign": { 654 | "version": "4.1.1", 655 | "bundled": true 656 | }, 657 | "once": { 658 | "version": "1.4.0", 659 | "bundled": true, 660 | "requires": { 661 | "wrappy": "1.0.2" 662 | } 663 | }, 664 | "os-homedir": { 665 | "version": "1.0.2", 666 | "bundled": true 667 | }, 668 | "os-tmpdir": { 669 | "version": "1.0.2", 670 | "bundled": true 671 | }, 672 | "osenv": { 673 | "version": "0.1.5", 674 | "bundled": true, 675 | "requires": { 676 | "os-homedir": "1.0.2", 677 | "os-tmpdir": "1.0.2" 678 | } 679 | }, 680 | "path-is-absolute": { 681 | "version": "1.0.1", 682 | "bundled": true 683 | }, 684 | "process-nextick-args": { 685 | "version": "2.0.0", 686 | "bundled": true 687 | }, 688 | "rc": { 689 | "version": "1.2.6", 690 | "bundled": true, 691 | "requires": { 692 | "deep-extend": "0.4.2", 693 | "ini": "1.3.5", 694 | "minimist": "1.2.0", 695 | "strip-json-comments": "2.0.1" 696 | }, 697 | "dependencies": { 698 | "minimist": { 699 | "version": "1.2.0", 700 | "bundled": true 701 | } 702 | } 703 | }, 704 | "readable-stream": { 705 | "version": "2.3.5", 706 | "bundled": true, 707 | "requires": { 708 | "core-util-is": "1.0.2", 709 | "inherits": "2.0.3", 710 | "isarray": "1.0.0", 711 | "process-nextick-args": "2.0.0", 712 | "safe-buffer": "5.1.1", 713 | "string_decoder": "1.0.3", 714 | "util-deprecate": "1.0.2" 715 | } 716 | }, 717 | "rimraf": { 718 | "version": "2.6.2", 719 | "bundled": true, 720 | "requires": { 721 | "glob": "7.1.2" 722 | } 723 | }, 724 | "safe-buffer": { 725 | "version": "5.1.1", 726 | "bundled": true 727 | }, 728 | "sax": { 729 | "version": "1.2.4", 730 | "bundled": true 731 | }, 732 | "semver": { 733 | "version": "5.5.0", 734 | "bundled": true 735 | }, 736 | "set-blocking": { 737 | "version": "2.0.0", 738 | "bundled": true 739 | }, 740 | "signal-exit": { 741 | "version": "3.0.2", 742 | "bundled": true 743 | }, 744 | "string-width": { 745 | "version": "1.0.2", 746 | "bundled": true, 747 | "requires": { 748 | "code-point-at": "1.1.0", 749 | "is-fullwidth-code-point": "1.0.0", 750 | "strip-ansi": "3.0.1" 751 | } 752 | }, 753 | "string_decoder": { 754 | "version": "1.0.3", 755 | "bundled": true, 756 | "requires": { 757 | "safe-buffer": "5.1.1" 758 | } 759 | }, 760 | "strip-ansi": { 761 | "version": "3.0.1", 762 | "bundled": true, 763 | "requires": { 764 | "ansi-regex": "2.1.1" 765 | } 766 | }, 767 | "strip-json-comments": { 768 | "version": "2.0.1", 769 | "bundled": true 770 | }, 771 | "tar": { 772 | "version": "4.4.0", 773 | "bundled": true, 774 | "requires": { 775 | "chownr": "1.0.1", 776 | "fs-minipass": "1.2.5", 777 | "minipass": "2.2.1", 778 | "minizlib": "1.1.0", 779 | "mkdirp": "0.5.1", 780 | "yallist": "3.0.2" 781 | } 782 | }, 783 | "util-deprecate": { 784 | "version": "1.0.2", 785 | "bundled": true 786 | }, 787 | "wide-align": { 788 | "version": "1.1.2", 789 | "bundled": true, 790 | "requires": { 791 | "string-width": "1.0.2" 792 | } 793 | }, 794 | "wrappy": { 795 | "version": "1.0.2", 796 | "bundled": true 797 | }, 798 | "yallist": { 799 | "version": "3.0.2", 800 | "bundled": true 801 | } 802 | } 803 | }, 804 | "statuses": { 805 | "version": "1.4.0", 806 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 807 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 808 | }, 809 | "temp-dir": { 810 | "version": "1.0.0", 811 | "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", 812 | "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", 813 | "dev": true 814 | }, 815 | "tempy": { 816 | "version": "0.2.1", 817 | "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.2.1.tgz", 818 | "integrity": "sha512-LB83o9bfZGrntdqPuRdanIVCPReam9SOZKW0fOy5I9X3A854GGWi0tjCqoXEk84XIEYBc/x9Hq3EFop/H5wJaw==", 819 | "dev": true, 820 | "requires": { 821 | "temp-dir": "1.0.0", 822 | "unique-string": "1.0.0" 823 | } 824 | }, 825 | "type-is": { 826 | "version": "1.6.16", 827 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 828 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 829 | "requires": { 830 | "media-typer": "0.3.0", 831 | "mime-types": "2.1.18" 832 | } 833 | }, 834 | "unique-string": { 835 | "version": "1.0.0", 836 | "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", 837 | "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", 838 | "dev": true, 839 | "requires": { 840 | "crypto-random-string": "1.0.0" 841 | } 842 | }, 843 | "unpipe": { 844 | "version": "1.0.0", 845 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 846 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 847 | }, 848 | "utils-merge": { 849 | "version": "1.0.1", 850 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 851 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 852 | }, 853 | "vary": { 854 | "version": "1.1.2", 855 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 856 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 857 | } 858 | } 859 | } 860 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pauls-dat-forum", 3 | "version": "1.0.0", 4 | "description": "A Web forum built on Dat and HTTP", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Paul Frazee ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "await-lock": "^1.1.3", 13 | "co-express": "^2.0.0", 14 | "ejs": "^2.6.1", 15 | "express": "^4.16.3", 16 | "mkdirp": "^0.5.1", 17 | "sqlite": "^2.9.2" 18 | }, 19 | "devDependencies": { 20 | "tempy": "^0.2.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pfrazee/node-http-dat-forum/052321a909456cd2e002238095d64bdaf41a543c/screenshot.png -------------------------------------------------------------------------------- /test-run.js: -------------------------------------------------------------------------------- 1 | const tempy = require('tempy') 2 | const {setup} = require('./backend') 3 | 4 | main() 5 | async function main () { 6 | var hostname = 'localhost' 7 | var dataPath = tempy.directory() 8 | var server = await setup(hostname, dataPath) 9 | 10 | console.log('generate test users') 11 | var [alice, bob, carla] = await Promise.all([ 12 | server.dat.createArchive({title: 'Alice', description: 'Test user for pauls-dat-forum', type: ['user-profile']}), 13 | server.dat.createArchive({title: 'Bob', description: 'Test user for pauls-dat-forum', type: ['user-profile']}), 14 | server.dat.createArchive({title: 'Carla', description: 'Test user for pauls-dat-forum', type: ['user-profile']}) 15 | ]) 16 | 17 | console.log('create their directory structures') 18 | async function createFolders (archive) { 19 | await archive.mkdir('/posts') 20 | await archive.mkdir(`/posts/${hostname}`) 21 | } 22 | await Promise.all([ 23 | createFolders(alice), 24 | createFolders(bob), 25 | createFolders(carla) 26 | ]) 27 | 28 | console.log('write a few threads') 29 | var _n = 1 30 | async function writePost (archive, post) { 31 | var path = `/posts/${hostname}/${_n++}.json` 32 | await archive.writeFile(path, JSON.stringify(post)) 33 | return archive.url + path 34 | } 35 | var thread1 = await writePost(alice, {title: 'Hello, world!', body: 'How well is this forum working?'}) 36 | var thread1reply1 = await writePost(bob, {title: 'RE: Hello, world!', body: 'Hi Alice!', threadRootUrl: thread1, threadParentUrl: thread1}) 37 | var thread1reply2 = await writePost(carla, {title: 'RE: Hello, world!', body: 'Hi Alice and Bob!', threadRootUrl: thread1, threadParentUrl: thread1reply1}) 38 | var thread2 = await writePost(alice, {title: 'Hello, world!', body: 'How well is this forum working?'}) 39 | var thread2reply1 = await writePost(bob, {title: 'RE: Hello, world!', body: 'Hi Alice!', threadRootUrl: thread2, threadParentUrl: thread2}) 40 | var thread2reply2 = await writePost(carla, {title: 'RE: Hello, world!', body: 'Hi Alice and Bob!', threadRootUrl: thread2, threadParentUrl: thread2reply1}) 41 | 42 | console.log('add users') 43 | await server.addUser({name: 'alice', url: alice.url}) 44 | await server.addUser({name: 'bob', url: bob.url}) 45 | await server.addUser({name: 'carla', url: carla.url}) 46 | 47 | console.log('ready! explore at localhost:3000') 48 | } --------------------------------------------------------------------------------