feat: topic backfill logic via resolvable context #12647

This commit is contained in:
Julian Lam
2024-06-28 12:54:32 -04:00
parent 615aaa01d6
commit e75ec39b70
6 changed files with 98 additions and 7 deletions

View File

@@ -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;
};

View File

@@ -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 };

View File

@@ -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');

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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),