mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-02 12:05:57 +01:00
feat: ability to browse to any ActivityPub note and have the entire topic chain render
Added methods for going up the inReplyTo chain to parent, asserting the topic, etc.
This commit is contained in:
82
src/activitypub/notes.js
Normal file
82
src/activitypub/notes.js
Normal file
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
const winston = require('winston');
|
||||
|
||||
const db = require('../database');
|
||||
const posts = require('../posts');
|
||||
|
||||
const activitypub = module.parent.exports;
|
||||
const Notes = module.exports;
|
||||
|
||||
// todo: when asserted, notes aren't added to a global sorted set
|
||||
// also, db.exists call is probably expensive
|
||||
Notes.assert = async (uid, input) => {
|
||||
// Ensures that each note has been saved to the database
|
||||
await Promise.all(input.map(async (item) => {
|
||||
const id = activitypub.helpers.isUri(item) ? item : item.id;
|
||||
const key = `post:${id}`;
|
||||
const exists = await db.exists(key);
|
||||
winston.verbose(`[activitypub/notes.assert] Asserting note id ${id}`);
|
||||
|
||||
let postData;
|
||||
if (!exists) {
|
||||
winston.verbose(`[activitypub/notes.assert] Not found, saving note to database`);
|
||||
const object = activitypub.helpers.isUri(item) ? await activitypub.get(uid, item) : item;
|
||||
postData = await activitypub.mocks.post(object);
|
||||
if (postData) {
|
||||
await db.setObject(key, postData);
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
Notes.getParentChain = async (uid, input) => {
|
||||
// Traverse upwards via `inReplyTo` until you find the root-level Note
|
||||
const id = activitypub.helpers.isUri(input) ? input : input.id;
|
||||
|
||||
const chain = new Set();
|
||||
const traverse = async (uid, id) => {
|
||||
const exists = await db.exists(`post:${id}`);
|
||||
if (exists) {
|
||||
const { toPid, timestamp } = await posts.getPostFields(id, ['toPid', 'timestamp']);
|
||||
chain.add({ id, timestamp });
|
||||
if (toPid) {
|
||||
await traverse(uid, toPid);
|
||||
}
|
||||
} else {
|
||||
let object = await activitypub.get(uid, id);
|
||||
object = await activitypub.mocks.post(object);
|
||||
if (object) {
|
||||
chain.add({ id, timestamp: object.timestamp });
|
||||
if (object.hasOwnProperty('toPid') && object.toPid) {
|
||||
await traverse(uid, object.toPid);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await traverse(uid, id);
|
||||
return chain;
|
||||
};
|
||||
|
||||
Notes.assertTopic = async (uid, id) => {
|
||||
// Given the id of any post, traverses up (and soon, down) to cache the entire threaded context
|
||||
const chain = Array.from(await Notes.getParentChain(uid, id));
|
||||
const tid = chain[chain.length - 1].id;
|
||||
|
||||
const sorted = chain.sort((a, b) => a.timestamp - b.timestamp);
|
||||
const [ids, timestamps] = [
|
||||
sorted.map(n => n.id),
|
||||
sorted.map(n => n.timestamp),
|
||||
];
|
||||
|
||||
await db.sortedSetAdd(`topicRemote:${tid}`, timestamps, ids);
|
||||
await Notes.assert(uid, chain);
|
||||
|
||||
return tid;
|
||||
};
|
||||
|
||||
Notes.getTopicPosts = async (tid, uid, start, stop) => {
|
||||
const pids = await db.getSortedSetRange(`topicRemote:${tid}`, start, stop);
|
||||
return await posts.getPostsByPids(pids, uid);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ const helpers = require('../helpers');
|
||||
const Controller = module.exports;
|
||||
|
||||
Controller.profiles = require('./profiles');
|
||||
Controller.topics = require('./topics');
|
||||
|
||||
Controller.getActor = async (req, res) => {
|
||||
// todo: view:users priv gate
|
||||
|
||||
81
src/controllers/activitypub/topics.js
Normal file
81
src/controllers/activitypub/topics.js
Normal file
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
|
||||
const user = require('../../user');
|
||||
const topics = require('../../topics');
|
||||
|
||||
const { notes } = require('../../activitypub');
|
||||
// const helpers = require('../helpers');
|
||||
// const pagination = require('../../pagination');
|
||||
|
||||
const controller = module.exports;
|
||||
|
||||
controller.get = async function (req, res, next) {
|
||||
const tid = await notes.assertTopic(req.uid, req.query.resource);
|
||||
|
||||
let postIndex = parseInt(req.params.post_index, 10) || 1;
|
||||
const [
|
||||
// userPrivileges,
|
||||
settings,
|
||||
// topicData,
|
||||
] = await Promise.all([
|
||||
// privileges.topics.get(tid, req.uid),
|
||||
user.getSettings(req.uid),
|
||||
// topics.getTopicData(tid),
|
||||
]);
|
||||
|
||||
const topicData = {
|
||||
tid,
|
||||
postCount: 6,
|
||||
category: {}, // todo
|
||||
};
|
||||
|
||||
let currentPage = parseInt(req.query.page, 10) || 1;
|
||||
const pageCount = Math.max(1, Math.ceil((topicData && topicData.postcount) / settings.postsPerPage));
|
||||
const invalidPagination = (settings.usePagination && (currentPage < 1 || currentPage > pageCount));
|
||||
if (
|
||||
!topicData ||
|
||||
// userPrivileges.disabled ||
|
||||
invalidPagination// ||
|
||||
// (topicData.scheduled && !userPrivileges.view_scheduled)
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!req.query.page) {
|
||||
currentPage = calculatePageFromIndex(postIndex, settings);
|
||||
}
|
||||
if (settings.usePagination && req.query.page) {
|
||||
const top = ((currentPage - 1) * settings.postsPerPage) + 1;
|
||||
const bottom = top + settings.postsPerPage;
|
||||
if (!req.params.post_index || (postIndex < top || postIndex > bottom)) {
|
||||
postIndex = top;
|
||||
}
|
||||
}
|
||||
const { start, stop } = calculateStartStop(currentPage, postIndex, settings);
|
||||
|
||||
topicData.posts = await notes.getTopicPosts(tid, req.uid, start, stop);
|
||||
topicData.posts = await topics.addPostData(topicData.posts, req.uid);
|
||||
|
||||
res.render('topic', topicData);
|
||||
};
|
||||
|
||||
// todo: expose from topic controller?
|
||||
function calculatePageFromIndex(postIndex, settings) {
|
||||
return 1 + Math.floor((postIndex - 1) / settings.postsPerPage);
|
||||
}
|
||||
|
||||
// todo: expose from topic controller?
|
||||
function calculateStartStop(page, postIndex, settings) {
|
||||
let startSkip = 0;
|
||||
|
||||
if (!settings.usePagination) {
|
||||
if (postIndex > 1) {
|
||||
page = 1;
|
||||
}
|
||||
startSkip = Math.max(0, postIndex - Math.ceil(settings.postsPerPage / 2));
|
||||
}
|
||||
|
||||
const start = ((page - 1) * settings.postsPerPage) + startSkip;
|
||||
const stop = start + settings.postsPerPage - 1;
|
||||
return { start: Math.max(0, start), stop: Math.max(0, stop) };
|
||||
}
|
||||
@@ -9,11 +9,14 @@ const topics = require('../topics');
|
||||
const categories = require('../categories');
|
||||
const posts = require('../posts');
|
||||
const privileges = require('../privileges');
|
||||
const activitypub = require('../activitypub');
|
||||
const helpers = require('./helpers');
|
||||
const pagination = require('../pagination');
|
||||
const utils = require('../utils');
|
||||
const analytics = require('../analytics');
|
||||
|
||||
const activitypubController = require('./activitypub');
|
||||
|
||||
const topicsController = module.exports;
|
||||
|
||||
const url = nconf.get('url');
|
||||
@@ -21,6 +24,10 @@ const relative_path = nconf.get('relative_path');
|
||||
const upload_url = nconf.get('upload_url');
|
||||
|
||||
topicsController.get = async function getTopic(req, res, next) {
|
||||
if (req.params.topic_id === 'remote' && activitypub.helpers.isUri(req.query.resource)) {
|
||||
return activitypubController.topics.get(req, res, next);
|
||||
}
|
||||
|
||||
const tid = req.params.topic_id;
|
||||
if (
|
||||
(req.params.post_index && !utils.isNumber(req.params.post_index) && req.params.post_index !== 'unread') ||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* These controllers only respond if the sender is making an json+activitypub style call (i.e. S2S-only)
|
||||
*/
|
||||
|
||||
module.exports = function (app, middleware, controllers) {
|
||||
const middlewares = [middleware.proceedOnActivityPub, middleware.exposeUid];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user