mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-01 11:35:55 +01:00
feat: activitypub actor endpoint for user accounts
This commit is contained in:
42
src/activitypub.js
Normal file
42
src/activitypub.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { generateKeyPairSync } = require('crypto');
|
||||||
|
|
||||||
|
const winston = require('winston');
|
||||||
|
|
||||||
|
const db = require('./database');
|
||||||
|
|
||||||
|
const ActivityPub = module.exports;
|
||||||
|
|
||||||
|
ActivityPub.getPublicKey = async (uid) => {
|
||||||
|
let publicKey;
|
||||||
|
|
||||||
|
try {
|
||||||
|
({ publicKey } = await db.getObject(`uid:${uid}:keys`));
|
||||||
|
} catch (e) {
|
||||||
|
({ publicKey } = await generateKeys(uid));
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function generateKeys(uid) {
|
||||||
|
winston.info(`[activitypub] Generating RSA key-pair for uid ${uid}`);
|
||||||
|
const {
|
||||||
|
publicKey,
|
||||||
|
privateKey,
|
||||||
|
} = generateKeyPairSync('rsa', {
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'spki',
|
||||||
|
format: 'pem',
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'pkcs8',
|
||||||
|
format: 'pem',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.setObject(`uid:${uid}:keys`, { publicKey, privateKey });
|
||||||
|
return { publicKey, privateKey };
|
||||||
|
}
|
||||||
41
src/controllers/activitypub.js
Normal file
41
src/controllers/activitypub.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const nconf = require('nconf');
|
||||||
|
|
||||||
|
const user = require('../user');
|
||||||
|
const activitypub = require('../activitypub');
|
||||||
|
|
||||||
|
const Controller = module.exports;
|
||||||
|
|
||||||
|
Controller.getActor = async (req, res) => {
|
||||||
|
// todo: view:users priv gate
|
||||||
|
const { userslug } = req.params;
|
||||||
|
const { uid } = res.locals;
|
||||||
|
const { username, aboutme, picture, 'cover:url': cover } = await user.getUserData(uid);
|
||||||
|
const publicKey = await activitypub.getPublicKey(uid);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
],
|
||||||
|
id: `${nconf.get('url')}/user/${userslug}`,
|
||||||
|
url: `${nconf.get('url')}/user/${userslug}`,
|
||||||
|
followers: `${nconf.get('url')}/user/${userslug}/followers`,
|
||||||
|
following: `${nconf.get('url')}/user/${userslug}/following`,
|
||||||
|
inbox: `${nconf.get('url')}/user/${userslug}/inbox`,
|
||||||
|
outbox: `${nconf.get('url')}/user/${userslug}/outbox`,
|
||||||
|
|
||||||
|
type: 'Person',
|
||||||
|
preferredUsername: username,
|
||||||
|
summary: aboutme,
|
||||||
|
icon: picture ? `${nconf.get('url')}${picture}` : null,
|
||||||
|
image: cover ? `${nconf.get('url')}${cover}` : null,
|
||||||
|
|
||||||
|
publicKey: {
|
||||||
|
id: `${nconf.get('url')}/user/${userslug}`,
|
||||||
|
owner: `${nconf.get('url')}/user/${userslug}#key`,
|
||||||
|
publicKeyPem: publicKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ const Controllers = module.exports;
|
|||||||
|
|
||||||
Controllers.ping = require('./ping');
|
Controllers.ping = require('./ping');
|
||||||
Controllers['well-known'] = require('./well-known');
|
Controllers['well-known'] = require('./well-known');
|
||||||
|
Controllers.activitypub = require('./activitypub');
|
||||||
Controllers.home = require('./home');
|
Controllers.home = require('./home');
|
||||||
Controllers.topics = require('./topics');
|
Controllers.topics = require('./topics');
|
||||||
Controllers.posts = require('./posts');
|
Controllers.posts = require('./posts');
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ Controller.webfinger = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canView = await privileges.global.can('view:users', req.uid);
|
const canView = await privileges.global.can('view:users', req.uid);
|
||||||
console.log('canView', canView, req.uid);
|
|
||||||
if (!canView) {
|
if (!canView) {
|
||||||
return res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
}
|
}
|
||||||
@@ -41,6 +40,11 @@ Controller.webfinger = async (req, res) => {
|
|||||||
type: 'text/html',
|
type: 'text/html',
|
||||||
href: `${nconf.get('url')}/user/${slug}`,
|
href: `${nconf.get('url')}/user/${slug}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rel: 'self',
|
||||||
|
type: 'application/activity+json',
|
||||||
|
href: `${nconf.get('url')}/user/${slug}`, // actor
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
0
src/messaging/uploads.js
Normal file
0
src/messaging/uploads.js
Normal file
@@ -297,3 +297,27 @@ middleware.handleMultipart = (req, res, next) => {
|
|||||||
|
|
||||||
multipartMiddleware(req, res, next);
|
multipartMiddleware(req, res, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
middleware.proceedOnActivityPub = (req, res, next) => {
|
||||||
|
// For whatever reason, express accepts does not recognize "profile" as a valid differentiator
|
||||||
|
// Therefore, manual header parsing is used here.
|
||||||
|
const { accept } = req.headers;
|
||||||
|
if (!accept) {
|
||||||
|
return next('route');
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptable = [
|
||||||
|
'application/activity+json',
|
||||||
|
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
|
];
|
||||||
|
const pass = accept.split(',').some((value) => {
|
||||||
|
const parts = value.split(';').map(v => v.trim());
|
||||||
|
return acceptable.includes(value || parts[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pass) {
|
||||||
|
return next('route');
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|||||||
7
src/routes/activitypub.js
Normal file
7
src/routes/activitypub.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (app, middleware, controllers) {
|
||||||
|
const middlewares = [middleware.proceedOnActivityPub, middleware.exposeUid];
|
||||||
|
|
||||||
|
app.get('/user/:userslug', middlewares, controllers.activitypub.getActor);
|
||||||
|
};
|
||||||
@@ -23,6 +23,7 @@ const _mounts = {
|
|||||||
admin: require('./admin'),
|
admin: require('./admin'),
|
||||||
feed: require('./feeds'),
|
feed: require('./feeds'),
|
||||||
'well-known': require('./well-known'),
|
'well-known': require('./well-known'),
|
||||||
|
activitypub: require('./activitypub'),
|
||||||
};
|
};
|
||||||
|
|
||||||
_mounts.main = (app, middleware, controllers) => {
|
_mounts.main = (app, middleware, controllers) => {
|
||||||
@@ -155,6 +156,7 @@ function addCoreRoutes(app, router, middleware, mounts) {
|
|||||||
_mounts.api(router, middleware, controllers);
|
_mounts.api(router, middleware, controllers);
|
||||||
_mounts.feed(router, middleware, controllers);
|
_mounts.feed(router, middleware, controllers);
|
||||||
|
|
||||||
|
_mounts.activitypub(router, middleware, controllers);
|
||||||
_mounts.main(router, middleware, controllers);
|
_mounts.main(router, middleware, controllers);
|
||||||
_mounts.mod(router, middleware, controllers);
|
_mounts.mod(router, middleware, controllers);
|
||||||
_mounts.globalMod(router, middleware, controllers);
|
_mounts.globalMod(router, middleware, controllers);
|
||||||
|
|||||||
Reference in New Issue
Block a user