mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: #12695 Topic Synchronization via resolvable context
- Generation of a context collection digest via object ids - Sending of said digest in ETag header - Parsing of digests via If-None-Match header - Update note assertion logic to handle 304 response
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
const winston = require('winston');
|
||||
|
||||
const db = require('../database');
|
||||
const posts = require('../posts');
|
||||
const topics = require('../topics');
|
||||
|
||||
const activitypub = module.parent.exports;
|
||||
const Contexts = module.exports;
|
||||
@@ -13,20 +15,38 @@ Contexts.get = async (uid, id) => {
|
||||
let context;
|
||||
let type;
|
||||
|
||||
// Generate digest for If-None-Match if locally cached
|
||||
const tid = await posts.getPostField(id, 'tid');
|
||||
const headers = {};
|
||||
if (tid) {
|
||||
const [mainPid, pids] = await Promise.all([
|
||||
topics.getTopicField(tid, 'mainPid'),
|
||||
db.getSortedSetMembers(`tid:${tid}:posts`),
|
||||
]);
|
||||
pids.push(mainPid);
|
||||
const digest = activitypub.helpers.generateDigest(new Set(pids));
|
||||
headers['If-None-Match'] = `"${digest}"`;
|
||||
}
|
||||
|
||||
try {
|
||||
({ context } = await activitypub.get('uid', uid, id));
|
||||
({ context } = await activitypub.get('uid', uid, id, { headers }));
|
||||
if (!context) {
|
||||
winston.verbose(`[activitypub/context] ${id} contains no context.`);
|
||||
return false;
|
||||
}
|
||||
({ type } = await activitypub.get('uid', uid, context));
|
||||
} catch (e) {
|
||||
if (e.code === 'ap_get_304') {
|
||||
winston.verbose(`[activitypub/context] ${id} context unchanged.`);
|
||||
return { tid };
|
||||
}
|
||||
|
||||
winston.verbose(`[activitypub/context] ${id} context not resolvable.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (acceptableTypes.includes(type)) {
|
||||
return context;
|
||||
return { context };
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -4,6 +4,7 @@ const { generateKeyPairSync } = require('crypto');
|
||||
const nconf = require('nconf');
|
||||
const validator = require('validator');
|
||||
const cheerio = require('cheerio');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const meta = require('../meta');
|
||||
const posts = require('../posts');
|
||||
@@ -19,6 +20,7 @@ const webfingerCache = ttl({
|
||||
max: 5000,
|
||||
ttl: 1000 * 60 * 60 * 24, // 24 hours
|
||||
});
|
||||
const sha256 = payload => crypto.createHash('sha256').update(payload).digest('hex');
|
||||
|
||||
const Helpers = module.exports;
|
||||
|
||||
@@ -403,3 +405,20 @@ Helpers.generateCollection = async ({ set, method, page, perPage, url }) => {
|
||||
|
||||
return object;
|
||||
};
|
||||
|
||||
Helpers.generateDigest = (set) => {
|
||||
if (!(set instanceof Set)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
return Array
|
||||
.from(set)
|
||||
.map(item => sha256(item))
|
||||
.reduce((memo, cur) => {
|
||||
const a = Buffer.from(memo, 'hex');
|
||||
const b = Buffer.from(cur, 'hex');
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const result = a.map((x, i) => x ^ b[i]);
|
||||
return result.toString('hex');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -43,7 +43,11 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => {
|
||||
|
||||
let chain;
|
||||
const context = await activitypub.contexts.get(uid, id);
|
||||
if (context) {
|
||||
if (context.tid) {
|
||||
unlock(id);
|
||||
const { tid } = context;
|
||||
return { tid, count: 0 };
|
||||
} else if (context.context) {
|
||||
chain = Array.from(await activitypub.contexts.getItems(uid, context));
|
||||
} else {
|
||||
// Fall back to inReplyTo traversal
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const nconf = require('nconf');
|
||||
|
||||
const db = require('../../database');
|
||||
const meta = require('../../meta');
|
||||
const privileges = require('../../privileges');
|
||||
const posts = require('../../posts');
|
||||
@@ -100,13 +101,33 @@ Actors.topic = async function (req, res, next) {
|
||||
const perPage = meta.config.postsPerPage;
|
||||
const { cid, titleRaw: name, mainPid, slug } = await topics.getTopicFields(req.params.tid, ['cid', 'title', 'mainPid', 'slug']);
|
||||
try {
|
||||
const collection = await activitypub.helpers.generateCollection({
|
||||
let [collection, pids] = await Promise.all([
|
||||
activitypub.helpers.generateCollection({
|
||||
set: `tid:${req.params.tid}:posts`,
|
||||
method: posts.getPidsFromSet,
|
||||
page,
|
||||
perPage,
|
||||
url: `${nconf.get('url')}/topic/${req.params.tid}`,
|
||||
}),
|
||||
db.getSortedSetMembers(`tid:${req.params.tid}:posts`),
|
||||
]);
|
||||
|
||||
// Generate digest for ETag
|
||||
pids.push(mainPid);
|
||||
pids = pids.map(pid => (utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid));
|
||||
const digest = activitypub.helpers.generateDigest(new Set(pids));
|
||||
const ifNoneMatch = req.get('If-None-Match').split(',').map((tag) => {
|
||||
tag = tag.trim();
|
||||
if (tag.startsWith('"') && tag.endsWith('"')) {
|
||||
return tag.slice(1, tag.length - 1);
|
||||
}
|
||||
|
||||
return tag;
|
||||
});
|
||||
if (ifNoneMatch.includes(digest)) {
|
||||
return res.sendStatus(304);
|
||||
}
|
||||
res.set('ETag', digest);
|
||||
|
||||
// Convert pids to urls
|
||||
collection.totalItems += 1;
|
||||
|
||||
Reference in New Issue
Block a user