Merge pull request #2 from dariusk/1-fix-create-object

Fix guids on message object, make IDs dereferencable
This commit is contained in:
Darius Kazemi 2019-03-26 00:02:09 -07:00 committed by GitHub
commit f9260b4128
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 233 additions and 157 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules/ node_modules/
package-lock.json package-lock.json
*.db *.db
config.json

View file

@ -20,6 +20,10 @@ Clone the repository, then `cd` into its root directory. Install dependencies:
`npm i` `npm i`
Copy `config-template.json` to `config.json`.
`cp config-template.json config.json`
Update your `config.json` file: Update your `config.json` file:
```js ```js
@ -45,7 +49,7 @@ Enter "test" in the "Create Account" section and hit the "Create Account" button
## Local testing ## Local testing
You can use a service like [ngrok](https://ngrok.com/) to test things out before you deploy on a real server. All you need to do is install ngrok and run `ngrok http 3000` (or whatever port you're using if you changed it). Then go to your `config.json` and update the `DOMAIN` field to whatever `abcdef.ngrok.io` domain that ngrok gives you and restart your server. You can use a service like [ngrok](https://ngrok.com/) to test things out before you deploy on a real server. All you need to do is install ngrok and run `ngrok http 3000` (or whatever port you're using if you changed it). Then go to your `config.json` and update the `DOMAIN` field to whatever `abcdef.ngrok.io` domain that ngrok gives you and restart your server. *For local testing you do not need to specify `PRIVKEY_PATH` or `CERT_PARTH`.*
## Admin Page ## Admin Page

View file

@ -2,8 +2,8 @@ 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 sqlite3 = require('sqlite3').verbose(); const Database = require('better-sqlite3');
const db = new sqlite3.Database('bot-node.db'); const db = new Database('bot-node.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,7 +27,9 @@ try {
} }
// if there is no `accounts` table in the DB, create an empty table // if there is no `accounts` table in the DB, create an empty table
db.run('CREATE TABLE IF NOT EXISTS accounts (name TEXT PRIMARY KEY, privkey TEXT, pubkey TEXT, webfinger TEXT, actor TEXT, apikey TEXT, followers TEXT, messages TEXT)'); 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('db', db);
app.set('domain', DOMAIN); app.set('domain', DOMAIN);
@ -65,6 +67,7 @@ app.use('/api/admin', cors({ credentials: true, origin: true }), basicUserAuth,
app.use('/admin', express.static('public/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('/api/inbox', cors(), routes.inbox); app.use('/api/inbox', cors(), routes.inbox);
http.createServer(app).listen(app.get('port'), function(){ http.createServer(app).listen(app.get('port'), function(){

View file

@ -4,13 +4,13 @@
"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",
"generate-rsa-keypair": "^0.1.2", "generate-rsa-keypair": "^0.1.2",
"request": "^2.87.0", "request": "^2.87.0"
"sqlite3": "^4.0.2"
}, },
"engines": { "engines": {
"node": ">=10.10.0" "node": ">=10.10.0"

View file

@ -15,6 +15,7 @@ function createActor(name, domain, pubkey) {
'type': 'Person', 'type': 'Person',
'preferredUsername': `${name}`, 'preferredUsername': `${name}`,
'inbox': `https://${domain}/api/inbox`, 'inbox': `https://${domain}/api/inbox`,
'followers': `https://${domain}/u/${name}/followers`,
'publicKey': { 'publicKey': {
'id': `https://${domain}/u/${name}#main-key`, 'id': `https://${domain}/u/${name}#main-key`,
@ -51,16 +52,13 @@ router.post('/create', function (req, res) {
let actorRecord = createActor(account, domain, pair.public); let actorRecord = createActor(account, domain, pair.public);
let webfingerRecord = createWebfinger(account, domain); let webfingerRecord = createWebfinger(account, domain);
const apikey = crypto.randomBytes(16).toString('hex'); const apikey = crypto.randomBytes(16).toString('hex');
db.run('insert or replace into accounts(name, actor, apikey, pubkey, privkey, webfinger) values($name, $actor, $apikey, $pubkey, $privkey, $webfinger)', { try {
$name: `${account}@${domain}`, db.prepare('insert or replace into accounts(name, actor, apikey, pubkey, privkey, webfinger) values(?, ?, ?, ?, ?, ?)').run(`${account}@${domain}`, JSON.stringify(actorRecord), apikey, pair.public, pair.private, JSON.stringify(webfingerRecord));
$apikey: apikey,
$pubkey: pair.public,
$privkey: pair.private,
$actor: JSON.stringify(actorRecord),
$webfinger: JSON.stringify(webfingerRecord)
}, (err, accounts) => {
res.status(200).json({msg: 'ok', apikey}); res.status(200).json({msg: 'ok', apikey});
}); }
catch(e) {
res.status(200).json({error: e});
}
}); });
module.exports = router; module.exports = router;

View file

@ -11,21 +11,20 @@ router.post('/sendMessage', function (req, res) {
let apikey = req.body.apikey; let apikey = req.body.apikey;
let message = req.body.message; let message = req.body.message;
// check to see if your API key matches // check to see if your API key matches
db.get('select apikey from accounts where name = $name', {$name: `${acct}@${domain}`}, (err, result) => { let result = db.prepare('select apikey from accounts where name = ?').get(`${acct}@${domain}`);
if (result.apikey === apikey) { if (result.apikey === apikey) {
sendCreateMessage(message, acct, domain, req, res); sendCreateMessage(message, acct, domain, req, res);
} }
else { else {
res.status(403).json({msg: 'wrong api key'}); res.status(403).json({msg: 'wrong api key'});
} }
});
}); });
function signAndSend(message, name, domain, req, res, targetDomain, inbox) { function signAndSend(message, name, domain, req, res, targetDomain, inbox) {
// get the private key // get the private key
let db = req.app.get('db'); let db = req.app.get('db');
let inboxFragment = inbox.replace('https://'+targetDomain,''); let inboxFragment = inbox.replace('https://'+targetDomain,'');
db.get('select privkey from accounts where name = $name', {$name: `${name}@${domain}`}, (err, result) => { let result = db.prepare('select privkey from accounts where name = ?').get(`${name}@${domain}`);
if (result === undefined) { if (result === undefined) {
console.log(`No record found for ${name}.`); console.log(`No record found for ${name}.`);
} }
@ -59,36 +58,45 @@ function signAndSend(message, name, domain, req, res, targetDomain, inbox) {
} }
}); });
} }
});
} }
function createMessage(text, name, domain) { function createMessage(text, name, domain, req, res, follower) {
const guid = crypto.randomBytes(16).toString('hex'); const guidCreate = crypto.randomBytes(16).toString('hex');
const guidNote = crypto.randomBytes(16).toString('hex');
let db = req.app.get('db');
let d = new Date(); let d = new Date();
return { let noteMessage = {
'@context': 'https://www.w3.org/ns/activitystreams', 'id': `https://${domain}/m/${guidNote}`,
'id': `https://${domain}/${guid}`,
'type': 'Create',
'actor': `https://${domain}/u/${name}`,
'object': {
'id': `https://${domain}/${guid}`,
'type': 'Note', 'type': 'Note',
'published': d.toISOString(), 'published': d.toISOString(),
'attributedTo': `https://${domain}/u/${name}`, 'attributedTo': `https://${domain}/u/${name}`,
'content': text, 'content': text,
'to': 'https://www.w3.org/ns/activitystreams#Public' 'to': ['https://www.w3.org/ns/activitystreams#Public'],
}
}; };
let createMessage = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${domain}/m/${guidCreate}`,
'type': 'Create',
'actor': `https://${domain}/u/${name}`,
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'cc': [follower],
'object': noteMessage
};
db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidCreate, JSON.stringify(createMessage));
db.prepare('insert or replace into messages(guid, message) values(?, ?)').run( guidNote, JSON.stringify(noteMessage));
return createMessage;
} }
function sendCreateMessage(text, name, domain, req, res) { function sendCreateMessage(text, name, domain, req, res) {
let message = createMessage(text, name, domain);
let db = req.app.get('db'); let db = req.app.get('db');
db.get('select followers from accounts where name = $name', {$name: `${name}@${domain}`}, (err, result) => { let result = db.prepare('select followers from accounts where name = ?').get(`${name}@${domain}`);
let followers = JSON.parse(result.followers); let followers = JSON.parse(result.followers);
console.log(followers); console.log(followers);
console.log('type',typeof followers); console.log('type',typeof followers);
@ -101,11 +109,11 @@ function sendCreateMessage(text, name, domain, req, res) {
let inbox = follower+'/inbox'; let inbox = follower+'/inbox';
let myURL = new URL(follower); let myURL = new URL(follower);
let targetDomain = myURL.hostname; let targetDomain = myURL.hostname;
let message = createMessage(text, name, domain, req, res, follower);
signAndSend(message, name, domain, req, res, targetDomain, inbox); signAndSend(message, name, domain, req, res, targetDomain, inbox);
} }
res.status(200).json({msg: 'ok'}); res.status(200).json({msg: 'ok'});
} }
});
} }
module.exports = router; module.exports = router;

View file

@ -5,9 +5,12 @@ const express = require('express'),
router = express.Router(); router = express.Router();
function signAndSend(message, name, domain, req, res, targetDomain) { 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 // get the private key
let db = req.app.get('db'); let db = req.app.get('db');
db.get('select privkey from accounts where name = $name', {$name: `${name}@${domain}`}, (err, result) => { let result = db.prepare('select privkey from accounts where name = ?').get(`${name}@${domain}`);
if (result === undefined) { if (result === undefined) {
return res.status(404).send(`No record found for ${name}.`); return res.status(404).send(`No record found for ${name}.`);
} }
@ -15,14 +18,14 @@ function signAndSend(message, name, domain, req, res, targetDomain) {
let privkey = result.privkey; let privkey = result.privkey;
const signer = crypto.createSign('sha256'); const signer = crypto.createSign('sha256');
let d = new Date(); let d = new Date();
let stringToSign = `(request-target): post /inbox\nhost: ${targetDomain}\ndate: ${d.toUTCString()}`; let stringToSign = `(request-target): post ${inboxFragment}\nhost: ${targetDomain}\ndate: ${d.toUTCString()}`;
signer.update(stringToSign); signer.update(stringToSign);
signer.end(); signer.end();
const signature = signer.sign(privkey); const signature = signer.sign(privkey);
const signature_b64 = signature.toString('base64'); const signature_b64 = signature.toString('base64');
let header = `keyId="https://${domain}/u/${name}",headers="(request-target) host date",signature="${signature_b64}"`; let header = `keyId="https://${domain}/u/${name}",headers="(request-target) host date",signature="${signature_b64}"`;
request({ request({
url: `https://${targetDomain}/inbox`, url: inbox,
headers: { headers: {
'Host': targetDomain, 'Host': targetDomain,
'Date': d.toUTCString(), 'Date': d.toUTCString(),
@ -41,7 +44,6 @@ function signAndSend(message, name, domain, req, res, targetDomain) {
}); });
return res.status(200); return res.status(200);
} }
});
} }
function sendAcceptMessage(thebody, name, domain, req, res, targetDomain) { function sendAcceptMessage(thebody, name, domain, req, res, targetDomain) {
@ -76,7 +78,7 @@ router.post('/', function (req, res) {
// Add the user to the DB of accounts that follow the account // Add the user to the DB of accounts that follow the account
let db = req.app.get('db'); let db = req.app.get('db');
// get the followers JSON for the user // get the followers JSON for the user
db.get('select followers from accounts where name = $name', {$name: `${name}@${domain}`}, (err, result) => { let result = db.prepare('select followers from accounts where name = ?').get(`${name}@${domain}`);
if (result === undefined) { if (result === undefined) {
console.log(`No record found for ${name}.`); console.log(`No record found for ${name}.`);
} }
@ -92,12 +94,15 @@ router.post('/', function (req, res) {
followers = [req.body.actor]; followers = [req.body.actor];
} }
let followersText = JSON.stringify(followers); let followersText = JSON.stringify(followers);
try {
// update into DB // update into DB
db.run('update accounts set followers=$followers where name = $name', {$name: `${name}@${domain}`, $followers: followersText}, (err, result) => { let newFollowers = db.prepare('update accounts set followers=? where name = ?').run(followersText, `${name}@${domain}`);
console.log('updated followers!', err, result); console.log('updated followers!', newFollowers);
}); }
catch(e) {
console.log('error', e);
}
} }
});
} }
}); });

View file

@ -4,6 +4,7 @@ module.exports = {
api: require('./api'), api: require('./api'),
admin: require('./admin'), admin: require('./admin'),
user: require('./user'), user: require('./user'),
message: require('./message'),
inbox: require('./inbox'), inbox: require('./inbox'),
webfinger: require('./webfinger'), webfinger: require('./webfinger'),
}; };

22
routes/message.js Normal file
View file

@ -0,0 +1,22 @@
'use strict';
const express = require('express'),
router = express.Router();
router.get('/:guid', function (req, res) {
let guid = req.params.guid;
if (!guid) {
return res.status(400).send('Bad request.');
}
else {
let db = req.app.get('db');
let result = db.prepare('select message from messages where guid = ?').get(guid);
if (result === undefined) {
return res.status(404).send(`No record found for ${guid}.`);
}
else {
res.json(JSON.parse(result.message));
}
}
});
module.exports = router;

View file

@ -10,15 +10,50 @@ router.get('/:name', function (req, res) {
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 username = name;
name = `${name}@${domain}`; name = `${name}@${domain}`;
db.get('select actor from accounts where name = $name', {$name: name}, (err, result) => { let result = db.prepare('select actor from accounts where name = ?').get(name);
if (result === undefined) { if (result === undefined) {
return res.status(404).send(`No record found for ${name}.`); return res.status(404).send(`No record found for ${name}.`);
} }
else { else {
res.json(JSON.parse(result.actor)); 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);
}
}
});
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);
} }
}); });

View file

@ -10,14 +10,13 @@ router.get('/', function (req, res) {
else { else {
let name = resource.replace('acct:',''); let name = resource.replace('acct:','');
let db = req.app.get('db'); let db = req.app.get('db');
db.get('select webfinger from accounts where name = $name', {$name: name}, (err, result) => { let result = db.prepare('select webfinger from accounts where name = ?').get(name);
if (result === undefined) { if (result === undefined) {
return res.status(404).send(`No record found for ${name}.`); return res.status(404).send(`No record found for ${name}.`);
} }
else { else {
res.json(JSON.parse(result.webfinger)); res.json(JSON.parse(result.webfinger));
} }
});
} }
}); });