refactor: move crosspost methods into their own file in src/topics

This commit is contained in:
Julian Lam
2025-12-12 13:56:08 -05:00
parent 0041cfe2ed
commit 1be88ca0ea
5 changed files with 123 additions and 112 deletions

View File

@@ -216,14 +216,14 @@ Topics.move = async (req, res) => {
Topics.crosspost = async (req, res) => {
const { cid } = req.body;
const crossposts = await topics.tools.crosspost(req.params.tid, cid, req.uid);
const crossposts = await topics.crossposts.add(req.params.tid, cid, req.uid);
helpers.formatApiResponse(200, res, { crossposts });
};
Topics.uncrosspost = async (req, res) => {
const { cid } = req.body;
const crossposts = await topics.tools.uncrosspost(req.params.tid, cid, req.uid);
const crossposts = await topics.crossposts.remove(req.params.tid, cid, req.uid);
helpers.formatApiResponse(200, res, { crossposts });
};

115
src/topics/crossposts.js Normal file
View File

@@ -0,0 +1,115 @@
'use strict';
const db = require('../database');
const topics = require('.');
const categories = require('../categories');
const posts = require('../posts');
const activitypub = require('../activitypub');
const utils = require('../utils');
const Crossposts = module.exports;
Crossposts.get = async function (tid) {
const crosspostIds = await db.getSortedSetMembers(`tid:${tid}:crossposts`);
let crossposts = await db.getObjects(crosspostIds.map(id => `crosspost:${id}`));
crossposts = crossposts.map((crosspost, idx) => {
crosspost.id = crosspostIds[idx];
return crosspost;
});
return crossposts;
};
Crossposts.add = async function (tid, cid, uid) {
// Target cid must exist
if (!utils.isNumber(cid)) {
await activitypub.actors.assert(cid);
}
const exists = await categories.exists(cid);
if (!exists) {
throw new Error('[[error:invalid-cid]]');
}
const crossposts = await Crossposts.get(tid);
const crosspostedCids = crossposts.map(crosspost => String(crosspost.cid));
const now = Date.now();
const crosspostId = utils.generateUUID();
if (!crosspostedCids.includes(String(cid))) {
const [topicData, pids] = await Promise.all([
topics.getTopicFields(tid, ['uid', 'cid', 'timestamp']),
topics.getPids(tid),
]);
let pidTimestamps = await posts.getPostsFields(pids, ['timestamp']);
pidTimestamps = pidTimestamps.map(({ timestamp }) => timestamp);
if (cid === topicData.cid) {
throw new Error('[[error:invalid-cid]]');
}
const zsets = [
`cid:${topicData.cid}:tids`,
`cid:${topicData.cid}:tids:create`,
`cid:${topicData.cid}:tids:lastposttime`,
`cid:${topicData.cid}:uid:${topicData.uid}:tids`,
`cid:${topicData.cid}:tids:votes`,
`cid:${topicData.cid}:tids:posts`,
`cid:${topicData.cid}:tids:views`,
];
const scores = await db.sortedSetsScore(zsets, tid);
const bulkAdd = zsets.map((zset, idx) => {
return [zset.replace(`cid:${topicData.cid}`, `cid:${cid}`), scores[idx], tid];
});
await Promise.all([
db.sortedSetAddBulk(bulkAdd),
db.sortedSetAdd(`cid:${cid}:pids`, pidTimestamps, pids),
db.setObject(`crosspost:${crosspostId}`, { uid, tid, cid, timestamp: now }),
db.sortedSetAdd(`tid:${tid}:crossposts`, now, crosspostId),
db.sortedSetAdd(`uid:${uid}:crossposts`, now, crosspostId),
]);
await categories.onTopicsMoved([cid]);
} else {
throw new Error('[[error:topic-already-crossposted]]');
}
return [...crossposts, { id: crosspostId, uid, tid, cid, timestamp: now }];
};
Crossposts.remove = async function (tid, cid, uid) {
let crossposts = await Crossposts.get(tid);
const crosspostId = crossposts.reduce((id, { id: _id, cid: _cid, uid: _uid }) => {
if (String(cid) === String(_cid) && String(uid) === String(_uid)) {
id = _id;
}
return id;
}, null);
if (!crosspostId) {
throw new Error('[[error:invalid-data]]');
}
const [author, pids] = await Promise.all([
topics.getTopicField(tid, 'uid'),
topics.getPids(tid),
]);
let bulkRemove = [
`cid:${cid}:tids`,
`cid:${cid}:tids:create`,
`cid:${cid}:tids:lastposttime`,
`cid:${cid}:uid:${author}:tids`,
`cid:${cid}:tids:votes`,
`cid:${cid}:tids:posts`,
`cid:${cid}:tids:views`,
];
bulkRemove = bulkRemove.map(zset => [zset, tid]);
bulkRemove.push([`cid:${cid}:pids`, pids]);
await Promise.all([
db.sortedSetRemoveBulk(bulkRemove),
db.delete(`crosspost:${crosspostId}`),
db.sortedSetRemove(`tid:${tid}:crossposts`, crosspostId),
db.sortedSetRemove(`uid:${uid}:crossposts`, crosspostId),
]);
await categories.onTopicsMoved([cid]);
crossposts = await Crossposts.get(tid);
return crossposts;
};

View File

@@ -35,6 +35,7 @@ Topics.thumbs = require('./thumbs');
require('./bookmarks')(Topics);
require('./merge')(Topics);
Topics.events = require('./events');
Topics.crossposts = require('./crossposts');
Topics.exists = async function (tids) {
return await db.exists(

View File

@@ -317,109 +317,4 @@ module.exports = function (Topics) {
db.sortedSetAdd(set, timestamp, tid),
]);
};
async function getCrossposts(tid) {
const crosspostIds = await db.getSortedSetMembers(`tid:${tid}:crossposts`);
let crossposts = await db.getObjects(crosspostIds.map(id => `crosspost:${id}`));
crossposts = crossposts.map((crosspost, idx) => {
crosspost.id = crosspostIds[idx];
return crosspost;
});
return crossposts;
}
topicTools.crosspost = async function (tid, cid, uid) {
// Target cid must exist
if (!utils.isNumber(cid)) {
await activitypub.actors.assert(cid);
}
const exists = await categories.exists(cid);
if (!exists) {
throw new Error('[[error:invalid-cid]]');
}
const crossposts = await getCrossposts(tid);
const crosspostedCids = crossposts.map(crosspost => String(crosspost.cid));
const now = Date.now();
const crosspostId = utils.generateUUID();
if (!crosspostedCids.includes(String(cid))) {
const [topicData, pids] = await Promise.all([
topics.getTopicFields(tid, ['uid', 'cid', 'timestamp']),
topics.getPids(tid),
]);
let pidTimestamps = await posts.getPostsFields(pids, ['timestamp']);
pidTimestamps = pidTimestamps.map(({ timestamp }) => timestamp);
if (cid === topicData.cid) {
throw new Error('[[error:invalid-cid]]');
}
const zsets = [
`cid:${topicData.cid}:tids`,
`cid:${topicData.cid}:tids:create`,
`cid:${topicData.cid}:tids:lastposttime`,
`cid:${topicData.cid}:uid:${topicData.uid}:tids`,
`cid:${topicData.cid}:tids:votes`,
`cid:${topicData.cid}:tids:posts`,
`cid:${topicData.cid}:tids:views`,
];
const scores = await db.sortedSetsScore(zsets, tid);
const bulkAdd = zsets.map((zset, idx) => {
return [zset.replace(`cid:${topicData.cid}`, `cid:${cid}`), scores[idx], tid];
});
await Promise.all([
db.sortedSetAddBulk(bulkAdd),
db.sortedSetAdd(`cid:${cid}:pids`, pidTimestamps, pids),
db.setObject(`crosspost:${crosspostId}`, { uid, tid, cid, timestamp: now }),
db.sortedSetAdd(`tid:${tid}:crossposts`, now, crosspostId),
db.sortedSetAdd(`uid:${uid}:crossposts`, now, crosspostId),
]);
await categories.onTopicsMoved([cid]);
} else {
throw new Error('[[error:topic-already-crossposted]]');
}
return [...crossposts, { id: crosspostId, uid, tid, cid, timestamp: now }];
};
topicTools.uncrosspost = async function (tid, cid, uid) {
let crossposts = await getCrossposts(tid);
const crosspostId = crossposts.reduce((id, { id: _id, cid: _cid, uid: _uid }) => {
if (String(cid) === String(_cid) && String(uid) === String(_uid)) {
id = _id;
}
return id;
}, null);
if (!crosspostId) {
throw new Error('[[error:invalid-data]]');
}
const [author, pids] = await Promise.all([
topics.getTopicField(tid, 'uid'),
topics.getPids(tid),
]);
let bulkRemove = [
`cid:${cid}:tids`,
`cid:${cid}:tids:create`,
`cid:${cid}:tids:lastposttime`,
`cid:${cid}:uid:${author}:tids`,
`cid:${cid}:tids:votes`,
`cid:${cid}:tids:posts`,
`cid:${cid}:tids:views`,
];
bulkRemove = bulkRemove.map(zset => [zset, tid]);
bulkRemove.push([`cid:${cid}:pids`, pids]);
await Promise.all([
db.sortedSetRemoveBulk(bulkRemove),
db.delete(`crosspost:${crosspostId}`),
db.sortedSetRemove(`tid:${tid}:crossposts`, crosspostId),
db.sortedSetRemove(`uid:${uid}:crossposts`, crosspostId),
]);
await categories.onTopicsMoved([cid]);
crossposts = await getCrossposts(tid);
return crossposts;
};
};

View File

@@ -73,7 +73,7 @@ describe('Crossposting (& related logic)', () => {
});
it('should successfully crosspost to another cid', async () => {
const crossposts = await topics.tools.crosspost(tid, cid2, uid);
const crossposts = await topics.crossposts.add(tid, cid2, uid);
assert(Array.isArray(crossposts));
assert.strictEqual(crossposts.length, 1);
@@ -104,7 +104,7 @@ describe('Crossposting (& related logic)', () => {
it('should throw on cross-posting again when already cross-posted', async () => {
await assert.rejects(
topics.tools.crosspost(tid, cid2, uid),
topics.crossposts.add(tid, cid2, uid),
{ message: '[[error:topic-already-crossposted]]' },
);
});
@@ -129,11 +129,11 @@ describe('Crossposting (& related logic)', () => {
});
tid = topicData.tid;
await topics.tools.crosspost(tid, cid2, uid);
await topics.crossposts.add(tid, cid2, uid);
});
it('should successfully uncrosspost from a cid', async () => {
const crossposts = await topics.tools.uncrosspost(tid, cid2, uid);
const crossposts = await topics.crossposts.remove(tid, cid2, uid);
assert(Array.isArray(crossposts));
assert.strictEqual(crossposts.length, 0);
@@ -152,7 +152,7 @@ describe('Crossposting (& related logic)', () => {
it('should throw on uncrossposting if already uncrossposted', async () => {
assert.rejects(
topics.tools.uncrosspost(tid, cid2, uid),
topics.crossposts.remove(tid, cid2, uid),
'[[error:invalid-data]]',
);
});