mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-20 07:20:27 +01:00
feat: context removal logic (aka moving topics to uncategorized, and federating this to other NodeBBs)
Squashed commit of the following: commit3309117eb1Author: Julian Lam <julian@nodebb.org> Date: Tue Oct 21 11:48:12 2025 -0400 fix: activitypubApi.remove.context to use oldCid instead of cid commite90c5f79ebAuthor: Julian Lam <julian@nodebb.org> Date: Tue Oct 21 11:41:05 2025 -0400 fix: parseInt cid in cid detection for api.topics.move commitab6561e60fAuthor: Julian Lam <julian@nodebb.org> Date: Mon Oct 20 14:03:45 2025 -0400 feat: inbox handler for Remove(Context) commit30dc527cc0Author: 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:
@@ -13,6 +13,7 @@ const notifications = require('../notifications');
|
|||||||
const messaging = require('../messaging');
|
const messaging = require('../messaging');
|
||||||
const flags = require('../flags');
|
const flags = require('../flags');
|
||||||
const api = require('../api');
|
const api = require('../api');
|
||||||
|
const utils = require('../utils');
|
||||||
const activitypub = require('.');
|
const activitypub = require('.');
|
||||||
|
|
||||||
const socketHelpers = require('../socket.io/helpers');
|
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) => {
|
inbox.update = async (req) => {
|
||||||
const { actor, object } = req.body;
|
const { actor, object } = req.body;
|
||||||
const isPublic = publiclyAddressed([...(object.to || []), ...(object.cc || [])]);
|
const isPublic = publiclyAddressed([...(object.to || []), ...(object.cc || [])]);
|
||||||
@@ -200,8 +237,6 @@ inbox.delete = async (req) => {
|
|||||||
|
|
||||||
if (type === 'Tombstone') {
|
if (type === 'Tombstone') {
|
||||||
method = 'delete'; // soft delete
|
method = 'delete'; // soft delete
|
||||||
} else if (activitypub._constants.acceptable.contextTypes.includes(type)) {
|
|
||||||
method = 'move'; // move to cid -1
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// probably 410/404
|
// probably 410/404
|
||||||
@@ -221,11 +256,6 @@ inbox.delete = async (req) => {
|
|||||||
// db.isSortedSetMember('usersRemote:lastCrawled', object.id),
|
// db.isSortedSetMember('usersRemote:lastCrawled', object.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 'move' method only applicable for contexts
|
|
||||||
if (method === 'move' && !isContext) {
|
|
||||||
return reject('Delete', object, actor);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case isNote: {
|
case isNote: {
|
||||||
const cid = await posts.getCidByPid(id);
|
const cid = await posts.getCidByPid(id);
|
||||||
@@ -248,13 +278,8 @@ inbox.delete = async (req) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { tid, uid } = await posts.getPostFields(pid, ['tid', 'uid']);
|
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}.`);
|
activitypub.helpers.log(`[activitypub/inbox.delete] Deleting tid ${tid}.`);
|
||||||
await api.topics[method]({ uid }, { tids: [tid] });
|
await api.topics[method]({ uid }, { tids: [tid] });
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,12 +372,6 @@ inbox.announce = async (req) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case object.type === 'Delete': {
|
|
||||||
req.body = object;
|
|
||||||
await inbox.delete(req);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case object.type === 'Create': {
|
case object.type === 'Create': {
|
||||||
object = object.object;
|
object = object.object;
|
||||||
// falls through
|
// falls through
|
||||||
|
|||||||
@@ -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 = {};
|
||||||
|
|
||||||
// activitypubApi.undo.follow =
|
// activitypubApi.undo.follow =
|
||||||
@@ -573,3 +516,38 @@ activitypubApi.undo.flag = enabledCheck(async (caller, flag) => {
|
|||||||
});
|
});
|
||||||
await db.sortedSetRemove(`flag:${flag.flagId}:remote`, caller.uid);
|
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}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ const meta = require('../meta');
|
|||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
const events = require('../events');
|
const events = require('../events');
|
||||||
const batch = require('../batch');
|
const batch = require('../batch');
|
||||||
|
const utils = require('../utils');
|
||||||
|
|
||||||
const activitypubApi = require('./activitypub');
|
const activitypubApi = require('./activitypub');
|
||||||
const apiHelpers = require('./helpers');
|
const apiHelpers = require('./helpers');
|
||||||
@@ -321,13 +322,12 @@ topicsAPI.move = async (caller, { tid, cid }) => {
|
|||||||
if (!topicData.deleted) {
|
if (!topicData.deleted) {
|
||||||
socketHelpers.sendNotificationToTopicOwner(tid, caller.uid, 'move', 'notifications:moved-your-topic');
|
socketHelpers.sendNotificationToTopicOwner(tid, caller.uid, 'move', 'notifications:moved-your-topic');
|
||||||
|
|
||||||
// AP: Announce(Delete(Object))
|
if (utils.isNumber(cid) && parseInt(cid, 10) === -1) {
|
||||||
if (cid === -1) {
|
activitypubApi.remove.context(caller, { tid });
|
||||||
await activitypubApi.announce.delete({ uid: caller.uid }, { tid });
|
|
||||||
// tbd: activitypubApi.undo.announce?
|
// tbd: activitypubApi.undo.announce?
|
||||||
} else {
|
} else {
|
||||||
|
// tbd: activitypubApi.move
|
||||||
activitypubApi.announce.category(caller, { tid });
|
activitypubApi.announce.category(caller, { tid });
|
||||||
// tbd: api.activitypub.announce.move
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ module.exports = function (Topics) {
|
|||||||
deleterUid: uid,
|
deleterUid: uid,
|
||||||
deletedTimestamp: Date.now(),
|
deletedTimestamp: Date.now(),
|
||||||
}),
|
}),
|
||||||
api.activitypub.announce.delete({ uid }, { tid }),
|
api.activitypub.remove.context({ uid }, { tid }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await categories.updateRecentTidForCid(cid);
|
await categories.updateRecentTidForCid(cid);
|
||||||
@@ -82,7 +82,6 @@ module.exports = function (Topics) {
|
|||||||
}
|
}
|
||||||
deletedTopic.tags = tags;
|
deletedTopic.tags = tags;
|
||||||
await deleteFromFollowersIgnorers(tid);
|
await deleteFromFollowersIgnorers(tid);
|
||||||
await api.activitypub.announce.delete({ uid }, { tid }),
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.deleteAll([
|
db.deleteAll([
|
||||||
|
|||||||
Reference in New Issue
Block a user