mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +01:00
fix: wrap majority of note assertion logic in try..catch to handle exceptions so that the lock is always released
This commit is contained in:
@@ -64,213 +64,219 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.skipChecks) {
|
try {
|
||||||
id = (await activitypub.checkHeader(id)) || id;
|
if (!options.skipChecks) {
|
||||||
}
|
id = (await activitypub.checkHeader(id)) || id;
|
||||||
|
|
||||||
let chain;
|
|
||||||
let context = await activitypub.contexts.get(uid, id);
|
|
||||||
if (context.tid) {
|
|
||||||
await unlock(id);
|
|
||||||
const { tid } = context;
|
|
||||||
return { tid, count: 0 };
|
|
||||||
} else if (context.context) {
|
|
||||||
chain = Array.from(await activitypub.contexts.getItems(uid, context.context, { input }));
|
|
||||||
if (chain && chain.length) {
|
|
||||||
// Context resolves, use in later topic creation
|
|
||||||
context = context.context;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
context = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chain || !chain.length) {
|
|
||||||
// Fall back to inReplyTo traversal on context retrieval failure
|
|
||||||
chain = Array.from(await Notes.getParentChain(uid, input));
|
|
||||||
chain.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can't resolve — give up.
|
|
||||||
if (!chain.length) {
|
|
||||||
await unlock(id);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, title, content, sourceContent, _activitypub } = mainPost;
|
|
||||||
const hasTid = !!tid;
|
|
||||||
|
|
||||||
const cid = hasTid ? await topics.getTopicField(tid, 'cid') : options.cid || -1;
|
|
||||||
|
|
||||||
if (options.cid && cid === -1) {
|
|
||||||
// Move topic if currently uncategorized
|
|
||||||
await topics.tools.move(tid, { cid: options.cid, uid: 'system' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = await db.isSortedSetMembers(`tid:${tid}:posts`, chain.slice(1).map(p => p.pid));
|
|
||||||
members.unshift(await posts.exists(mainPid));
|
|
||||||
if (tid && members.every(Boolean)) {
|
|
||||||
// All cached, return early.
|
|
||||||
activitypub.helpers.log('[notes/assert] No new notes to process.');
|
|
||||||
await unlock(id);
|
|
||||||
return { tid, count: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasTid) {
|
|
||||||
mainPid = await topics.getTopicField(tid, 'mainPid');
|
|
||||||
} else {
|
|
||||||
// Check recipients/audience for category (local or remote)
|
|
||||||
const set = activitypub.helpers.makeSet(_activitypub, ['to', 'cc', 'audience']);
|
|
||||||
await activitypub.actors.assert(Array.from(set));
|
|
||||||
|
|
||||||
// Local
|
|
||||||
const resolved = await Promise.all(Array.from(set).map(async id => await activitypub.helpers.resolveLocalId(id)));
|
|
||||||
const recipientCids = resolved
|
|
||||||
.filter(Boolean)
|
|
||||||
.filter(({ type }) => type === 'category')
|
|
||||||
.map(obj => obj.id);
|
|
||||||
|
|
||||||
// Remote
|
|
||||||
let remoteCid;
|
|
||||||
const assertedGroups = await categories.exists(Array.from(set));
|
|
||||||
try {
|
|
||||||
const { hostname } = new URL(mainPid);
|
|
||||||
remoteCid = Array.from(set).filter((id, idx) => {
|
|
||||||
const { hostname: cidHostname } = new URL(id);
|
|
||||||
return assertedGroups[idx] && cidHostname === hostname;
|
|
||||||
}).shift();
|
|
||||||
} catch (e) {
|
|
||||||
// noop
|
|
||||||
winston.error('[activitypub/notes.assert] Could not parse URL of mainPid', e.stack);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteCid || recipientCids.length) {
|
let chain;
|
||||||
// Overrides passed-in value, respect addressing from main post over booster
|
let context = await activitypub.contexts.get(uid, id);
|
||||||
options.cid = remoteCid || recipientCids.shift();
|
if (context.tid) {
|
||||||
|
await unlock(id);
|
||||||
|
const { tid } = context;
|
||||||
|
return { tid, count: 0 };
|
||||||
|
} else if (context.context) {
|
||||||
|
chain = Array.from(await activitypub.contexts.getItems(uid, context.context, { input }));
|
||||||
|
if (chain && chain.length) {
|
||||||
|
// Context resolves, use in later topic creation
|
||||||
|
context = context.context;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-categorization (takes place only if all other categorization efforts fail)
|
if (!chain || !chain.length) {
|
||||||
if (!options.cid) {
|
// Fall back to inReplyTo traversal on context retrieval failure
|
||||||
options.cid = await assignCategory(mainPost);
|
chain = Array.from(await Notes.getParentChain(uid, input));
|
||||||
|
chain.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// mainPid ok to leave as-is
|
// Can't resolve — give up.
|
||||||
if (!title) {
|
if (!chain.length) {
|
||||||
const prettified = pretty(content || sourceContent);
|
await unlock(id);
|
||||||
const sentences = tokenizer.sentences(prettified, { sanitize: true, newline_boundaries: true });
|
|
||||||
title = sentences.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any custom emoji from title
|
|
||||||
if (_activitypub && _activitypub.tag && Array.isArray(_activitypub.tag)) {
|
|
||||||
_activitypub.tag
|
|
||||||
.filter(tag => tag.type === 'Emoji')
|
|
||||||
.forEach((tag) => {
|
|
||||||
title = title.replace(new RegExp(tag.name, 'g'), '');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mainPid = utils.isNumber(mainPid) ? parseInt(mainPid, 10) : mainPid;
|
|
||||||
|
|
||||||
// Relation & privilege check for local categories
|
|
||||||
const inputIndex = chain.map(n => n.pid).indexOf(id);
|
|
||||||
const hasRelation =
|
|
||||||
uid || hasTid ||
|
|
||||||
options.skipChecks || options.cid ||
|
|
||||||
await assertRelation(chain[inputIndex !== -1 ? inputIndex : 0]);
|
|
||||||
|
|
||||||
const privilege = `topics:${tid ? 'reply' : 'create'}`;
|
|
||||||
const allowed = await privileges.categories.can(privilege, options.cid || cid, activitypub._constants.uid);
|
|
||||||
if (!hasRelation || !allowed) {
|
|
||||||
if (!hasRelation) {
|
|
||||||
activitypub.helpers.log(`[activitypub/notes.assert] Not asserting ${id} as it has no relation to existing tracked content.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await unlock(id);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
tid = tid || utils.generateUUID();
|
|
||||||
mainPost.tid = tid;
|
|
||||||
|
|
||||||
const urlMap = chain.reduce((map, post) => (post.url ? map.set(post.url, post.id) : map), new Map());
|
|
||||||
const unprocessed = chain.map((post) => {
|
|
||||||
post.tid = tid; // add tid to post hash
|
|
||||||
|
|
||||||
// Ensure toPids in replies are ids
|
|
||||||
if (urlMap.has(post.toPid)) {
|
|
||||||
post.toPid = urlMap.get(post.toPid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return post;
|
|
||||||
}).filter((p, idx) => !members[idx]);
|
|
||||||
const count = unprocessed.length;
|
|
||||||
activitypub.helpers.log(`[notes/assert] ${count} new note(s) found.`);
|
|
||||||
|
|
||||||
if (!hasTid) {
|
|
||||||
const { to, cc, attachment } = mainPost._activitypub;
|
|
||||||
const tags = await Notes._normalizeTags(mainPost._activitypub.tag || []);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await topics.post({
|
|
||||||
tid,
|
|
||||||
uid: authorId,
|
|
||||||
cid: options.cid || cid,
|
|
||||||
pid: mainPid,
|
|
||||||
title,
|
|
||||||
timestamp,
|
|
||||||
tags,
|
|
||||||
content: mainPost.content,
|
|
||||||
sourceContent: mainPost.sourceContent,
|
|
||||||
_activitypub: mainPost._activitypub,
|
|
||||||
});
|
|
||||||
unprocessed.shift();
|
|
||||||
} catch (e) {
|
|
||||||
activitypub.helpers.log(`[activitypub/notes.assert] Could not post topic (${mainPost.pid}): ${e.message}`);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These must come after topic is posted
|
// 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, title, content, sourceContent, _activitypub } = mainPost;
|
||||||
|
const hasTid = !!tid;
|
||||||
|
|
||||||
|
const cid = hasTid ? await topics.getTopicField(tid, 'cid') : options.cid || -1;
|
||||||
|
|
||||||
|
if (options.cid && cid === -1) {
|
||||||
|
// Move topic if currently uncategorized
|
||||||
|
await topics.tools.move(tid, { cid: options.cid, uid: 'system' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = await db.isSortedSetMembers(`tid:${tid}:posts`, chain.slice(1).map(p => p.pid));
|
||||||
|
members.unshift(await posts.exists(mainPid));
|
||||||
|
if (tid && members.every(Boolean)) {
|
||||||
|
// All cached, return early.
|
||||||
|
activitypub.helpers.log('[notes/assert] No new notes to process.');
|
||||||
|
await unlock(id);
|
||||||
|
return { tid, count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTid) {
|
||||||
|
mainPid = await topics.getTopicField(tid, 'mainPid');
|
||||||
|
} else {
|
||||||
|
// Check recipients/audience for category (local or remote)
|
||||||
|
const set = activitypub.helpers.makeSet(_activitypub, ['to', 'cc', 'audience']);
|
||||||
|
await activitypub.actors.assert(Array.from(set));
|
||||||
|
|
||||||
|
// Local
|
||||||
|
const resolved = await Promise.all(Array.from(set).map(async id => await activitypub.helpers.resolveLocalId(id)));
|
||||||
|
const recipientCids = resolved
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(({ type }) => type === 'category')
|
||||||
|
.map(obj => obj.id);
|
||||||
|
|
||||||
|
// Remote
|
||||||
|
let remoteCid;
|
||||||
|
const assertedGroups = await categories.exists(Array.from(set));
|
||||||
|
try {
|
||||||
|
const { hostname } = new URL(mainPid);
|
||||||
|
remoteCid = Array.from(set).filter((id, idx) => {
|
||||||
|
const { hostname: cidHostname } = new URL(id);
|
||||||
|
return assertedGroups[idx] && cidHostname === hostname;
|
||||||
|
}).shift();
|
||||||
|
} catch (e) {
|
||||||
|
// noop
|
||||||
|
winston.error('[activitypub/notes.assert] Could not parse URL of mainPid', e.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteCid || recipientCids.length) {
|
||||||
|
// Overrides passed-in value, respect addressing from main post over booster
|
||||||
|
options.cid = remoteCid || recipientCids.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-categorization (takes place only if all other categorization efforts fail)
|
||||||
|
if (!options.cid) {
|
||||||
|
options.cid = await assignCategory(mainPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mainPid ok to leave as-is
|
||||||
|
if (!title) {
|
||||||
|
const prettified = pretty(content || sourceContent);
|
||||||
|
const sentences = tokenizer.sentences(prettified, { sanitize: true, newline_boundaries: true });
|
||||||
|
title = sentences.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any custom emoji from title
|
||||||
|
if (_activitypub && _activitypub.tag && Array.isArray(_activitypub.tag)) {
|
||||||
|
_activitypub.tag
|
||||||
|
.filter(tag => tag.type === 'Emoji')
|
||||||
|
.forEach((tag) => {
|
||||||
|
title = title.replace(new RegExp(tag.name, 'g'), '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainPid = utils.isNumber(mainPid) ? parseInt(mainPid, 10) : mainPid;
|
||||||
|
|
||||||
|
// Relation & privilege check for local categories
|
||||||
|
const inputIndex = chain.map(n => n.pid).indexOf(id);
|
||||||
|
const hasRelation =
|
||||||
|
uid || hasTid ||
|
||||||
|
options.skipChecks || options.cid ||
|
||||||
|
await assertRelation(chain[inputIndex !== -1 ? inputIndex : 0]);
|
||||||
|
|
||||||
|
const privilege = `topics:${tid ? 'reply' : 'create'}`;
|
||||||
|
const allowed = await privileges.categories.can(privilege, options.cid || cid, activitypub._constants.uid);
|
||||||
|
if (!hasRelation || !allowed) {
|
||||||
|
if (!hasRelation) {
|
||||||
|
activitypub.helpers.log(`[activitypub/notes.assert] Not asserting ${id} as it has no relation to existing tracked content.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await unlock(id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
tid = tid || utils.generateUUID();
|
||||||
|
mainPost.tid = tid;
|
||||||
|
|
||||||
|
const urlMap = chain.reduce((map, post) => (post.url ? map.set(post.url, post.id) : map), new Map());
|
||||||
|
const unprocessed = chain.map((post) => {
|
||||||
|
post.tid = tid; // add tid to post hash
|
||||||
|
|
||||||
|
// Ensure toPids in replies are ids
|
||||||
|
if (urlMap.has(post.toPid)) {
|
||||||
|
post.toPid = urlMap.get(post.toPid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return post;
|
||||||
|
}).filter((p, idx) => !members[idx]);
|
||||||
|
const count = unprocessed.length;
|
||||||
|
activitypub.helpers.log(`[notes/assert] ${count} new note(s) found.`);
|
||||||
|
|
||||||
|
if (!hasTid) {
|
||||||
|
const { to, cc, attachment } = mainPost._activitypub;
|
||||||
|
const tags = await Notes._normalizeTags(mainPost._activitypub.tag || []);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await topics.post({
|
||||||
|
tid,
|
||||||
|
uid: authorId,
|
||||||
|
cid: options.cid || cid,
|
||||||
|
pid: mainPid,
|
||||||
|
title,
|
||||||
|
timestamp,
|
||||||
|
tags,
|
||||||
|
content: mainPost.content,
|
||||||
|
sourceContent: mainPost.sourceContent,
|
||||||
|
_activitypub: mainPost._activitypub,
|
||||||
|
});
|
||||||
|
unprocessed.shift();
|
||||||
|
} catch (e) {
|
||||||
|
activitypub.helpers.log(`[activitypub/notes.assert] Could not post topic (${mainPost.pid}): ${e.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These must come after topic is posted
|
||||||
|
await Promise.all([
|
||||||
|
Notes.updateLocalRecipients(mainPid, { to, cc }),
|
||||||
|
mainPost._activitypub.image ? topics.thumbs.associate({
|
||||||
|
id: tid,
|
||||||
|
path: mainPost._activitypub.image,
|
||||||
|
}) : null,
|
||||||
|
posts.attachments.update(mainPid, attachment),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
activitypub.helpers.log(`[activitypub/notes.assert] Associating tid ${tid} with context ${context}`);
|
||||||
|
await topics.setTopicField(tid, 'context', context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const post of unprocessed) {
|
||||||
|
const { to, cc, attachment } = post._activitypub;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await topics.reply(post);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await Promise.all([
|
||||||
|
Notes.updateLocalRecipients(post.pid, { to, cc }),
|
||||||
|
posts.attachments.update(post.pid, attachment),
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
activitypub.helpers.log(`[activitypub/notes.assert] Could not add reply (${post.pid}): ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Notes.updateLocalRecipients(mainPid, { to, cc }),
|
Notes.syncUserInboxes(tid, uid),
|
||||||
mainPost._activitypub.image ? topics.thumbs.associate({
|
unlock(id),
|
||||||
id: tid,
|
|
||||||
path: mainPost._activitypub.image,
|
|
||||||
}) : null,
|
|
||||||
posts.attachments.update(mainPid, attachment),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (context) {
|
return { tid, count };
|
||||||
activitypub.helpers.log(`[activitypub/notes.assert] Associating tid ${tid} with context ${context}`);
|
} catch (e) {
|
||||||
await topics.setTopicField(tid, 'context', context);
|
winston.warn(`[activitypub/notes.assert] Could not assert ${id} (${e.message}), releasing lock.`);
|
||||||
}
|
await unlock(id);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const post of unprocessed) {
|
|
||||||
const { to, cc, attachment } = post._activitypub;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await topics.reply(post);
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await Promise.all([
|
|
||||||
Notes.updateLocalRecipients(post.pid, { to, cc }),
|
|
||||||
posts.attachments.update(post.pid, attachment),
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
|
||||||
activitypub.helpers.log(`[activitypub/notes.assert] Could not add reply (${post.pid}): ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
Notes.syncUserInboxes(tid, uid),
|
|
||||||
unlock(id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { tid, count };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Notes.assertPrivate = async (object) => {
|
Notes.assertPrivate = async (object) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user