From cb137ffcb1e5623fb495fd51893bc04d1ab8394c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 12 Sep 2019 20:03:04 -0500 Subject: [PATCH] mongo for user object requests and initial inbox functionality --- db/setup.js | 30 +++++ index.js | 70 +++++++--- package.json | 6 +- routes/inbox.js | 127 ++++-------------- routes/user.js | 84 ++++++------ .../requests/activity_inbox_post.http-request | 14 ++ utils/index.js | 38 ++++++ 7 files changed, 202 insertions(+), 167 deletions(-) create mode 100644 db/setup.js create mode 100644 test/requests/activity_inbox_post.http-request create mode 100644 utils/index.js diff --git a/db/setup.js b/db/setup.js new file mode 100644 index 0000000..2975b6c --- /dev/null +++ b/db/setup.js @@ -0,0 +1,30 @@ +module.exports = function dbSetup (db, domain) { + return db.collection('streams').createIndex({ + _target: 1, + _id: -1, + }).then(() => { + return db.collection('objects').findOneAndReplace( + {preferredUsername: 'dummy'}, + { + id: `https://${domain}/u/dummy`, + "type": "Person", + "following": `https://${domain}/u/dummy/following`, + "followers": `https://${domain}/u/dummy/followers`, + "liked": `https://${domain}/u/dummy/liked`, + "inbox": `https://${domain}/u/dummy/inbox`, + "outbox": `https://${domain}/u/dummy/outbox`, + "preferredUsername": "dummy", + "name": "Dummy Person", + "summary": "Gotta have someone in the db", + "icon": `http://${domain}/f/dummy.png`, + attachment: [ + `http://${domain}/f/dummy.glb` + ] + }, + { + upsert: true, + returnOriginal: false, + } + ) + }) +} \ No newline at end of file diff --git a/index.js b/index.js index 2c7ebbf..8e75bcb 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,21 @@ +const { promisify } = require('util') const config = require('./config.json'); const { USER, PASS, DOMAIN, PRIVKEY_PATH, CERT_PATH, PORT } = config; const express = require('express'); const app = express(); -const Database = require('better-sqlite3'); -const db = new Database('bot-node.db'); +const MongoClient = require('mongodb').MongoClient; +// Connection URL +const url = 'mongodb://localhost:27017'; + +const dbSetup = require('./db/setup'); +// Database Name +const dbName = 'test'; + +// Create a new MongoClient +const client = new MongoClient(url, {useUnifiedTopology: true}); + +let db; + const fs = require('fs'); const routes = require('./routes'), bodyParser = require('body-parser'), @@ -27,16 +39,13 @@ try { } } -// if there is no `accounts` table in the DB, create an empty table -db.prepare('CREATE TABLE IF NOT EXISTS accounts (name TEXT PRIMARY KEY, privkey TEXT, pubkey TEXT, webfinger TEXT, actor TEXT, apikey TEXT, followers TEXT, messages TEXT)').run(); -// if there is no `messages` table in the DB, create an empty table -db.prepare('CREATE TABLE IF NOT EXISTS messages (guid TEXT PRIMARY KEY, message TEXT)').run(); - -app.set('db', db); app.set('domain', DOMAIN); app.set('port', process.env.PORT || PORT || 3000); app.set('port-https', process.env.PORT_HTTPS || 8443); -app.use(bodyParser.json({type: 'application/activity+json'})); // support json encoded bodies +app.use(bodyParser.json({type: [ + 'application/activity+json', + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' +]})); // support json encoded bodies app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies // basic http authorizer @@ -59,24 +68,49 @@ function asyncAuthorizer(username, password, cb) { } } +app.param('name', function (req, res, next, id) { + req.user = id + next() +}) + app.get('/', (req, res) => res.send('Hello World!')); // admin page app.options('/api', cors()); app.use('/api', cors(), routes.api); app.use('/api/admin', cors({ credentials: true, origin: true }), basicUserAuth, routes.admin); -app.use('/admin', express.static('public/admin')); app.use('/.well-known/webfinger', cors(), routes.webfinger); app.use('/u', cors(), routes.user); app.use('/m', cors(), routes.message); -app.use('/api/inbox', cors(), routes.inbox); +// app.use('/api/inbox', cors(), routes.inbox); +app.use('/u/:name/inbox', routes.inbox); +app.use('/admin', express.static('public/admin')); +app.use('/f', express.static('public/files')); app.use('/hubs', express.static('../hubs/dist')); -http.createServer(app).listen(app.get('port'), function(){ - console.log('Express server listening on port ' + app.get('port')); -}); -if (sslOptions) { - https.createServer(sslOptions, app).listen(app.get('port-https'), function () { - console.log('Express server listening on port ' + app.get('port-https')); +// Use connect method to connect to the Server +let objs +client.connect({useNewUrlParser: true}) + .then(() => { + console.log("Connected successfully to server"); + db = client.db(dbName); + app.set('db', db); + objs = db.collection('objects'); + app.set('objs', db.collection('objects')); + + return dbSetup(db, DOMAIN) + }) + + .then(() => { + http.createServer(app).listen(app.get('port'), function(){ + console.log('Express server listening on port ' + app.get('port')); + }); + if (sslOptions) { + https.createServer(sslOptions, app).listen(app.get('port-https'), function () { + console.log('Express server listening on port ' + app.get('port-https')); + }); + } + }) + .catch(err => { + throw new Error(err) }); -} \ No newline at end of file diff --git a/package.json b/package.json index b749408..568cedd 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,19 @@ "description": "", "main": "index.js", "dependencies": { - "better-sqlite3": "^5.4.0", "body-parser": "^1.18.3", "cors": "^2.8.4", "express": "^4.16.3", "express-basic-auth": "^1.1.5", + "mongodb": "^3.3.2", "request": "^2.87.0" }, "engines": { "node": ">=10.10.0" }, - "devDependencies": {}, + "devDependencies": { + "standardjs": "^1.0.0-alpha" + }, "scripts": { "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/routes/inbox.js b/routes/inbox.js index 424f1ee..da25075 100644 --- a/routes/inbox.js +++ b/routes/inbox.js @@ -1,109 +1,32 @@ -'use strict'; const express = require('express'), - crypto = require('crypto'), - request = require('request'), router = express.Router(); - -function signAndSend(message, name, domain, req, res, targetDomain) { - // get the URI of the actor object and append 'inbox' to it - let inbox = message.object.actor+'/inbox'; - let inboxFragment = inbox.replace('https://'+targetDomain,''); - // get the private key - let db = req.app.get('db'); - let result = db.prepare('select privkey from accounts where name = ?').get(`${name}@${domain}`); - if (result === undefined) { - return res.status(404).send(`No record found for ${name}.`); - } - else { - let privkey = result.privkey; - const signer = crypto.createSign('sha256'); - let d = new Date(); - let stringToSign = `(request-target): post ${inboxFragment}\nhost: ${targetDomain}\ndate: ${d.toUTCString()}`; - signer.update(stringToSign); - signer.end(); - const signature = signer.sign(privkey); - const signature_b64 = signature.toString('base64'); - let header = `keyId="https://${domain}/u/${name}",headers="(request-target) host date",signature="${signature_b64}"`; - request({ - url: inbox, - headers: { - 'Host': targetDomain, - 'Date': d.toUTCString(), - 'Signature': header - }, - method: 'POST', - json: true, - body: message - }, function (error, response){ - if (error) { - console.log('Error:', error, response.body); - } - else { - console.log('Response:', response.body); - } - }); - return res.status(200); - } -} - -function sendAcceptMessage(thebody, name, domain, req, res, targetDomain) { - const guid = crypto.randomBytes(16).toString('hex'); - let message = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': `https://${domain}/${guid}`, - 'type': 'Accept', - 'actor': `https://${domain}/u/${name}`, - 'object': thebody, - }; - signAndSend(message, name, domain, req, res, targetDomain); -} - -function parseJSON(text) { - try { - return JSON.parse(text); - } catch(e) { - return null; - } -} +const utils = require('../utils') router.post('/', function (req, res) { - // pass in a name for an account, if the account doesn't exist, create it! - let domain = req.app.get('domain'); - const myURL = new URL(req.body.actor); - let targetDomain = myURL.hostname; - // TODO: add "Undo" follow event - if (typeof req.body.object === 'string' && req.body.type === 'Follow') { - let name = req.body.object.replace(`https://${domain}/u/`,''); - sendAcceptMessage(req.body, name, domain, req, res, targetDomain); - // Add the user to the DB of accounts that follow the account - let db = req.app.get('db'); - // get the followers JSON for the user - let result = db.prepare('select followers from accounts where name = ?').get(`${name}@${domain}`); - if (result === undefined) { - console.log(`No record found for ${name}.`); - } - else { - // update followers - let followers = parseJSON(result.followers); - if (followers) { - followers.push(req.body.actor); - // unique items - followers = [...new Set(followers)]; - } - else { - followers = [req.body.actor]; - } - let followersText = JSON.stringify(followers); - try { - // update into DB - let newFollowers = db.prepare('update accounts set followers=? where name = ?').run(followersText, `${name}@${domain}`); - console.log('updated followers!', newFollowers); - } - catch(e) { - console.log('error', e); - } - } - } + const db = req.app.get('db'); + req.body._target = req.user + delete req.body['@context'] + db.collection('streams').insertOne(req.body) + .then(() => res.status(200).send()) + .catch(err => { + console.log(err) + res.status(500).send() + }) }); +router.get('/', async function (req, res) { + const db = req.app.get('db'); + db.collection('streams') + .find({_target: req.user}) + .sort({_id: -1}) + .project({_id: 0, _target: 0}) + .toArray() + .then(stream => res.json(utils.arrayToCollection(stream, true))) + .catch(err => { + console.log(err) + return res.status(500).send() + }) + ; +}) + module.exports = router; diff --git a/routes/user.js b/routes/user.js index 65d627b..0688f8a 100644 --- a/routes/user.js +++ b/routes/user.js @@ -1,60 +1,54 @@ 'use strict'; const express = require('express'), router = express.Router(); +// const inbox = require('./inbox'); +const {toJSONLD} = require('../utils/index.js'); -router.get('/:name', function (req, res) { +router.get('/:name', async function (req, res) { let name = req.params.name; if (!name) { return res.status(400).send('Bad request.'); } else { - let db = req.app.get('db'); - let domain = req.app.get('domain'); - let username = name; - name = `${name}@${domain}`; - let result = db.prepare('select actor from accounts where name = ?').get(name); - if (result === undefined) { - return res.status(404).send(`No record found for ${name}.`); - } - else { - let tempActor = JSON.parse(result.actor); - // Added this followers URI for Pleroma compatibility, see https://github.com/dariusk/rss-to-activitypub/issues/11#issuecomment-471390881 - // New Actors should have this followers URI but in case of migration from an old version this will add it in on the fly - if (tempActor.followers === undefined) { - tempActor.followers = `https://${domain}/u/${username}/followers`; - } - res.json(tempActor); + let objs = req.app.get('objs'); + const id = `https://${req.app.get('domain')}/u/${name}` + console.log(`looking up '${id}'`) + const user = await objs.findOne({type: 'Person', id: id}, {fields: {_id: 0}}) + // .project({_id: 0}) + if (user) { + return res.json(toJSONLD(user)) } + return res.status(404).send('Person not found') } }); -router.get('/:name/followers', function (req, res) { - let name = req.params.name; - if (!name) { - return res.status(400).send('Bad request.'); - } - else { - let db = req.app.get('db'); - let domain = req.app.get('domain'); - let result = db.prepare('select followers from accounts where name = ?').get(`${name}@${domain}`); - console.log(result); - result.followers = result.followers || '[]'; - let followers = JSON.parse(result.followers); - let followersCollection = { - "type":"OrderedCollection", - "totalItems":followers.length, - "id":`https://${domain}/u/${name}/followers`, - "first": { - "type":"OrderedCollectionPage", - "totalItems":followers.length, - "partOf":`https://${domain}/u/${name}/followers`, - "orderedItems": followers, - "id":`https://${domain}/u/${name}/followers?page=1` - }, - "@context":["https://www.w3.org/ns/activitystreams"] - }; - res.json(followersCollection); - } -}); +// router.get('/:name/followers', function (req, res) { +// let name = req.params.name; +// if (!name) { +// return res.status(400).send('Bad request.'); +// } +// else { +// let db = req.app.get('db'); +// let domain = req.app.get('domain'); +// let result = db.prepare('select followers from accounts where name = ?').get(`${name}@${domain}`); +// console.log(result); +// result.followers = result.followers || '[]'; +// let followers = JSON.parse(result.followers); +// let followersCollection = { +// "type":"OrderedCollection", +// "totalItems":followers.length, +// "id":`https://${domain}/u/${name}/followers`, +// "first": { +// "type":"OrderedCollectionPage", +// "totalItems":followers.length, +// "partOf":`https://${domain}/u/${name}/followers`, +// "orderedItems": followers, +// "id":`https://${domain}/u/${name}/followers?page=1` +// }, +// "@context":["https://www.w3.org/ns/activitystreams"] +// }; +// res.json(toJSONLD(followersCollection)); +// } +// }); module.exports = router; diff --git a/test/requests/activity_inbox_post.http-request b/test/requests/activity_inbox_post.http-request new file mode 100644 index 0000000..dcb8286 --- /dev/null +++ b/test/requests/activity_inbox_post.http-request @@ -0,0 +1,14 @@ +{ + "body": { + "content": "ew0KICAiQGNvbnRleHQiOiAiaHR0cHM6Ly93d3cudzMub3JnL25zL2FjdGl2aXR5c3RyZWFtcyIsDQogICJ0eXBlIjogIkNyZWF0ZSIsDQogICJpZCI6ICJodHRwczovL2V4YW1wbGUubmV0L35tYWxsb3J5Lzg3Mzc0IiwNCiAgImFjdG9yIjogImh0dHBzOi8vZXhhbXBsZS5uZXQvfm1hbGxvcnkiLA0KICAib2JqZWN0Ijogew0KICAgICJpZCI6ICJodHRwczovL2V4YW1wbGUuY29tL35tYWxsb3J5L25vdGUvNzIiLA0KICAgICJ0eXBlIjogIk5vdGUiLA0KICAgICJhdHRyaWJ1dGVkVG8iOiAiaHR0cHM6Ly9leGFtcGxlLm5ldC9+bWFsbG9yeSIsDQogICAgImNvbnRlbnQiOiAiVGhpcyBpcyBhIG5vdGUiLA0KICAgICJwdWJsaXNoZWQiOiAiMjAxNS0wMi0xMFQxNTowNDo1NVoiLA0KICAgICJ0byI6IFsiaHR0cHM6Ly9leGFtcGxlLm9yZy9+am9obi8iXSwNCiAgICAiY2MiOiBbImh0dHBzOi8vZXhhbXBsZS5jb20vfmVyaWsvZm9sbG93ZXJzIiwNCiAgICAgICAgICAgImh0dHBzOi8vd3d3LnczLm9yZy9ucy9hY3Rpdml0eXN0cmVhbXMjUHVibGljIl0NCiAgfSwNCiAgInB1Ymxpc2hlZCI6ICIyMDE1LTAyLTEwVDE1OjA0OjU1WiIsDQogICJ0byI6IFsiaHR0cHM6Ly9leGFtcGxlLm9yZy9+am9obi8iXSwNCiAgImNjIjogWyJodHRwczovL2V4YW1wbGUuY29tL35lcmlrL2ZvbGxvd2VycyIsDQogICAgICAgICAiaHR0cHM6Ly93d3cudzMub3JnL25zL2FjdGl2aXR5c3RyZWFtcyNQdWJsaWMiXQ0KfQ==", + "file": "c:\\Users\\William\\Desktop\\activity.json", + "fileSize": 736 + }, + "headers": { + "content-length": "736", + "content-type": "application/activity+json" + }, + "method": "POST", + "title": "New HTTP Request", + "url": "http://localhost:3000/u/dummy/inbox" +} \ No newline at end of file diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..71b8fc9 --- /dev/null +++ b/utils/index.js @@ -0,0 +1,38 @@ +const ASContext = 'https://www.w3.org/ns/activitystreams'; + +function convertId(obj) { + if (obj._id) { + obj.id = obj._id + delete obj._id + } + return obj +} +function isObject(value) { + return value && typeof value === 'object' && value.constructor === Object +} +// outtermost closure starts the recursion counter +// const level = 0; +function traverseObject(obj, f) { + const traverse = o => { + // const level = level + 1 + // if (level > 5) return o + traverseObject(o, f) + } + if (!isObject(obj)) return obj; + Object.keys(obj).forEach(traverse) + return f(obj); +} +module.exports.toJSONLD = function (obj) { + obj['@context'] = ASContext; + return obj; +} + +module.exports.arrayToCollection = function (arr, ordered) { + + return { + '@context': ASContext, + totalItems: arr.length, + type: ordered ? 'orderedCollection' : 'collection', + [ordered ? 'orderedItems' : 'items']: arr, + } +} \ No newline at end of file