diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 50ce01bf3c..733aff354f 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -63,212 +63,219 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { return null; } - 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); + try { + if (!options.skipChecks) { + id = (await activitypub.checkHeader(id)) || id; } - if (remoteCid || recipientCids.length) { - // Overrides passed-in value, respect addressing from main post over booster - options.cid = remoteCid || recipientCids.shift(); + 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; } - // Auto-categorization (takes place only if all other categorization efforts fail) - if (!options.cid) { - options.cid = await assignCategory(mainPost); + if (!chain || !chain.length) { + // Fall back to inReplyTo traversal on context retrieval failure + chain = Array.from(await Notes.getParentChain(uid, input)); + chain.reverse(); } - // mainPid ok to leave as-is - if (!title) { - const sentences = tokenizer.sentences(content || sourceContent, { sanitize: 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}`); + // Can't resolve — give up. + if (!chain.length) { + await unlock(id); 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 sentences = tokenizer.sentences(content || sourceContent, { sanitize: 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}`); + await unlock(id); + 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([ - Notes.updateLocalRecipients(mainPid, { to, cc }), - mainPost._activitypub.image ? topics.thumbs.associate({ - id: tid, - path: mainPost._activitypub.image, - }) : null, - posts.attachments.update(mainPid, attachment), + Notes.syncUserInboxes(tid, uid), + unlock(id), ]); - if (context) { - activitypub.helpers.log(`[activitypub/notes.assert] Associating tid ${tid} with context ${context}`); - await topics.setTopicField(tid, 'context', context); - } + return { tid, count }; + } catch (e) { + 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) => {