Skip to content

Commit

Permalink
feat: activitypub actor endpoint for user accounts, WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
julianlam committed May 17, 2023
1 parent c90ca5f commit 5531414
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 1 deletion.
42 changes: 42 additions & 0 deletions src/activitypub.js
@@ -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 changes: 41 additions & 0 deletions src/controllers/activitypub.js
@@ -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,
},
});
};
1 change: 1 addition & 0 deletions src/controllers/index.js
Expand Up @@ -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');
Expand Down
6 changes: 5 additions & 1 deletion src/controllers/well-known.js
Expand Up @@ -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);
}
Expand All @@ -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
},
],
};

Expand Down
Empty file added src/messaging/uploads.js
Empty file.
24 changes: 24 additions & 0 deletions src/middleware/index.js
Expand Up @@ -278,3 +278,27 @@ middleware.checkRequired = function (fields, req, res, next) {

controllers.helpers.formatApiResponse(400, res, new Error(`[[error:required-parameters-missing, ${missing.join(' ')}]]`));
};

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 changes: 7 additions & 0 deletions src/routes/activitypub.js
@@ -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);
};
2 changes: 2 additions & 0 deletions src/routes/index.js
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 5531414

Please sign in to comment.