diff --git a/src/activitypub/contexts.js b/src/activitypub/contexts.js new file mode 100644 index 0000000000..ebe3ddfe20 --- /dev/null +++ b/src/activitypub/contexts.js @@ -0,0 +1,77 @@ +'use strict'; + +const winston = require('winston'); + +const posts = require('../posts'); + +const activitypub = module.parent.exports; +const Contexts = module.exports; + +const acceptableTypes = ['Collection', 'CollectionPage', 'OrderedCollection', 'OrderedCollectionPage']; + +Contexts.get = async (uid, id) => { + let context; + let type; + + try { + ({ context } = await activitypub.get('uid', uid, id)); + ({ type } = await activitypub.get('uid', uid, context)); + } catch (e) { + winston.verbose(`[activitypub/context] ${id} context not resolvable.`); + return false; + } + + if (acceptableTypes.includes(type)) { + return context; + } + + return false; +}; + +Contexts.getItems = async (uid, id, root = true) => { + winston.verbose(`[activitypub/context] Retrieving context ${id}`); + let { type, items, first, next } = await activitypub.get('uid', uid, id); + if (!acceptableTypes.includes(type)) { + return []; + } + + if (items) { + items = (await Promise.all(items.map(async (item) => { + const { type, id } = await activitypub.helpers.resolveLocalId(item); + const pid = type === 'post' && id ? id : item; + const postData = await posts.getPostData(pid); + if (postData) { + // Already cached + return postData; + } + + // No local copy, fetch from source + try { + const object = await activitypub.get('uid', uid, pid); + winston.verbose(`[activitypub/context] Retrieved ${pid}`); + return await activitypub.mocks.post(object); + } catch (e) { + // Unresolvable, either temporariliy or permanent, ignore for now. + winston.verbose(`[activitypub/context] Cannot retrieve ${id}`); + return null; + } + }))).filter(Boolean); + winston.verbose(`[activitypub/context] Found ${items.length} items.`); + } + + const chain = new Set(items || []); + if (!next && root && first) { + next = first; + } + + if (next) { + winston.verbose('[activitypub/context] Fetching next page...'); + Array + .from(await Contexts.getItems(uid, next, false)) + .forEach((item) => { + chain.add(item); + }); + } + + return chain; +}; diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 71639298fb..1721ce40a7 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -134,7 +134,6 @@ Helpers.resolveLocalId = async (input) => { activityData = { activity, data, timestamp }; } - // https://bb.devnull.land/cid/2#activity/follow/activitypub@community.nodebb.org│ switch (prefix) { case 'uid': return { type: 'user', id: value, ...activityData }; diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 21aa47a492..3fa7055ee8 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -40,6 +40,7 @@ ActivityPub.helpers = require('./helpers'); ActivityPub.inbox = require('./inbox'); ActivityPub.mocks = require('./mocks'); ActivityPub.notes = require('./notes'); +ActivityPub.contexts = require('./contexts'); ActivityPub.actors = require('./actors'); ActivityPub.instances = require('./instances'); diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 794d0660b8..2209dda8f4 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -43,13 +43,23 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { return null; } - const chain = Array.from(await Notes.getParentChain(uid, input)); + let chain; + const context = await activitypub.contexts.get(uid, id); + if (context) { + chain = Array.from(await activitypub.contexts.getItems(uid, context)); + } else { + // Fall back to inReplyTo traversal + chain = Array.from(await Notes.getParentChain(uid, input)); + } if (!chain.length) { unlock(id); return null; } - const mainPost = chain[chain.length - 1]; + // Reorder chain items by timestamp + chain = chain.sort((a, b) => a.timestamp - b.timestamp); + + const mainPost = chain[0]; let { pid: mainPid, tid, uid: authorId, timestamp, name, content, _activitypub } = mainPost; const hasTid = !!tid; @@ -133,7 +143,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { .filter(o => o.type === 'Hashtag' && !systemTags.includes(o.name.slice(1))) .map(o => o.name.slice(1)); - if (maxTags && tags.length > maxTags) { + if (tags.length > maxTags) { tags.length = maxTags; } @@ -152,10 +162,9 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { Notes.updateLocalRecipients(mainPid, { to, cc }), posts.attachments.update(mainPid, attachment), ]); - unprocessed.pop(); + unprocessed.shift(); } - unprocessed.reverse(); for (const post of unprocessed) { const { to, cc, attachment } = post._activitypub; diff --git a/src/controllers/activitypub/actors.js b/src/controllers/activitypub/actors.js index 4391f03cee..bcd8709b74 100644 --- a/src/controllers/activitypub/actors.js +++ b/src/controllers/activitypub/actors.js @@ -101,7 +101,8 @@ Actors.topic = async function (req, res, next) { url: `${nconf.get('url')}/topic/${slug}`, name, type: paginate && items ? 'OrderedCollectionPage' : 'OrderedCollection', - audience: `${nconf.get('url')}/category/${cid}`, + attributedTo: `${nconf.get('url')}/category/${cid}`, + audience: cid !== -1 ? `${nconf.get('url')}/category/${cid}/followers` : undefined, totalItems: postcount, }; diff --git a/src/posts/create.js b/src/posts/create.js index 2c23560266..af1ea92c25 100644 --- a/src/posts/create.js +++ b/src/posts/create.js @@ -82,6 +82,10 @@ module.exports = function (Posts) { } async function checkToPid(toPid, uid) { + if (!utils.isNumber(toPid)) { + return; + } + const [toPost, canViewToPid] = await Promise.all([ Posts.getPostFields(toPid, ['pid', 'deleted']), privileges.posts.can('posts:view_deleted', toPid, uid),