mirror of
https://github.com/dariusk/express-activitypub.git
synced 2024-11-24 00:01:00 +00:00
Merge pull request #2 from dariusk/1-fix-create-object
Fix guids on message object, make IDs dereferencable
This commit is contained in:
commit
f9260b4128
12 changed files with 233 additions and 157 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
node_modules/
|
||||
package-lock.json
|
||||
*.db
|
||||
config.json
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
9
index.js
9
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(){
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
154
routes/api.js
154
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;
|
||||
|
|
113
routes/inbox.js
113
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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
22
routes/message.js
Normal 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;
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue