diff --git a/db/setup.js b/db/setup.js index 4ad9d81..eda76ac 100644 --- a/db/setup.js +++ b/db/setup.js @@ -2,14 +2,20 @@ const utils = require('../utils') const crypto = require('crypto') module.exports = async function dbSetup (db, domain) { + // inbox await db.collection('streams').createIndex({ _target: 1, _id: -1, }) + // outbox await db.collection('streams').createIndex({ actor: 1, _id: -1, }) + // object lookup + await db.collection('objects').createIndex({ + id: 1 + }) const dummyUser = await utils.createLocalActor('dummy', 'Person') await db.collection('objects').findOneAndReplace( {preferredUsername: 'dummy'}, diff --git a/routes/user.js b/routes/user.js index 6f6b9e8..11e9ba2 100644 --- a/routes/user.js +++ b/routes/user.js @@ -10,15 +10,8 @@ router.get('/:name', async function (req, res) { return res.status(400).send('Bad request.'); } else { - let objs = req.app.get('objs'); let db = req.app.get('db') - const id = utils.userNameToIRI(name) - console.log(`looking up '${id}'`) - const user = await db.collection('objects') - .find({type: 'Person', id: id}) - .limit(1) - .project({_id: 0, _meta: 0}) - .next() + const user = await utils.getOrCreateActor(name, db) if (user) { return res.json(toJSONLD(user)) } @@ -26,33 +19,28 @@ router.get('/:name', async function (req, res) { } }); -// 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)); -// } -// }); +router.get('/:name/followers', function (req, res) { + let name = req.params.name; + if (!name) { + return res.status(400).send('Bad request.'); + } + const db = req.app.get('db') + db.collection('streams') + .find({ + type: 'Follow', + _target: name, + 'object.id': utils.usernameToIRI(name) + }) + .project({_id: 0, actor: 1}) + .toArray() + .then(follows => { + const followers = follows.map(utils.actorFromActivity) + return res.json(utils.arrayToCollection(followers)) + }) + .catch(err => { + console.log(err) + return res.status(500).send() + }) +}); module.exports = router; diff --git a/routes/webfinger.js b/routes/webfinger.js index df88066..977cd57 100644 --- a/routes/webfinger.js +++ b/routes/webfinger.js @@ -13,12 +13,7 @@ router.get('/', function (req, res) { return res.status(400).send('Requested user is not from this domain') } let db = req.app.get('db'); - const userId = utils.userNameToIRI(acct[1]); - db.collection('objects') - .find({id: userId}) - .limit(1) - .project({_id: 0}) - .next() + utils.getOrCreateActor(acct[1], db) .then(result => { if (!result) { return res.status(404).send(`${acct[1]}@${acct[2]} not found`) @@ -29,7 +24,7 @@ router.get('/', function (req, res) { { 'rel': 'self', 'type': 'application/activity+json', - 'href': userId + 'href': result.id } ] } diff --git a/utils/index.js b/utils/index.js index 9554752..78041b5 100644 --- a/utils/index.js +++ b/utils/index.js @@ -1,9 +1,10 @@ const crypto = require('crypto') const {promisify} = require('util') const {ASContext} = require('./consts') -module.exports.validators = require('./validators'); const config = require('../config.json') +module.exports.validators = require('./validators'); + function isObject(value) { return value && typeof value === 'object' && value.constructor === Object } @@ -34,13 +35,13 @@ module.exports.arrayToCollection = function (arr, ordered) { } } -function userNameToIRI (user) { +function usernameToIRI (user) { return `https://${config.DOMAIN}/u/${user}` } -module.exports.userNameToIRI = userNameToIRI +module.exports.usernameToIRI = usernameToIRI const generateKeyPairPromise = promisify(crypto.generateKeyPair) -module.exports.createLocalActor = function (name, type) { +function createLocalActor (name, type) { return generateKeyPairPromise('rsa', { modulusLength: 4096, publicKeyEncoding: { @@ -54,7 +55,7 @@ module.exports.createLocalActor = function (name, type) { passphrase: config.KEYPASS } }).then(pair => { - const actorBase = userNameToIRI(name); + const actorBase = usernameToIRI(name); return { _meta: { privateKey: pair.privateKey, @@ -81,3 +82,35 @@ module.exports.createLocalActor = function (name, type) { } }) } +module.exports.createLocalActor = createLocalActor + +async function getOrCreateActor(preferredUsername, db) { + const id = usernameToIRI(preferredUsername) + let user = await db.collection('objects') + .find({id: id}) + .limit(1) + .project({_id: 0, _meta: 0}) + .next() + if (user) { + return user + } + // auto create groups whenever an unknown actor is referenced + user = await createLocalActor(preferredUsername, 'Group') + await db.collection('objects').insertOne(user) + // only executed on success + delete user._meta + delete user._id + return user +} +module.exports.getOrCreateActor = getOrCreateActor + +function actorFromActivity (activity) { + if (Object.prototype.toString.call(activity.actor) === '[object String]') { + return activity.actor + } + if (activity.actor.type === 'Link') { + return activity.actor.href + } + return activity.actor.id +} +module.exports.actorFromActivity = actorFromActivity \ No newline at end of file diff --git a/utils/validators.js b/utils/validators.js index be3670e..30fb976 100644 --- a/utils/validators.js +++ b/utils/validators.js @@ -28,7 +28,7 @@ module.exports.outboxActivity = function outboxActivity (req, res, next) { if (!validateObject(req.body)) { return res.status(400).send('Invalid activity') } - const newID = ObjectId() + const newID = new ObjectId() req.body = { _id: newID, '@context': ASContext,