feat: context removal logic (aka moving topics to uncategorized, and federating this to other NodeBBs)

Squashed commit of the following:

commit 3309117eb1
Author: Julian Lam <julian@nodebb.org>
Date:   Tue Oct 21 11:48:12 2025 -0400

    fix: activitypubApi.remove.context to use oldCid instead of cid

commit e90c5f79eb
Author: Julian Lam <julian@nodebb.org>
Date:   Tue Oct 21 11:41:05 2025 -0400

    fix: parseInt cid in cid detection for api.topics.move

commit ab6561e60f
Author: Julian Lam <julian@nodebb.org>
Date:   Mon Oct 20 14:03:45 2025 -0400

    feat: inbox handler for Remove(Context)

commit 30dc527cc0
Author: Julian Lam <julian@nodebb.org>
Date:   Mon Oct 20 12:17:23 2025 -0400

    feat: unwind announce(delete), federate out Remove(Context) on delete, but not on purge
This commit is contained in:
Julian Lam
2025-10-21 12:00:01 -04:00
parent 83a172c9a4
commit 34e95e6d46
4 changed files with 79 additions and 83 deletions

View File

@@ -13,6 +13,7 @@ const notifications = require('../notifications');
const messaging = require('../messaging');
const flags = require('../flags');
const api = require('../api');
const utils = require('../utils');
const activitypub = require('.');
const socketHelpers = require('../socket.io/helpers');
@@ -78,6 +79,42 @@ inbox.add = async (req) => {
}
};
inbox.remove = async (req) => {
const { actor, object } = req.body;
const isContext = activitypub._constants.acceptable.contextTypes.has(object.type);
if (!isContext) {
return; // don't know how to handle other types
}
console.log('isContext?', isContext);
const mainPid = await activitypub.contexts.getItems(0, object.id, { returnRootId: true });
const exists = await posts.exists(mainPid);
if (!exists) {
return; // post not cached; do nothing.
}
console.log('mainPid is', mainPid);
// Ensure that cid is same-origin as the actor
const tid = await posts.getPostField(mainPid, 'tid');
const cid = await topics.getTopicField(tid, 'cid');
if (utils.isNumber(cid)) {
// remote removal of topic in local cid; what??
return;
}
const actorHostname = new URL(actor).hostname;
const cidHostname = new URL(cid).hostname;
if (actorHostname !== cidHostname) {
throw new Error('[[error:activitypub.origin-mismatch]]');
}
activitypub.helpers.log(`[activitypub/inbox/remove] Removing topic ${tid} from ${cid}`);
await topics.tools.move(tid, {
cid: -1,
uid: 'system',
});
};
inbox.update = async (req) => {
const { actor, object } = req.body;
const isPublic = publiclyAddressed([...(object.to || []), ...(object.cc || [])]);
@@ -200,8 +237,6 @@ inbox.delete = async (req) => {
if (type === 'Tombstone') {
method = 'delete'; // soft delete
} else if (activitypub._constants.acceptable.contextTypes.includes(type)) {
method = 'move'; // move to cid -1
}
} catch (e) {
// probably 410/404
@@ -221,11 +256,6 @@ inbox.delete = async (req) => {
// db.isSortedSetMember('usersRemote:lastCrawled', object.id),
]);
// 'move' method only applicable for contexts
if (method === 'move' && !isContext) {
return reject('Delete', object, actor);
}
switch (true) {
case isNote: {
const cid = await posts.getCidByPid(id);
@@ -248,13 +278,8 @@ inbox.delete = async (req) => {
return;
}
const { tid, uid } = await posts.getPostFields(pid, ['tid', 'uid']);
if (method === 'move') {
activitypub.helpers.log(`[activitypub/inbox.delete] Moving tid ${tid} to cid -1.`);
await api.topics.move({ uid }, { tid, cid: -1 });
} else {
activitypub.helpers.log(`[activitypub/inbox.delete] Deleting tid ${tid}.`);
await api.topics[method]({ uid }, { tids: [tid] });
}
activitypub.helpers.log(`[activitypub/inbox.delete] Deleting tid ${tid}.`);
await api.topics[method]({ uid }, { tids: [tid] });
break;
}
@@ -347,12 +372,6 @@ inbox.announce = async (req) => {
break;
}
case object.type === 'Delete': {
req.body = object;
await inbox.delete(req);
break;
}
case object.type === 'Create': {
object = object.object;
// falls through