From 2981f663ce30366d467874ce128d2e2d290968cc Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 16 Jan 2024 10:44:47 -0500 Subject: [PATCH] fix: issues related to adding new reply chains to an existing topic, resolveId method in notes module --- src/activitypub/notes.js | 82 ++++++++++++++++++++------- src/controllers/activitypub/topics.js | 6 ++ 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 4dbca8fe4e..41a25fc837 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -8,24 +8,32 @@ const posts = require('../posts'); const activitypub = module.parent.exports; const Notes = module.exports; +Notes.resolveId = async (uid, id) => { + ({ id } = await activitypub.get(uid, id)); + return id; +}; + // todo: when asserted, notes aren't added to a global sorted set // also, db.exists call is probably expensive Notes.assert = async (uid, input) => { // Ensures that each note has been saved to the database await Promise.all(input.map(async (item) => { - const id = activitypub.helpers.isUri(item) ? item : item.id; + const id = activitypub.helpers.isUri(item) ? item : item.pid; const key = `post:${id}`; const exists = await db.exists(key); winston.verbose(`[activitypub/notes.assert] Asserting note id ${id}`); - let postData; if (!exists) { + let postData; winston.verbose(`[activitypub/notes.assert] Not found, saving note to database`); - const object = activitypub.helpers.isUri(item) ? await activitypub.get(uid, item) : item; - postData = await activitypub.mocks.post(object); - if (postData) { - await db.setObject(key, postData); + if (activitypub.helpers.isUri(item)) { + const object = await activitypub.get(uid, item); + postData = await activitypub.mocks.post(object); + } else { + postData = item; } + + await db.setObject(key, postData); } })); }; @@ -59,21 +67,41 @@ Notes.getParentChain = async (uid, input) => { return chain; }; +Notes.assertParentChain = async (chain) => { + const data = []; + chain.reduce((child, parent) => { + data.push([`pid:${parent.pid}:replies`, child.timestamp, child.pid]); + return parent; + }); + + await db.sortedSetAddBulk(data); +}; + Notes.assertTopic = async (uid, id) => { - // Given the id of any post, traverses up (and soon, down) to cache the entire threaded context + /** + * Given the id of any post, traverses up to cache the entire threaded context + * + * Unfortunately, due to limitations and fragmentation of the existing ActivityPub landscape, + * retrieving the entire reply tree is not possible at this time. + */ + const chain = Array.from(await Notes.getParentChain(uid, id)); const tid = chain[chain.length - 1].pid; - const sorted = chain.sort((a, b) => a.timestamp - b.timestamp); - const [ids, timestamps] = [ - sorted.map(n => n.id), - sorted.map(n => n.timestamp), - ]; + const members = await db.isSortedSetMembers(`tidRemote:${tid}:posts`, chain.map(p => p.pid)); + if (members.every(Boolean)) { + // All cached, return early. + winston.info('[notes/assertTopic] No new notes to process.'); + return tid; + } - const postercount = chain.reduce((set, cur) => { - set.add(cur.uid); - return set; - }, new Set()); + const unprocessed = chain.filter((p, idx) => !members[idx]); + winston.info(`[notes/assertTopic] ${unprocessed.length} new notes found.`); + + const [ids, timestamps] = [ + unprocessed.map(n => n.pid), + unprocessed.map(n => n.timestamp), + ]; await Promise.all([ db.setObject(`topicRemote:${tid}`, { @@ -83,16 +111,32 @@ Notes.assertTopic = async (uid, id) => { mainPid: tid, title: 'TBD', slug: `remote?resource=${encodeURIComponent(tid)}`, - postcount: sorted.length, - postercount, }), db.sortedSetAdd(`tidRemote:${tid}:posts`, timestamps, ids), - Notes.assert(uid, chain), + Notes.assert(uid, unprocessed), + ]); + await Promise.all([ // must be done after .assert() + Notes.assertParentChain(chain), + Notes.updateTopicCounts(tid), ]); return tid; }; +Notes.updateTopicCounts = async function (tid) { + const pids = await db.getSortedSetMembers(`tidRemote:${tid}:posts`); + let uids = await db.getObjectsFields(pids.map(p => `post:${p}`), ['uid']); + uids = uids.reduce((set, { uid }) => { + set.add(uid); + return set; + }, new Set()); + + db.setObject(`topicRemote:${tid}`, { + postercount: uids.size, + postcount: pids.length, + }); +}; + Notes.getTopicPosts = async (tid, uid, start, stop) => { const pids = await db.getSortedSetRange(`tidRemote:${tid}:posts`, start, stop); return await posts.getPostsByPids(pids, uid); diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 5bcb466299..fc5cdea85a 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -9,10 +9,16 @@ const topics = require('../../topics'); const { notes } = require('../../activitypub'); // const helpers = require('../helpers'); const pagination = require('../../pagination'); +const helpers = require('../helpers'); const controller = module.exports; controller.get = async function (req, res, next) { + const pid = await notes.resolveId(req.uid, req.query.resource); + if (pid !== req.query.resource) { + return helpers.redirect(res, `/topic/remote?resource=${pid}`, true); + } + const tid = await notes.assertTopic(req.uid, req.query.resource); let postIndex = await db.sortedSetRank(`tidRemote:${tid}:posts`, req.query.resource);