From 3301fcf45dbec43e672dae382ce3c2a62bda8923 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 12 Dec 2025 13:56:08 -0500 Subject: [PATCH] refactor: move crosspost methods into their own file in src/topics --- src/controllers/write/topics.js | 4 +- src/topics/crossposts.js | 115 ++++++++++++++++++++++++++++++++ src/topics/index.js | 1 + src/topics/tools.js | 105 ----------------------------- test/topics/crossposts.js | 10 +-- 5 files changed, 123 insertions(+), 112 deletions(-) create mode 100644 src/topics/crossposts.js diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index 16270d75d3..2dea5dff5d 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -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 }); }; diff --git a/src/topics/crossposts.js b/src/topics/crossposts.js new file mode 100644 index 0000000000..88b6c80675 --- /dev/null +++ b/src/topics/crossposts.js @@ -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; +}; \ No newline at end of file diff --git a/src/topics/index.js b/src/topics/index.js index 871d010dcf..0f9c067535 100644 --- a/src/topics/index.js +++ b/src/topics/index.js @@ -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( diff --git a/src/topics/tools.js b/src/topics/tools.js index b9fdde671f..8fb0474788 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -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; - }; }; diff --git a/test/topics/crossposts.js b/test/topics/crossposts.js index 809821f97e..e0db6772ac 100644 --- a/test/topics/crossposts.js +++ b/test/topics/crossposts.js @@ -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]]', ); });