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/
package-lock.json
*.db
config.json

View file

@ -20,6 +20,10 @@ Clone the repository, then `cd` into its root directory. Install dependencies:
`npm i`
Copy `config-template.json` to `config.json`.
`cp config-template.json config.json`
Update your `config.json` file:
```js
@ -45,7 +49,7 @@ Enter "test" in the "Create Account" section and hit the "Create Account" button
## 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

View file

@ -2,8 +2,8 @@ const config = require('./config.json');
const { USER, PASS, DOMAIN, PRIVKEY_PATH, CERT_PATH, PORT } = config;
const express = require('express');
const app = express();
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('bot-node.db');
const Database = require('better-sqlite3');
const db = new Database('bot-node.db');
const fs = require('fs');
const routes = require('./routes'),
bodyParser = require('body-parser'),
@ -27,7 +27,9 @@ try {
}
// 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('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('/.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);
http.createServer(app).listen(app.get('port'), function(){

View file

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

View file

@ -15,6 +15,7 @@ function createActor(name, domain, pubkey) {
'type': 'Person',
'preferredUsername': `${name}`,
'inbox': `https://${domain}/api/inbox`,
'followers': `https://${domain}/u/${name}/followers`,
'publicKey': {
'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 webfingerRecord = createWebfinger(account, domain);
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)', {
$name: `${account}@${domain}`,
$apikey: apikey,
$pubkey: pair.public,
$privkey: pair.private,
$actor: JSON.stringify(actorRecord),
$webfinger: JSON.stringify(webfingerRecord)
}, (err, accounts) => {
try {
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));
res.status(200).json({msg: 'ok', apikey});
});
}
catch(e) {
res.status(200).json({error: e});
}
});
module.exports = router;

View file

@ -11,101 +11,109 @@ router.post('/sendMessage', function (req, res) {
let apikey = req.body.apikey;
let message = req.body.message;
// check to see if your API key matches
db.get('select apikey from accounts where name = $name', {$name: `${acct}@${domain}`}, (err, result) => {
if (result.apikey === apikey) {
sendCreateMessage(message, acct, domain, req, res);
}
else {
res.status(403).json({msg: 'wrong api key'});
}
});
let result = db.prepare('select apikey from accounts where name = ?').get(`${acct}@${domain}`);
if (result.apikey === apikey) {
sendCreateMessage(message, acct, domain, req, res);
}
else {
res.status(403).json({msg: 'wrong api key'});
}
});
function signAndSend(message, name, domain, req, res, targetDomain, inbox) {
// get the private key
let db = req.app.get('db');
let inboxFragment = inbox.replace('https://'+targetDomain,'');
db.get('select privkey from accounts where name = $name', {$name: `${name}@${domain}`}, (err, result) => {
if (result === undefined) {
console.log(`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){
console.log(`Sent message to an inbox at ${targetDomain}!`);
if (error) {
console.log('Error:', error, response);
}
else {
console.log('Response Status Code:', response.statusCode);
}
});
}
});
let result = db.prepare('select privkey from accounts where name = ?').get(`${name}@${domain}`);
if (result === undefined) {
console.log(`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){
console.log(`Sent message to an inbox at ${targetDomain}!`);
if (error) {
console.log('Error:', error, response);
}
else {
console.log('Response Status Code:', response.statusCode);
}
});
}
}
function createMessage(text, name, domain) {
const guid = crypto.randomBytes(16).toString('hex');
function createMessage(text, name, domain, req, res, follower) {
const guidCreate = crypto.randomBytes(16).toString('hex');
const guidNote = crypto.randomBytes(16).toString('hex');
let db = req.app.get('db');
let d = new Date();
return {
let noteMessage = {
'id': `https://${domain}/m/${guidNote}`,
'type': 'Note',
'published': d.toISOString(),
'attributedTo': `https://${domain}/u/${name}`,
'content': text,
'to': ['https://www.w3.org/ns/activitystreams#Public'],
};
let createMessage = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': `https://${domain}/${guid}`,
'id': `https://${domain}/m/${guidCreate}`,
'type': 'Create',
'actor': `https://${domain}/u/${name}`,
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'cc': [follower],
'object': {
'id': `https://${domain}/${guid}`,
'type': 'Note',
'published': d.toISOString(),
'attributedTo': `https://${domain}/u/${name}`,
'content': text,
'to': 'https://www.w3.org/ns/activitystreams#Public'
}
'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) {
let message = createMessage(text, name, domain);
let db = req.app.get('db');
db.get('select followers from accounts where name = $name', {$name: `${name}@${domain}`}, (err, result) => {
let followers = JSON.parse(result.followers);
console.log(followers);
console.log('type',typeof followers);
if (followers === null) {
console.log('aaaa');
res.status(400).json({msg: `No followers for account ${name}@${domain}`});
let result = db.prepare('select followers from accounts where name = ?').get(`${name}@${domain}`);
let followers = JSON.parse(result.followers);
console.log(followers);
console.log('type',typeof followers);
if (followers === null) {
console.log('aaaa');
res.status(400).json({msg: `No followers for account ${name}@${domain}`});
}
else {
for (let follower of followers) {
let inbox = follower+'/inbox';
let myURL = new URL(follower);
let targetDomain = myURL.hostname;
let message = createMessage(text, name, domain, req, res, follower);
signAndSend(message, name, domain, req, res, targetDomain, inbox);
}
else {
for (let follower of followers) {
let inbox = follower+'/inbox';
let myURL = new URL(follower);
let targetDomain = myURL.hostname;
signAndSend(message, name, domain, req, res, targetDomain, inbox);
}
res.status(200).json({msg: 'ok'});
}
});
res.status(200).json({msg: 'ok'});
}
}
module.exports = router;

View file

@ -5,43 +5,45 @@ const express = require('express'),
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');
db.get('select privkey from accounts where name = $name', {$name: `${name}@${domain}`}, (err, result) => {
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 /inbox\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: `https://${targetDomain}/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);
}
});
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) {
@ -76,28 +78,31 @@ router.post('/', function (req, res) {
// 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
db.get('select followers from accounts where name = $name', {$name: `${name}@${domain}`}, (err, result) => {
if (result === undefined) {
console.log(`No record found for ${name}.`);
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 {
// 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);
// update into DB
db.run('update accounts set followers=$followers where name = $name', {$name: `${name}@${domain}`, $followers: followersText}, (err, result) => {
console.log('updated followers!', err, result);
});
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);
}
}
}
});

View file

@ -4,6 +4,7 @@ module.exports = {
api: require('./api'),
admin: require('./admin'),
user: require('./user'),
message: require('./message'),
inbox: require('./inbox'),
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 {
let db = req.app.get('db');
let domain = req.app.get('domain');
let username = name;
name = `${name}@${domain}`;
db.get('select actor from accounts where name = $name', {$name: name}, (err, result) => {
if (result === undefined) {
return res.status(404).send(`No record found for ${name}.`);
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`;
}
else {
res.json(JSON.parse(result.actor));
}
});
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 {
let name = resource.replace('acct:','');
let db = req.app.get('db');
db.get('select webfinger from accounts where name = $name', {$name: name}, (err, result) => {
if (result === undefined) {
return res.status(404).send(`No record found for ${name}.`);
}
else {
res.json(JSON.parse(result.webfinger));
}
});
let result = db.prepare('select webfinger from accounts where name = ?').get(name);
if (result === undefined) {
return res.status(404).send(`No record found for ${name}.`);
}
else {
res.json(JSON.parse(result.webfinger));
}
}
});