refactor: allow passing an array to topics.purge and topics.purgePost… (#14018)

* refactor: allow passing an array to topics.purge and topics.purgePostsAndTopic

deprecate action:topic.purge, add action:topics.purge with array of deleted topics
update usage of topics.purge to pass in an array
fix an issue in posts/delete where cids were passed to parseInt, caused AP cids to get saved into category:NaN

* lint

* refactor: change style

* use array of tids
This commit is contained in:
Barış Uşaklı
2026-02-26 13:09:42 -05:00
committed by GitHub
parent b460506e4d
commit e4c945f636
14 changed files with 269 additions and 115 deletions

View File

@@ -748,11 +748,11 @@ Mocks.notes.public = async (post) => {
if (isArticle) { if (isArticle) {
// Preview is not adopted by anybody, so is left commented-out for now // Preview is not adopted by anybody, so is left commented-out for now
// preview = { // preview = {
// type: 'Note', // type: 'Note',
// attributedTo: `${nconf.get('url')}/uid/${post.user.uid}`, // attributedTo: `${nconf.get('url')}/uid/${post.user.uid}`,
// content: post.content, // content: post.content,
// published, // published,
// attachment, // attachment,
// }; // };
const breakString = '[...]'; const breakString = '[...]';

View File

@@ -786,7 +786,7 @@ async function pruneCidTids(cid, cuttoff) {
winston.info(`[notes/prune] ${tidsWithNoEngagement.length} topics eligible in cid:${cid} for pruning`); winston.info(`[notes/prune] ${tidsWithNoEngagement.length} topics eligible in cid:${cid} for pruning`);
await batch.processArray(tidsWithNoEngagement, async (tids) => { await batch.processArray(tidsWithNoEngagement, async (tids) => {
await Promise.all(tids.map(async tid => await topics.purgePostsAndTopic(tid, 0))); await topics.purgePostsAndTopic(tids, 0);
}, { batch: 100 }); }, { batch: 100 });
} }

View File

@@ -87,11 +87,21 @@ module.exports = function (Categories) {
}; };
Categories.setCategoryField = async function (cid, field, value) { Categories.setCategoryField = async function (cid, field, value) {
await db.setObjectField(`${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`, field, value); await db.setObjectField(
utils.isNumber(cid) ? `category:${cid}` : `categoryRemote:${cid}`,
field, value
);
};
Categories.setCategoryFields = async function (cid, fields) {
await db.setObject(
utils.isNumber(cid) ? `category:${cid}` : `categoryRemote:${cid}`,
fields
);
}; };
Categories.incrementCategoryFieldBy = async function (cid, field, value) { Categories.incrementCategoryFieldBy = async function (cid, field, value) {
await db.incrObjectFieldBy(`${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`, field, value); await db.incrObjectFieldBy(utils.isNumber(cid) ? `category:${cid}` : `categoryRemote:${cid}`, field, value);
}; };
}; };

View File

@@ -1,6 +1,5 @@
'use strict'; 'use strict';
const async = require('async');
const db = require('../database'); const db = require('../database');
const batch = require('../batch'); const batch = require('../batch');
const plugins = require('../plugins'); const plugins = require('../plugins');
@@ -14,17 +13,14 @@ const utils = require('../utils');
module.exports = function (Categories) { module.exports = function (Categories) {
Categories.purge = async function (cid, uid) { Categories.purge = async function (cid, uid) {
await batch.processSortedSet(`cid:${cid}:tids`, async (tids) => { await batch.processSortedSet(`cid:${cid}:tids`, async (tids) => {
await async.eachLimit(tids, 10, async (tid) => { await topics.purgePostsAndTopic(tids, uid);
await topics.purgePostsAndTopic(tid, uid);
});
await db.sortedSetRemove(`cid:${cid}:tids`, tids); await db.sortedSetRemove(`cid:${cid}:tids`, tids);
}, { alwaysStartAt: 0 }); }, { alwaysStartAt: 0 });
const pinnedTids = await db.getSortedSetRevRange(`cid:${cid}:tids:pinned`, 0, -1); const pinnedTids = await db.getSortedSetRevRange(`cid:${cid}:tids:pinned`, 0, -1);
await async.eachLimit(pinnedTids, 10, async (tid) => { await topics.purgePostsAndTopic(pinnedTids, uid);
await topics.purgePostsAndTopic(tid, uid);
});
await db.sortedSetRemove(`cid:${cid}:tids:pinned`, pinnedTids); await db.sortedSetRemove(`cid:${cid}:tids:pinned`, pinnedTids);
const categoryData = await Categories.getCategoryData(cid); const categoryData = await Categories.getCategoryData(cid);
await purgeCategory(cid, categoryData); await purgeCategory(cid, categoryData);
plugins.hooks.fire('action:category.delete', { cid: cid, uid: uid, category: categoryData }); plugins.hooks.fire('action:category.delete', { cid: cid, uid: uid, category: categoryData });

View File

@@ -198,13 +198,15 @@ module.exports = function (Categories) {
Categories.onTopicsMoved = async (cids) => { Categories.onTopicsMoved = async (cids) => {
await Promise.all(cids.map(async (cid) => { await Promise.all(cids.map(async (cid) => {
const [topicCount, postCount] = await db.sortedSetsCard([
`cid:${cid}:tids:lastposttime`,
`cid:${cid}:pids`,
]);
await Promise.all([ await Promise.all([
Categories.setCategoryField( Categories.setCategoryFields(cid, {
cid, 'topic_count', await db.sortedSetCard(`cid:${cid}:tids:lastposttime`) topic_count: topicCount,
), post_count: postCount,
Categories.setCategoryField( }),
cid, 'post_count', await db.sortedSetCard(`cid:${cid}:pids`)
),
Categories.updateRecentTidForCid(cid), Categories.updateRecentTidForCid(cid),
]); ]);
})); }));

View File

@@ -13,6 +13,11 @@ Hooks._deprecated = new Map([
since: 'v4.0.0', since: 'v4.0.0',
until: 'v5.0.0', until: 'v5.0.0',
}], */ }], */
['action:topic.purge', {
new: 'action:topics.purge',
since: 'v4.9.0',
until: 'v5.0.0',
}],
]); ]);
Hooks.internals = { Hooks.internals = {

View File

@@ -108,9 +108,12 @@ module.exports = function (Posts) {
const localCount = postData.filter(p => utils.isNumber(p.pid)).length; const localCount = postData.filter(p => utils.isNumber(p.pid)).length;
const incrObjectBulk = [['global', { postCount: -localCount }]]; const incrObjectBulk = [['global', { postCount: -localCount }]];
const postsByCategory = _.groupBy(postData, p => parseInt(p.cid, 10)); const postsByCategory = _.groupBy(postData, p => String(p.cid));
for (const [cid, posts] of Object.entries(postsByCategory)) { for (const [cid, posts] of Object.entries(postsByCategory)) {
incrObjectBulk.push([`category:${cid}`, { post_count: -posts.length }]); incrObjectBulk.push([
utils.isNumber(cid) ? `category:${cid}` : `categoryRemote:${cid}`,
{ post_count: -posts.length },
]);
} }
const postsByTopic = _.groupBy(postData, p => String(p.tid)); const postsByTopic = _.groupBy(postData, p => String(p.tid));

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
const winston = require('winston');
const _ = require('lodash'); const _ = require('lodash');
const db = require('../database'); const db = require('../database');
const topics = require('.'); const topics = require('.');
@@ -117,8 +118,10 @@ Crossposts.add = async function (tid, cid, uid) {
Crossposts.remove = async function (tid, cid, uid) { Crossposts.remove = async function (tid, cid, uid) {
let crossposts = await Crossposts.get(tid); let crossposts = await Crossposts.get(tid);
const isPrivileged = await user.isAdminOrGlobalMod(uid); const [isPrivileged, isMod] = await Promise.all([
const isMod = await user.isModerator(uid, cid); user.isAdminOrGlobalMod(uid),
user.isModerator(uid, cid),
]);
const crosspostId = crossposts.reduce((id, { id: _id, cid: _cid, uid: _uid }) => { const crosspostId = crossposts.reduce((id, { id: _id, cid: _cid, uid: _uid }) => {
if (String(cid) === String(_cid) && (isPrivileged || isMod || String(uid) === String(_uid))) { if (String(cid) === String(_cid) && (isPrivileged || isMod || String(uid) === String(_uid))) {
id = _id; id = _id;
@@ -156,18 +159,24 @@ Crossposts.remove = async function (tid, cid, uid) {
topics.events.find(tid, { uid, toCid: cid, type: 'crosspost' }).then((eventIds) => { topics.events.find(tid, { uid, toCid: cid, type: 'crosspost' }).then((eventIds) => {
topics.events.purge(tid, eventIds); topics.events.purge(tid, eventIds);
}); }).catch(err => winston.error(err));
crossposts = await Crossposts.get(tid); crossposts = await Crossposts.get(tid);
return crossposts; return crossposts;
}; };
Crossposts.removeAll = async function (tid) { Crossposts.removeAll = async function (tids) {
const crosspostIds = await db.getSortedSetMembers(`tid:${tid}:crossposts`); if (!Array.isArray(tids)) {
const crossposts = await db.getObjects(crosspostIds.map(id => `crosspost:${id}`)); tids = [tids];
await Promise.all(crossposts.map(async ({ tid, cid, uid }) => { }
return Crossposts.remove(tid, cid, uid); const allCrosspostIds = (await db.getSortedSetsMembers(
})); tids.map(tid => `tid:${tid}:crossposts`)
)).flat();
const crossposts = (await db.getObjects(
allCrosspostIds.map(id => `crosspost:${id}`)
)).filter(Boolean);
return []; await Promise.all(
crossposts.map(({ tid, cid, uid }) => Crossposts.remove(tid, cid, uid))
);
}; };

View File

@@ -1,7 +1,8 @@
'use strict'; 'use strict';
const db = require('../database'); const _ = require('lodash');
const db = require('../database');
const user = require('../user'); const user = require('../user');
const posts = require('../posts'); const posts = require('../posts');
const categories = require('../categories'); const categories = require('../categories');
@@ -62,90 +63,189 @@ module.exports = function (Topics) {
await categories.updateRecentTidForCid(cid); await categories.updateRecentTidForCid(cid);
}; };
Topics.purgePostsAndTopic = async function (tid, uid) { Topics.purgePostsAndTopic = async function (tids, uid) {
const mainPid = await Topics.getTopicField(tid, 'mainPid'); if (!Array.isArray(tids)) {
await batch.processSortedSet(`tid:${tid}:posts`, async (pids) => { tids = [tids];
await posts.purge(pids, uid); }
await db.sortedSetRemove(`tid:${tid}:posts`, pids); // Guard against infinite loop if pid already does not exist in db let topicData = await Topics.getTopicsFields(tids, ['tid', 'mainPid']);
}, { alwaysStartAt: 0, batch: 500 }); topicData = topicData.filter(t => t && t.tid);
await posts.purge(mainPid, uid); const tidsToDelete = topicData.map(t => t.tid);
await Topics.purge(tid, uid);
await Promise.all(tidsToDelete.map(async (tid) => {
await batch.processSortedSet(`tid:${tid}:posts`, async (pids) => {
await posts.purge(pids, uid);
await db.sortedSetRemove(`tid:${tid}:posts`, pids); // Guard against infinite loop if pid already does not exist in db
}, { alwaysStartAt: 0, batch: 500 });
}));
await posts.purge(topicData.map(t => t.mainPid), uid);
await Topics.purge(tidsToDelete, uid);
}; };
Topics.purge = async function (tid, uid) { Topics.purge = async function (tids, uid) {
const deletedTopic = await Topics.getTopicData(tid); if (!Array.isArray(tids)) {
if (!deletedTopic) { tids = [tids];
}
const deletedTopics = (await Topics.getTopicsData(tids)).filter(Boolean);
if (!deletedTopics.length) {
return; return;
} }
deletedTopic.tags = deletedTopic.tags.map(tag => tag.value); const tidsToDelete = deletedTopics.map(t => t.tid);
await deleteFromFollowersIgnorers(tid); deletedTopics.forEach((t) => {
t.tags = t.tags.map(tag => tag.value);
});
await deleteFromFollowersIgnorers(tidsToDelete);
const remoteTids = [];
const localTids = [];
for (const tid of tidsToDelete) {
if (utils.isNumber(tid)) {
localTids.push(tid);
} else {
remoteTids.push(tid);
}
}
await Promise.all([ await Promise.all([
db.deleteAll([ deleteKeys(tidsToDelete),
`tid:${tid}:followers`,
`tid:${tid}:ignorers`,
`tid:${tid}:posts`,
`tid:${tid}:posts:votes`,
`tid:${tid}:bookmarks`,
`tid:${tid}:posters`,
]),
db.sortedSetsRemove([ db.sortedSetsRemove([
utils.isNumber(tid) ? 'topics:tid' : 'topicsRemote:tid',
'topics:recent', 'topics:recent',
'topics:scheduled', 'topics:scheduled',
], tid), ], tidsToDelete),
db.sortedSetsRemove(['views', 'posts', 'votes'].map(prop => `${utils.isNumber(tid) ? 'topics' : 'topicsRemote'}:${prop}`), tid), db.sortedSetsRemove([
deleteTopicFromCategoryAndUser(tid), 'topics:tid',
Topics.deleteTopicTags(tid), 'topics:views',
Topics.events.purge(tid), 'topics:posts',
Topics.thumbs.deleteAll(tid), 'topics:votes',
Topics.crossposts.removeAll(tid), ], localTids),
reduceCounters(tid), db.sortedSetsRemove([
'topicsRemote:tid',
'topicsRemote:views',
'topicsRemote:posts',
'topicsRemote:votes',
], remoteTids),
deleteTopicsFromCategoryAndUser(deletedTopics),
deleteFromTags(deletedTopics),
Topics.events.purge(tidsToDelete),
Topics.crossposts.removeAll(tidsToDelete),
reduceCounters(deletedTopics),
]); ]);
plugins.hooks.fire('action:topic.purge', { topic: deletedTopic, uid: uid });
await db.delete(`topic:${tid}`); // DEPRECATED hook
deletedTopics.forEach((topic) => {
plugins.hooks.fire('action:topic.purge', { topic, uid });
});
// new hook
plugins.hooks.fire('action:topics.purge', { topics: deletedTopics, uid });
await db.deleteAll(tids.map(tid => `topic:${tid}`));
}; };
async function deleteFromFollowersIgnorers(tid) { async function deleteFromFollowersIgnorers(tids) {
const [followers, ignorers] = await Promise.all([ const [followers, ignorers] = await Promise.all([
db.getSetMembers(`tid:${tid}:followers`), db.getSetsMembers(tids.map(tid => `tid:${tid}:followers`)),
db.getSetMembers(`tid:${tid}:ignorers`), db.getSetsMembers(tids.map(tid => `tid:${tid}:ignorers`)),
]); ]);
const followerKeys = followers.map(uid => `uid:${uid}:followed_tids`); const bulkRemove = [];
const ignorerKeys = ignorers.map(uid => `uid:${uid}ignored_tids`); tids.forEach((tid, index) => {
await db.sortedSetsRemove(followerKeys.concat(ignorerKeys), tid); followers[index].forEach((uid) => {
bulkRemove.push([`uid:${uid}:followed_tids`, tid]);
});
ignorers[index].forEach((uid) => {
bulkRemove.push([`uid:${uid}:followed_tids`, tid]);
});
});
await db.sortedSetRemoveBulk(bulkRemove);
} }
async function deleteTopicFromCategoryAndUser(tid) { async function deleteKeys(tids) {
const topicData = await Topics.getTopicFields(tid, ['cid', 'uid']); await db.deleteAll([
await Promise.all([ ...tids.map(tid => `tid:${tid}:followers`),
db.sortedSetsRemove([ ...tids.map(tid => `tid:${tid}:ignorers`),
`cid:${topicData.cid}:tids`, ...tids.map(tid => `tid:${tid}:posts`),
`cid:${topicData.cid}:tids:pinned`, ...tids.map(tid => `tid:${tid}:posts:votes`),
`cid:${topicData.cid}:tids:create`, ...tids.map(tid => `tid:${tid}:bookmarks`),
`cid:${topicData.cid}:tids:posts`, ...tids.map(tid => `tid:${tid}:posters`),
`cid:${topicData.cid}:tids:lastposttime`,
`cid:${topicData.cid}:tids:votes`,
`cid:${topicData.cid}:tids:views`,
`cid:${topicData.cid}:recent_tids`,
`cid:${topicData.cid}:uid:${topicData.uid}:tids`,
`uid:${topicData.uid}:topics`,
], tid),
user.decrementUserFieldBy(topicData.uid, 'topiccount', 1),
]); ]);
await categories.updateRecentTidForCid(topicData.cid);
} }
async function reduceCounters(tid) { async function deleteTopicsFromCategoryAndUser(topicsData) {
const incr = -1; const bulkRemove = [];
const { cid, postcount } = await Topics.getTopicFields(tid, ['cid', 'postcount']); for (const topic of topicsData) {
const postCountChange = incr * postcount; bulkRemove.push([`cid:${topic.cid}:tids`, topic.tid]);
const categoryKey = `${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`; bulkRemove.push([`cid:${topic.cid}:tids:pinned`, topic.tid]);
const bulkIncr = [ bulkRemove.push([`cid:${topic.cid}:tids:create`, topic.tid]);
[categoryKey, { post_count: postCountChange, topic_count: incr }], bulkRemove.push([`cid:${topic.cid}:tids:posts`, topic.tid]);
]; bulkRemove.push([`cid:${topic.cid}:tids:lastposttime`, topic.tid]);
if (utils.isNumber(tid)) { bulkRemove.push([`cid:${topic.cid}:tids:votes`, topic.tid]);
bulkIncr.push(['global', { postCount: postCountChange, topicCount: incr }]); bulkRemove.push([`cid:${topic.cid}:tids:views`, topic.tid]);
bulkRemove.push([`cid:${topic.cid}:recent_tids`, topic.tid]);
bulkRemove.push([`cid:${topic.cid}:uid:${topic.uid}:tids`, topic.tid]);
bulkRemove.push([`uid:${topic.uid}:topics`, topic.tid]);
}
await db.sortedSetRemoveBulk(bulkRemove);
const uniqCids = new Set();
const uniqUids = new Set();
topicsData.forEach((t) => {
uniqCids.add(String(t.cid));
uniqUids.add(String(t.uid));
});
await user.updateTopicCount(Array.from(uniqUids));
await Promise.all(Array.from(uniqCids).map(cid => categories.updateRecentTidForCid(cid)));
}
async function deleteFromTags(topicsData) {
const bulkRemove = [];
const uniqCids = new Set();
const uniqTags = new Set();
for (const topic of topicsData) {
for (const tag of topic.tags) {
bulkRemove.push([`tag:${tag}:topics`, topic.tid]);
bulkRemove.push([`cid:${topic.cid}:tag:${tag}:topics`, topic.tid]);
uniqTags.add(tag);
}
uniqCids.add(String(topic.cid));
}
await db.sortedSetRemoveBulk(bulkRemove);
await Topics.updateCategoryTagsCount(
Array.from(uniqCids),
Array.from(uniqTags)
);
await Topics.updateTagCount(uniqTags);
}
async function reduceCounters(topicsData) {
const bulkIncr = [];
let globalPostCountChange = 0;
let globalTopicCountChange = 0;
const topicsByCid = _.groupBy(topicsData, t => String(t.cid));
for (const [cid, topics] of Object.entries(topicsByCid)) {
const cidPostCountChange = Math.max(0, topics.reduce((acc, t) => acc + t.postcount, 0));
const categoryKey = `${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`;
bulkIncr.push([
categoryKey, { post_count: -cidPostCountChange, topic_count: -topics.length },
]);
for (const topic of topics) {
if (utils.isNumber(topic.tid)) {
globalPostCountChange += topic.postcount;
globalTopicCountChange += 1;
}
}
}
if (globalPostCountChange || globalTopicCountChange) {
bulkIncr.push([
'global', { postCount: -globalPostCountChange, topicCount: -globalTopicCountChange },
]);
} }
await db.incrObjectFieldByBulk(bulkIncr); await db.incrObjectFieldByBulk(bulkIncr);
} }

View File

@@ -272,7 +272,11 @@ Events.log = async (tid, payload) => {
}; };
Events.purge = async (tid, eventIds = []) => { Events.purge = async (tid, eventIds = []) => {
if (eventIds.length) { const isArray = Array.isArray(tid);
if (isArray && !tid.length) {
return;
}
if (eventIds.length && !isArray) {
const isTopicEvent = await db.isSortedSetMembers(`topic:${tid}:events`, eventIds); const isTopicEvent = await db.isSortedSetMembers(`topic:${tid}:events`, eventIds);
eventIds = eventIds.filter((id, index) => isTopicEvent[index]); eventIds = eventIds.filter((id, index) => isTopicEvent[index]);
await Promise.all([ await Promise.all([
@@ -280,8 +284,11 @@ Events.purge = async (tid, eventIds = []) => {
db.deleteAll(eventIds.map(id => `topicEvent:${id}`)), db.deleteAll(eventIds.map(id => `topicEvent:${id}`)),
]); ]);
} else { } else {
const keys = [`topic:${tid}:events`]; if (!isArray) {
const eventIds = await db.getSortedSetRange(keys[0], 0, -1); tid = [tid];
}
const keys = tid.map(tid => `topic:${tid}:events`);
const eventIds = await db.getSortedSetRange(keys, 0, -1);
keys.push(...eventIds.map(id => `topicEvent:${id}`)); keys.push(...eventIds.map(id => `topicEvent:${id}`));
await db.deleteAll(keys); await db.deleteAll(keys);

View File

@@ -29,7 +29,7 @@ module.exports = function (Topics) {
); );
await db.sortedSetsAdd(topicSets, timestamp, tid); await db.sortedSetsAdd(topicSets, timestamp, tid);
await Topics.updateCategoryTagsCount([cid], tags); await Topics.updateCategoryTagsCount([cid], tags);
await Promise.all(tags.map(updateTagCount)); await updateTagCount(tags);
}; };
Topics.filterTags = async function (tags, cid) { Topics.filterTags = async function (tags, cid) {
@@ -185,11 +185,21 @@ module.exports = function (Topics) {
await Topics.updateCategoryTagsCount(Object.keys(allCids), [newTagName]); await Topics.updateCategoryTagsCount(Object.keys(allCids), [newTagName]);
} }
async function updateTagCount(tag) { async function updateTagCount(tags) {
const count = await Topics.getTagTopicCount(tag); if (!Array.isArray(tags)) {
await db.sortedSetAdd('tags:topic:count', count || 0, tag); tags = [tags];
}
if (!tags.length) return;
const counts = await Promise.all(tags.map(tag => Topics.getTagTopicCount(tag)));
await db.sortedSetAdd(
'tags:topic:count',
tags.map((tag, index) => counts[index] || 0),
tags
);
cache.del('tags:topic:count'); cache.del('tags:topic:count');
} }
Topics.updateTagCount = updateTagCount;
Topics.getTagTids = async function (tag, start, stop) { Topics.getTagTids = async function (tag, start, stop) {
const tids = await db.getSortedSetRevRange(`tag:${tag}:topics`, start, stop); const tids = await db.getSortedSetRevRange(`tag:${tag}:topics`, start, stop);
@@ -381,7 +391,7 @@ module.exports = function (Topics) {
db.setObjectBulk(bulkSet), db.setObjectBulk(bulkSet),
]); ]);
await Promise.all(tags.map(updateTagCount)); await updateTagCount(tags);
await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags); await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
}; };
@@ -406,7 +416,7 @@ module.exports = function (Topics) {
db.setObjectBulk(bulkSet), db.setObjectBulk(bulkSet),
]); ]);
await Promise.all(tags.map(updateTagCount)); await updateTagCount(tags);
await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags); await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
}; };
@@ -430,7 +440,7 @@ module.exports = function (Topics) {
await db.sortedSetsRemove(sets, tid); await db.sortedSetsRemove(sets, tid);
await Topics.updateCategoryTagsCount([cid], tags); await Topics.updateCategoryTagsCount([cid], tags);
await Promise.all(tags.map(updateTagCount)); await updateTagCount(tags);
}; };
Topics.searchTags = async function (data) { Topics.searchTags = async function (data) {

View File

@@ -49,9 +49,7 @@ module.exports = function (User) {
async function deleteTopics(callerUid, uid) { async function deleteTopics(callerUid, uid) {
await batch.processSortedSet(`uid:${uid}:topics`, async (tids) => { await batch.processSortedSet(`uid:${uid}:topics`, async (tids) => {
await async.eachSeries(tids, async (tid) => { await topics.purge(tids, callerUid);
await topics.purge(tid, callerUid);
});
await db.sortedSetRemove(`uid:${uid}:topics`, tids); await db.sortedSetRemove(`uid:${uid}:topics`, tids);
}, { alwaysStartAt: 0, batch: 100 }); }, { alwaysStartAt: 0, batch: 100 });
} }

View File

@@ -111,12 +111,26 @@ module.exports = function (User) {
if (uids.length) { if (uids.length) {
const counts = await db.sortedSetsCard(uids.map(uid => `uid:${uid}:posts`)); const counts = await db.sortedSetsCard(uids.map(uid => `uid:${uid}:posts`));
await Promise.all([ await Promise.all([
db.setObjectBulk(uids.map((uid, index) => ([`user${activitypub.helpers.isUri(uid) ? 'Remote' : ''}:${uid}`, { postcount: counts[index] }]))), db.setObjectBulk(
uids.map((uid, index) => ([activitypub.helpers.isUri(uid) ? `userRemote:${uid}` : `user:${uid}`, { postcount: counts[index] }]))
),
db.sortedSetAdd('users:postcount', counts, uids), db.sortedSetAdd('users:postcount', counts, uids),
]); ]);
} }
}; };
User.updateTopicCount = async (uids) => {
uids = Array.isArray(uids) ? uids : [uids];
const exists = await User.exists(uids);
uids = uids.filter((uid, index) => exists[index]);
if (uids.length) {
const counts = await db.sortedSetsCard(uids.map(uid => `uid:${uid}:topics`));
await db.setObjectBulk(
uids.map((uid, index) => ([activitypub.helpers.isUri(uid) ? `userRemote:${uid}` : `user:${uid}`, { topiccount: counts[index] }]))
);
}
};
User.incrementUserPostCountBy = async function (uid, value) { User.incrementUserPostCountBy = async function (uid, value) {
return await incrementUserFieldAndSetBy(uid, 'postcount', 'users:postcount', value); return await incrementUserFieldAndSetBy(uid, 'postcount', 'users:postcount', value);
}; };

View File

@@ -2342,7 +2342,7 @@ describe('Topic\'s', () => {
}); });
it('should create a scheduled topic as pinned, deleted, included in "topics:scheduled" zset and with a timestamp in future', async () => { it('should create a scheduled topic as pinned, deleted, included in "topics:scheduled" zset and with a timestamp in future', async () => {
topicData = (await topics.post(topic)).topicData; topicData = (await topics.post({ ...topic })).topicData;
topicData = await topics.getTopicData(topicData.tid); topicData = await topics.getTopicData(topicData.tid);
assert(topicData.pinned); assert(topicData.pinned);
@@ -2499,7 +2499,7 @@ describe('Topic\'s', () => {
}); });
it('should allow to purge a scheduled topic', async () => { it('should allow to purge a scheduled topic', async () => {
topicData = (await topics.post(topic)).topicData; const { topicData } = await topics.post({ ...topic });
const { response } = await request.delete(`${nconf.get('url')}/api/v3/topics/${topicData.tid}`, adminApiOpts); const { response } = await request.delete(`${nconf.get('url')}/api/v3/topics/${topicData.tid}`, adminApiOpts);
assert.strictEqual(response.statusCode, 200); assert.strictEqual(response.statusCode, 200);
}); });