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

@@ -412,63 +412,6 @@ activitypubApi.announce.user = enabledCheck(async (caller, { tid }) => {
});
});
activitypubApi.announce.delete = enabledCheck(async ({ uid }, { tid }) => {
const now = new Date();
const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']);
// Only local categories
if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) {
return;
}
const allowed = await privileges.categories.can('topics:read', cid, activitypub._constants.uid);
if (!allowed) {
activitypub.helpers.log(`[activitypub/api] Not federating announce of pid ${pid} to the fediverse due to privileges.`);
return;
}
const { to, cc, targets } = await activitypub.buildRecipients({
to: [activitypub._constants.publicAddress],
cc: [`${nconf.get('url')}/category/${cid}/followers`],
}, { cid });
const deleteTpl = {
id: `${nconf.get('url')}/topic/${tid}#activity/delete/${now.getTime()}`,
type: 'Delete',
actor: `${nconf.get('url')}/category/${cid}`,
to,
cc,
origin: `${nconf.get('url')}/category/${cid}`,
};
// 7888 variant
await activitypub.send('cid', cid, Array.from(targets), {
id: `${nconf.get('url')}/topic/${tid}#activity/announce/delete/${now.getTime()}`,
type: 'Announce',
actor: `${nconf.get('url')}/category/${cid}`,
to,
cc,
object: {
...deleteTpl,
object: `${nconf.get('url')}/topic/${tid}`,
},
});
// 1b12 variant
await activitypub.send('cid', cid, Array.from(targets), {
id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/delete/${now.getTime()}`,
type: 'Announce',
actor: `${nconf.get('url')}/category/${cid}`,
to,
cc,
object: {
...deleteTpl,
actor: `${nconf.get('url')}/uid/${uid}`,
object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid,
},
});
});
activitypubApi.undo = {};
// activitypubApi.undo.follow =
@@ -573,3 +516,38 @@ activitypubApi.undo.flag = enabledCheck(async (caller, flag) => {
});
await db.sortedSetRemove(`flag:${flag.flagId}:remote`, caller.uid);
});
activitypubApi.remove = {};
activitypubApi.remove.context = enabledCheck(async ({ uid }, { tid }) => {
// Federates Remove(Context); where Context is the tid
const now = new Date();
const cid = await topics.getTopicField(tid, 'oldCid');
// Only local categories
if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) {
return;
}
const allowed = await privileges.categories.can('topics:read', cid, activitypub._constants.uid);
if (!allowed) {
activitypub.helpers.log(`[activitypub/api] Not federating deletion of tid ${tid} to the fediverse due to privileges.`);
return;
}
const { to, cc, targets } = await activitypub.buildRecipients({
to: [activitypub._constants.publicAddress],
cc: [`${nconf.get('url')}/category/${cid}/followers`],
}, { cid });
// Remove(Context)
await activitypub.send('uid', uid, Array.from(targets), {
id: `${nconf.get('url')}/topic/${tid}#activity/remove/${now.getTime()}`,
type: 'Remove',
actor: `${nconf.get('url')}/uid/${uid}`,
to,
cc,
object: `${nconf.get('url')}/topic/${tid}`,
origin: `${nconf.get('url')}/category/${cid}`,
});
});

View File

@@ -8,6 +8,7 @@ const meta = require('../meta');
const privileges = require('../privileges');
const events = require('../events');
const batch = require('../batch');
const utils = require('../utils');
const activitypubApi = require('./activitypub');
const apiHelpers = require('./helpers');
@@ -321,13 +322,12 @@ topicsAPI.move = async (caller, { tid, cid }) => {
if (!topicData.deleted) {
socketHelpers.sendNotificationToTopicOwner(tid, caller.uid, 'move', 'notifications:moved-your-topic');
// AP: Announce(Delete(Object))
if (cid === -1) {
await activitypubApi.announce.delete({ uid: caller.uid }, { tid });
if (utils.isNumber(cid) && parseInt(cid, 10) === -1) {
activitypubApi.remove.context(caller, { tid });
// tbd: activitypubApi.undo.announce?
} else {
// tbd: activitypubApi.move
activitypubApi.announce.category(caller, { tid });
// tbd: api.activitypub.announce.move
}
}