mongo for user object requests and initial inbox functionality

This commit is contained in:
Will Murphy 2019-09-12 20:03:04 -05:00
parent b11f50d395
commit cb137ffcb1
7 changed files with 202 additions and 167 deletions

30
db/setup.js Normal file
View file

@ -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,
}
)
})
}

View file

@ -1,9 +1,21 @@
const { promisify } = require('util')
const config = require('./config.json'); const config = require('./config.json');
const { USER, PASS, DOMAIN, PRIVKEY_PATH, CERT_PATH, PORT } = config; const { USER, PASS, DOMAIN, PRIVKEY_PATH, CERT_PATH, PORT } = config;
const express = require('express'); const express = require('express');
const app = express(); const app = express();
const Database = require('better-sqlite3'); const MongoClient = require('mongodb').MongoClient;
const db = new Database('bot-node.db'); // 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 fs = require('fs');
const routes = require('./routes'), const routes = require('./routes'),
bodyParser = require('body-parser'), 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('domain', DOMAIN);
app.set('port', process.env.PORT || PORT || 3000); app.set('port', process.env.PORT || PORT || 3000);
app.set('port-https', process.env.PORT_HTTPS || 8443); 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 app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies
// basic http authorizer // basic http authorizer
@ -59,19 +68,40 @@ 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!')); app.get('/', (req, res) => res.send('Hello World!'));
// admin page // admin page
app.options('/api', cors()); app.options('/api', cors());
app.use('/api', cors(), routes.api); app.use('/api', cors(), routes.api);
app.use('/api/admin', cors({ credentials: true, origin: true }), basicUserAuth, routes.admin); 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('/.well-known/webfinger', cors(), routes.webfinger);
app.use('/u', cors(), routes.user); app.use('/u', cors(), routes.user);
app.use('/m', cors(), routes.message); 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')); app.use('/hubs', express.static('../hubs/dist'));
// 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(){ http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port')); console.log('Express server listening on port ' + app.get('port'));
}); });
@ -80,3 +110,7 @@ if (sslOptions) {
console.log('Express server listening on port ' + app.get('port-https')); console.log('Express server listening on port ' + app.get('port-https'));
}); });
} }
})
.catch(err => {
throw new Error(err)
});

View file

@ -4,17 +4,19 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"better-sqlite3": "^5.4.0",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"cors": "^2.8.4", "cors": "^2.8.4",
"express": "^4.16.3", "express": "^4.16.3",
"express-basic-auth": "^1.1.5", "express-basic-auth": "^1.1.5",
"mongodb": "^3.3.2",
"request": "^2.87.0" "request": "^2.87.0"
}, },
"engines": { "engines": {
"node": ">=10.10.0" "node": ">=10.10.0"
}, },
"devDependencies": {}, "devDependencies": {
"standardjs": "^1.0.0-alpha"
},
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"

View file

@ -1,109 +1,32 @@
'use strict';
const express = require('express'), const express = require('express'),
crypto = require('crypto'),
request = require('request'),
router = express.Router(); router = express.Router();
const utils = require('../utils')
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;
}
}
router.post('/', function (req, res) { router.post('/', function (req, res) {
// pass in a name for an account, if the account doesn't exist, create it! const db = req.app.get('db');
let domain = req.app.get('domain'); req.body._target = req.user
const myURL = new URL(req.body.actor); delete req.body['@context']
let targetDomain = myURL.hostname; db.collection('streams').insertOne(req.body)
// TODO: add "Undo" follow event .then(() => res.status(200).send())
if (typeof req.body.object === 'string' && req.body.type === 'Follow') { .catch(err => {
let name = req.body.object.replace(`https://${domain}/u/`,''); console.log(err)
sendAcceptMessage(req.body, name, domain, req, res, targetDomain); res.status(500).send()
// 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);
}
}
}
}); });
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; module.exports = router;

View file

@ -1,60 +1,54 @@
'use strict'; 'use strict';
const express = require('express'), const express = require('express'),
router = express.Router(); 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; let name = req.params.name;
if (!name) { if (!name) {
return res.status(400).send('Bad request.'); return res.status(400).send('Bad request.');
} }
else { else {
let db = req.app.get('db'); let objs = req.app.get('objs');
let domain = req.app.get('domain'); const id = `https://${req.app.get('domain')}/u/${name}`
let username = name; console.log(`looking up '${id}'`)
name = `${name}@${domain}`; const user = await objs.findOne({type: 'Person', id: id}, {fields: {_id: 0}})
let result = db.prepare('select actor from accounts where name = ?').get(name); // .project({_id: 0})
if (result === undefined) { if (user) {
return res.status(404).send(`No record found for ${name}.`); return res.json(toJSONLD(user))
}
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);
} }
return res.status(404).send('Person not found')
} }
}); });
router.get('/:name/followers', function (req, res) { // router.get('/:name/followers', function (req, res) {
let name = req.params.name; // let name = req.params.name;
if (!name) { // if (!name) {
return res.status(400).send('Bad request.'); // return res.status(400).send('Bad request.');
} // }
else { // else {
let db = req.app.get('db'); // let db = req.app.get('db');
let domain = req.app.get('domain'); // let domain = req.app.get('domain');
let result = db.prepare('select followers from accounts where name = ?').get(`${name}@${domain}`); // let result = db.prepare('select followers from accounts where name = ?').get(`${name}@${domain}`);
console.log(result); // console.log(result);
result.followers = result.followers || '[]'; // result.followers = result.followers || '[]';
let followers = JSON.parse(result.followers); // let followers = JSON.parse(result.followers);
let followersCollection = { // let followersCollection = {
"type":"OrderedCollection", // "type":"OrderedCollection",
"totalItems":followers.length, // "totalItems":followers.length,
"id":`https://${domain}/u/${name}/followers`, // "id":`https://${domain}/u/${name}/followers`,
"first": { // "first": {
"type":"OrderedCollectionPage", // "type":"OrderedCollectionPage",
"totalItems":followers.length, // "totalItems":followers.length,
"partOf":`https://${domain}/u/${name}/followers`, // "partOf":`https://${domain}/u/${name}/followers`,
"orderedItems": followers, // "orderedItems": followers,
"id":`https://${domain}/u/${name}/followers?page=1` // "id":`https://${domain}/u/${name}/followers?page=1`
}, // },
"@context":["https://www.w3.org/ns/activitystreams"] // "@context":["https://www.w3.org/ns/activitystreams"]
}; // };
res.json(followersCollection); // res.json(toJSONLD(followersCollection));
} // }
}); // });
module.exports = router; module.exports = router;

View file

@ -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"
}

38
utils/index.js Normal file
View file

@ -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,
}
}