diff --git a/src/activitypub/contexts.js b/src/activitypub/contexts.js index 12dcae1059..3f32654e68 100644 --- a/src/activitypub/contexts.js +++ b/src/activitypub/contexts.js @@ -12,6 +12,7 @@ const acceptableTypes = ['Collection', 'CollectionPage', 'OrderedCollection', 'O Contexts.get = async (uid, id) => { let context; let type; + let collection; // Generate digest for If-None-Match if locally cached const tid = await posts.getPostField(id, 'tid'); @@ -27,12 +28,15 @@ Contexts.get = async (uid, id) => { } try { - ({ context } = await activitypub.get('uid', uid, id, { headers })); - if (!context) { + ({ id, type, context, posts: collection } = await activitypub.get('uid', uid, id, { headers })); + if (type === 'Conversation' && collection) { + activitypub.helpers.log(`[activitypub/context] ${id} is the context.`); + return { context: id, collection }; + } else if (!context) { activitypub.helpers.log(`[activitypub/context] ${id} contains no context.`); return false; } - ({ type } = await activitypub.get('uid', uid, context)); + ({ type, posts: collection } = await activitypub.get('uid', uid, context)); } catch (e) { if (e.code === 'ap_get_304') { activitypub.helpers.log(`[activitypub/context] ${id} context unchanged.`); @@ -43,8 +47,8 @@ Contexts.get = async (uid, id) => { return false; } - if (acceptableTypes.includes(type)) { - return { context }; + if (type === 'Conversation') { + return { context, collection }; } return false; @@ -98,9 +102,13 @@ Contexts.getItems = async (uid, id, options) => { const inputId = activitypub.helpers.isUri(options.input) ? options.input : options.input.id; const inCollection = Array.from(chain).map(p => p.pid).includes(inputId); if (!inCollection) { - chain.add(activitypub.helpers.isUri(options.input) ? + const item = activitypub.helpers.isUri(options.input) ? await parseString(uid, options.input) : - await parseItem(uid, options.input)); + await parseItem(uid, options.input); + + if (item) { + chain.add(item); + } } return chain; diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 03cca7d9d8..0e9df7bcb3 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -49,7 +49,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { const { tid } = context; return { tid, count: 0 }; } else if (context.context) { - chain = Array.from(await activitypub.contexts.getItems(uid, context.context, { input })); + chain = Array.from(await activitypub.contexts.getItems(uid, context.collection, { input })); if (chain && chain.length) { // Context resolves, use in later topic creation context = context.context; diff --git a/src/controllers/activitypub/actors.js b/src/controllers/activitypub/actors.js index c80f6e2867..7b05938c80 100644 --- a/src/controllers/activitypub/actors.js +++ b/src/controllers/activitypub/actors.js @@ -115,12 +115,52 @@ Actors.replies = async function (req, res) { Actors.topic = async function (req, res, next) { const allowed = await privileges.topics.can('topics:read', req.params.tid, activitypub._constants.uid); if (!allowed) { - return res.sendStatus(404); + return next(); + } + + const { cid, titleRaw: name, mainPid, slug } = await topics.getTopicFields(req.params.tid, ['cid', 'title', 'mainPid', 'slug']); + let pids = await db.getSortedSetMembers(`tid:${req.params.tid}:posts`); + pids.push(mainPid); + pids = pids.map(pid => (utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid)); + + // Generate digest for ETag + const digest = activitypub.helpers.generateDigest(new Set(pids)); + const ifNoneMatch = (req.get('If-None-Match') || '').split(',').map((tag) => { + tag = tag.trim(); + if (tag.startsWith('"') && tag.endsWith('"')) { + return tag.slice(1, tag.length - 1); + } + + return tag; + }); + if (ifNoneMatch.includes(digest)) { + return res.sendStatus(304); + } + res.set('ETag', digest); + + const object = { + '@context': 'https://www.w3.org/ns/activitystreams', + id: `${nconf.get('url')}/topic/${req.params.tid}`, + url: `${nconf.get('url')}/topic/${slug}`, + type: 'Conversation', + name, + attributedTo: `${nconf.get('url')}/category/${cid}`, + // audience: cid !== -1 ? `${nconf.get('url')}/category/${cid}` : undefined, + posts: `${nconf.get('url')}/topic/${req.params.tid}/posts`, + }; + + res.status(200).json(object); +}; + +Actors.topicPosts = async function (req, res, next) { + const allowed = await privileges.topics.can('topics:read', req.params.tid, activitypub._constants.uid); + if (!allowed) { + return next(); } const page = parseInt(req.query.page, 10) || undefined; const perPage = meta.config.postsPerPage; - const { cid, titleRaw: name, mainPid, slug } = await topics.getTopicFields(req.params.tid, ['cid', 'title', 'mainPid', 'slug']); + const mainPid = await topics.getTopicField(req.params.tid, 'mainPid'); try { let [collection, pids] = await Promise.all([ activitypub.helpers.generateCollection({ @@ -136,21 +176,6 @@ Actors.topic = async function (req, res, next) { pids = pids.map(pid => (utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid)); collection.totalItems += 1; // account for mainPid - // Generate digest for ETag - const digest = activitypub.helpers.generateDigest(new Set(pids)); - const ifNoneMatch = (req.get('If-None-Match') || '').split(',').map((tag) => { - tag = tag.trim(); - if (tag.startsWith('"') && tag.endsWith('"')) { - return tag.slice(1, tag.length - 1); - } - - return tag; - }); - if (ifNoneMatch.includes(digest)) { - return res.sendStatus(304); - } - res.set('ETag', digest); - // Convert pids to urls if (page || collection.totalItems < meta.config.postsPerPage) { collection.orderedItems = collection.orderedItems || []; @@ -162,11 +187,7 @@ Actors.topic = async function (req, res, next) { const object = { '@context': 'https://www.w3.org/ns/activitystreams', - id: `${nconf.get('url')}/topic/${req.params.tid}${collection.orderedItems && page ? `?page=${page}` : ''}`, - url: `${nconf.get('url')}/topic/${slug}`, - name, - attributedTo: `${nconf.get('url')}/category/${cid}`, - audience: cid !== -1 ? `${nconf.get('url')}/category/${cid}` : undefined, + id: `${nconf.get('url')}/topic/${req.params.tid}/posts${collection.orderedItems && page ? `?page=${page}` : ''}`, ...collection, }; diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index 8993cb8875..0c26a36ea4 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -40,13 +40,14 @@ module.exports = function (app, middleware, controllers) { app.get('/post/:pid', [...middlewares, middleware.assert.post], controllers.activitypub.actors.note); app.get('/post/:pid/replies', [...middlewares, middleware.assert.post], controllers.activitypub.actors.replies); + app.get('/topic/:tid/posts', [...middlewares, middleware.assert.topic], controllers.activitypub.actors.topicPosts); app.get('/topic/:tid/:slug?', [...middlewares, middleware.assert.topic], controllers.activitypub.actors.topic); - 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', [...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); + app.get('/category/:cid/:slug?', [...middlewares, middleware.assert.category], controllers.activitypub.actors.category); app.get('/message/:mid', [...middlewares, middleware.assert.message], controllers.activitypub.actors.message); };