mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 19:15:58 +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['well-known'] = require('./well-known'); | ||||
| Controllers.activitypub = require('./activitypub'); | ||||
| Controllers.home = require('./home'); | ||||
| Controllers.topics = require('./topics'); | ||||
| Controllers.posts = require('./posts'); | ||||
|   | ||||
| @@ -16,7 +16,6 @@ Controller.webfinger = async (req, res) => { | ||||
| 	} | ||||
|  | ||||
| 	const canView = await privileges.global.can('view:users', req.uid); | ||||
| 	console.log('canView', canView, req.uid); | ||||
| 	if (!canView) { | ||||
| 		return res.sendStatus(403); | ||||
| 	} | ||||
| @@ -41,6 +40,11 @@ Controller.webfinger = async (req, res) => { | ||||
| 				type: 'text/html', | ||||
| 				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); | ||||
| }; | ||||
|  | ||||
| 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'), | ||||
| 	feed: require('./feeds'), | ||||
| 	'well-known': require('./well-known'), | ||||
| 	activitypub: require('./activitypub'), | ||||
| }; | ||||
|  | ||||
| _mounts.main = (app, middleware, controllers) => { | ||||
| @@ -155,6 +156,7 @@ function addCoreRoutes(app, router, middleware, mounts) { | ||||
| 	_mounts.api(router, middleware, controllers); | ||||
| 	_mounts.feed(router, middleware, controllers); | ||||
|  | ||||
| 	_mounts.activitypub(router, middleware, controllers); | ||||
| 	_mounts.main(router, middleware, controllers); | ||||
| 	_mounts.mod(router, middleware, controllers); | ||||
| 	_mounts.globalMod(router, middleware, controllers); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user