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 { 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,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!'));
// 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'));
// 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'));
});
@ -80,3 +110,7 @@ if (sslOptions) {
console.log('Express server listening on port ' + app.get('port-https'));
});
}
})
.catch(err => {
throw new Error(err)
});

View file

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

View file

@ -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;

View file

@ -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;

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