diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index fb0504e386..1c8e1536a9 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -5,6 +5,7 @@ const winston = require('winston'); const nconf = require('nconf'); const validator = require('validator'); +const posts = require('../posts'); const request = require('../request'); const db = require('../database'); const ttl = require('../cache/ttl'); @@ -99,28 +100,34 @@ Helpers.generateKeys = async (type, id) => { Helpers.resolveLocalId = async (input) => { if (Helpers.isUri(input)) { - const { host, pathname } = new URL(input); + const { host, pathname, hash } = new URL(input); if (host === nconf.get('url_parsed').host) { const [prefix, value] = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean); + let activityData = {}; + if (hash.startsWith('#activity')) { + const [, activity, data] = hash.split('/', 3); + activityData = { activity, data }; + } + switch (prefix) { case 'uid': - return { type: 'user', id: value }; + return { type: 'user', id: value, ...activityData }; case 'post': - return { type: 'post', id: value }; + return { type: 'post', id: value, ...activityData }; case 'category': - return { type: 'category', id: value }; + return { type: 'category', id: value, ...activityData }; case 'user': { const uid = await user.getUidByUserslug(value); - return { type: 'user', id: uid }; + return { type: 'user', id: uid, ...activityData }; } } - return { type: null, id: null }; + return { type: null, id: null, ...activityData }; } return { type: null, id: null }; @@ -132,3 +139,75 @@ Helpers.resolveLocalId = async (input) => { return { type: null, id: null }; }; + +Helpers.resolveActor = (type, id) => { + switch (type) { + case 'user': + case 'uid': { + return `${nconf.get('url')}${id > 0 ? `/uid/${id}` : '/actor'}`; + } + + case 'category': + case 'cid': { + return `${nconf.get('url')}/category/${id}`; + } + + default: + throw new Error('[[error:activitypub.invalid-id]]'); + } +}; + +Helpers.resolveActivity = async (activity, data, id, resolved) => { + switch (activity.toLowerCase()) { + case 'follow': { + const actor = await Helpers.resolveActor(resolved.type, resolved.id); + const { actorUri: targetUri } = await Helpers.query(data); + return { + '@context': 'https://www.w3.org/ns/activitystreams', + actor, + id, + type: 'Follow', + object: targetUri, + }; + } + default: { + throw new Error('[[error:activitypub.not-implemented]]'); + } + } +}; + + +Helpers.resolveObjects = async (ids) => { + if (!Array.isArray(ids)) { + ids = [ids]; + } + const objects = await Promise.all(ids.map(async (id) => { + const { type, id: resolvedId, activity, data: activityData } = await Helpers.resolveLocalId(id); + if (activity) { + return Helpers.resolveActivity(activity, activityData, id, { type, id: resolvedId }); + } + switch (type) { + case 'user': { + return activitypub.mocks.actors.user(resolvedId); + } + case 'post': { + const post = (await posts.getPostSummaryByPids( + [resolvedId], + activitypub._constants.uid, + { stripTags: false } + )).pop(); + if (!post) { + return; + } + return activitypub.mocks.note(post); + } + case 'category': { + return activitypub.mocks.category(resolvedId); + } + default: { + return activitypub.get('uid', 0, id); + } + } + })); + return objects.length === 1 ? objects[0] : objects; +}; diff --git a/src/middleware/activitypub.js b/src/middleware/activitypub.js index bcb6dd9b1d..b4a1b548fc 100644 --- a/src/middleware/activitypub.js +++ b/src/middleware/activitypub.js @@ -55,8 +55,8 @@ middleware.validate = async function (req, res, next) { const actorHostname = new URL(actor).hostname; const objectHostname = new URL(object.id).hostname; if (actorHostname !== objectHostname) { - winston.verbose('[middleware/activitypub] Origin check failed.'); - return res.sendStatus(403); + winston.verbose('[middleware/activitypub] Origin check failed, stripping object down to id.'); + req.body.object = [object.id]; } winston.verbose('[middleware/activitypub] Origin check passed.'); } @@ -75,6 +75,21 @@ middleware.validate = async function (req, res, next) { next(); }; +middleware.resolveObjects = async function (req, res, next) { + const { object } = req.body; + if (typeof object === 'string' || (Array.isArray(object) && object.every(o => typeof o === 'string'))) { + winston.verbose('[middleware/activitypub] Resolving object(s)...'); + try { + req.body.object = await activitypub.helpers.resolveObjects(object); + winston.verbose('[middleware/activitypub] Object(s) successfully resolved.'); + } catch (e) { + winston.verbose('[middleware/activitypub] Failed to resolve object(s).'); + return res.sendStatus(400); + } + } + next(); +}; + middleware.configureResponse = async function (req, res, next) { res.header('Content-Type', 'application/activity+json'); next(); diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index 1431c37349..bcdf631e04 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -17,13 +17,18 @@ module.exports = function (app, middleware, controllers) { middleware.activitypub.configureResponse, ]; + const inboxMiddlewares = [ + middleware.activitypub.validate, + middleware.activitypub.resolveObjects, + ]; + app.get('/actor', middlewares, controllers.activitypub.actors.application); - app.post('/inbox', [...middlewares, middleware.activitypub.validate], controllers.activitypub.postInbox); + app.post('/inbox', [...middlewares, ...inboxMiddlewares], controllers.activitypub.postInbox); app.get('/uid/:uid', [...middlewares, middleware.assert.user], controllers.activitypub.actors.user); app.get('/user/:userslug', [...middlewares, middleware.exposeUid, middleware.assert.user], controllers.activitypub.actors.userBySlug); app.get('/uid/:uid/inbox', [...middlewares, middleware.assert.user], controllers.activitypub.getInbox); - app.post('/uid/:uid/inbox', [...middlewares, middleware.assert.user, middleware.activitypub.validate], controllers.activitypub.postInbox); + app.post('/uid/:uid/inbox', [...middlewares, middleware.assert.user, ...inboxMiddlewares], controllers.activitypub.postInbox); app.get('/uid/:uid/outbox', [...middlewares, middleware.assert.user], controllers.activitypub.getOutbox); app.post('/uid/:uid/outbox', [...middlewares, middleware.assert.user], controllers.activitypub.postOutbox); app.get('/uid/:uid/following', [...middlewares, middleware.assert.user], controllers.activitypub.getFollowing); @@ -35,7 +40,7 @@ module.exports = function (app, middleware, controllers) { app.get('/category/:cid/:slug?', [...middlewares, middleware.assert.category], controllers.activitypub.actors.category); app.get('/category/:cid/inbox', [...middlewares, middleware.assert.category], controllers.activitypub.getInbox); - app.post('/category/:cid/inbox', [...middlewares, middleware.assert.category, middleware.activitypub.validate], controllers.activitypub.postInbox); + app.post('/category/:cid/inbox', [...inboxMiddlewares, middleware.assert.category, ...inboxMiddlewares], controllers.activitypub.postInbox); app.get('/category/:cid/outbox', [...middlewares, middleware.assert.category], controllers.activitypub.getCategoryOutbox); app.post('/category/:cid/outbox', [...middlewares, middleware.assert.category], controllers.activitypub.postOutbox); };