diff --git a/.gitignore b/.gitignore index 4913e17..8d3e807 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ package-lock.json *.db +config.json diff --git a/README.md b/README.md index 6ed1d15..3dbf993 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.json b/config-template.json similarity index 100% rename from config.json rename to config-template.json diff --git a/index.js b/index.js index 6f6b133..e4b9140 100644 --- a/index.js +++ b/index.js @@ -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(){ diff --git a/package.json b/package.json index 791a1cb..89b33d2 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/routes/admin.js b/routes/admin.js index bec6d5e..0398389 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -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; diff --git a/routes/api.js b/routes/api.js index d7a38f0..d80a474 100644 --- a/routes/api.js +++ b/routes/api.js @@ -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; diff --git a/routes/inbox.js b/routes/inbox.js index e8f39a7..424f1ee 100644 --- a/routes/inbox.js +++ b/routes/inbox.js @@ -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); + } + } } }); diff --git a/routes/index.js b/routes/index.js index 2b97780..afd9141 100644 --- a/routes/index.js +++ b/routes/index.js @@ -4,6 +4,7 @@ module.exports = { api: require('./api'), admin: require('./admin'), user: require('./user'), + message: require('./message'), inbox: require('./inbox'), webfinger: require('./webfinger'), }; diff --git a/routes/message.js b/routes/message.js new file mode 100644 index 0000000..f002277 --- /dev/null +++ b/routes/message.js @@ -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; diff --git a/routes/user.js b/routes/user.js index 062e931..65d627b 100644 --- a/routes/user.js +++ b/routes/user.js @@ -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); } }); diff --git a/routes/webfinger.js b/routes/webfinger.js index 9cc67dd..c743d84 100644 --- a/routes/webfinger.js +++ b/routes/webfinger.js @@ -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)); + } } });