mongo for user object requests and initial inbox functionality
This commit is contained in:
parent
b11f50d395
commit
cb137ffcb1
7 changed files with 202 additions and 167 deletions
30
db/setup.js
Normal file
30
db/setup.js
Normal 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,
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
64
index.js
64
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(){
|
||||
// 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) {
|
||||
});
|
||||
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)
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
127
routes/inbox.js
127
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
14
test/requests/activity_inbox_post.http-request
Normal file
14
test/requests/activity_inbox_post.http-request
Normal 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
38
utils/index.js
Normal 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,
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue