2014-03-21 17:04:15 -04:00
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
2021-02-04 00:06:15 -07:00
|
|
|
const _ = require('lodash');
|
|
|
|
|
const validator = require('validator');
|
2021-09-29 12:26:15 -04:00
|
|
|
const nconf = require('nconf');
|
2014-03-21 17:04:15 -04:00
|
|
|
|
2021-02-04 00:06:15 -07:00
|
|
|
const db = require('../database');
|
|
|
|
|
const user = require('../user');
|
|
|
|
|
const posts = require('../posts');
|
|
|
|
|
const meta = require('../meta');
|
2024-01-10 20:49:27 -05:00
|
|
|
const activitypub = require('../activitypub');
|
2021-02-04 00:06:15 -07:00
|
|
|
const plugins = require('../plugins');
|
Webpack5 (#10311)
* feat: webpack 5 part 1
* fix: gruntfile fixes
* fix: fix taskbar warning
add app.importScript
copy public/src/modules to build folder
* refactor: remove commented old code
* feat: reenable admin
* fix: acp settings pages, fix sortable on manage categories
embedded require in html not allowed
* fix: bundle serialize/deserizeli so plugins dont break
* test: fixe util tests
* test: fix require path
* test: more test fixes
* test: require correct utils module
* test: require correct utils
* test: log stack
* test: fix db require blowing up tests
* test: move and disable bundle test
* refactor: add aliases
* test: disable testing route
* fix: move webpack modules necessary for build, into `dependencies`
* test: fix one more test
remove 500-embed.tpl
* fix: restore use of assets/nodebb.min.js, at least for now
* fix: remove unnecessary line break
* fix: point to proper ACP bundle
* test: maybe fix build test
* test: composer
* refactor: dont need dist
* refactor: more cleanup
use everything from build/public folder
* get rid of conditional import in app.js
* fix: ace
* refactor: cropper alias
* test: lint and test fixes
* lint: fix
* refactor: rename function to app.require
* refactor: go back to using app.require
* chore: use github branch
* chore: use webpack branch
* feat: webpack webinstaller
* feat: add chunkFile name with contenthash
* refactor: move hooks to top
* refactor: get rid of template500Function
* fix(deps): use webpack5 branch of 2factor plugin
* chore: tagging v2.0.0-beta.0 pre-release version :boom: :shipit: :tada: :rocket:
* refactor: disable cache on templates
loadTemplate is called once by benchpress and the result is cache internally
* refactor: add server side helpers.js
* feat: deprecate /plugins shorthand route, closes #10343
* refactor: use build/public for webpack
* test: fix filename
* fix: more specific selector
* lint: ignore
* refactor: fix comments
* test: add debug for random failing test
* refactor: cleanup
remove test page, remove dupe functions in utils.common
* lint: use relative path for now
* chore: bump prerelease version
* feat: add translateKeys
* fix: optional params
* fix: get rid of extra timeago files
* refactor: cleanup, require timeago locale earlier
remove translator.prepareDOM, it is in header.tpl html tag
* refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378)
* refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels
- Existing hooks are preserved (to be deprecated at a later date, possibly)
- New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks
* docs: fix typo in comment
* test: spec changes
* refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378)
* refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels
- Existing hooks are preserved (to be deprecated at a later date, possibly)
- New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks
* docs: fix typo in comment
* test: spec changes
* feat: allow app.require('bootbox'/'benchpressjs')
* refactor: require server side utils
* test: jquery ready
* change istaller to use build/public
* test: use document.addEventListener
* refactor: closes #10301
* refactor: generateTopicClass
* fix: column counts for other privileges
* fix: #10443, regression where sorted-list items did not render into the DOM in the predicted order [breaking]
* fix: typo in hook name
* refactor: introduce a generic autocomplete.init() method that can be called to add nodebb-style autocompletion but using different data sources (e.g. not user/groups/tags)
* fix: crash if `delay` not passed in (as it cannot be destructured)
* refactor: replace substr
* feat: set --panel-offset style in html element based on stored value in localStorage
* refactor: addDropupHandler() logic to be less naive
- Take into account height of the menu
- Don't apply dropUp logic if there's nothing in the dropdown
- Remove 'hidden' class (added by default in Persona for post tools) when menu items are added
closes #10423
* refactor: simplify utils.params [breaking]
Retrospective analysis of the usage of this method suggests that the options passed in are superfluous, and that only `url` is required. Using a browser built-in makes more sense to accomplish what this method sets out to do.
* feat: add support for returning full URLSearchParams for utils.params
* fix: utils.params() fallback handling
* fix: default empty obj for params()
* fix: remove \'loggedin\' and \'register\' qs parameters once they have been used, delay invocation of messages until ajaxify.end
* fix: utils.params() not allowing relative paths to be passed in
* refactor(DRY): new assertPasswordValidity utils method
* fix: incorrect error message returned on insufficient privilege on flag edit
* fix: read/update/delete access to flags API should be limited for moderators to only post flags in categories they moderate
- added failing tests and patched up middleware.assert.flags to fix
* refactor: flag api v3 tests to create new post and flags on every round
* fix: missing error:no-flag language key
* refactor: flags.canView to check flag existence, simplify middleware.assert.flag
* feat: flag deletion API endpoint, #10426
* feat: UI for flag deletion, closes #10426
* chore: update plugin versions
* chore: up emoji
* chore: update markdown
* chore: up emoji-android
* fix: regression caused by utils.params() refactor, supports arrays and pipes all values through utils.toType, adjusts tests to type check
Co-authored-by: Julian Lam <julian@nodebb.org>
2022-04-29 21:39:33 -04:00
|
|
|
const utils = require('../utils');
|
2014-03-21 17:04:15 -04:00
|
|
|
|
2021-09-29 12:26:15 -04:00
|
|
|
const backlinkRegex = new RegExp(`(?:${nconf.get('url').replace('/', '\\/')}|\b|\\s)\\/topic\\/(\\d+)(?:\\/\\w+)?`, 'g');
|
|
|
|
|
|
2016-10-13 11:43:39 +02:00
|
|
|
module.exports = function (Topics) {
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.onNewPostMade = async function (postData) {
|
|
|
|
|
await Topics.updateLastPostTime(postData.tid, postData.timestamp);
|
|
|
|
|
await Topics.addPostToTopic(postData.tid, postData);
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2021-12-06 17:00:50 -05:00
|
|
|
Topics.getTopicPosts = async function (topicData, set, start, stop, uid, reverse) {
|
|
|
|
|
if (!topicData) {
|
2021-11-30 15:47:19 -05:00
|
|
|
return [];
|
|
|
|
|
}
|
2021-11-15 18:08:09 -05:00
|
|
|
|
|
|
|
|
let repliesStart = start;
|
|
|
|
|
let repliesStop = stop;
|
|
|
|
|
if (stop > 0) {
|
|
|
|
|
repliesStop -= 1;
|
|
|
|
|
if (start > 0) {
|
|
|
|
|
repliesStart -= 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-06 16:36:30 -05:00
|
|
|
let pids = [];
|
|
|
|
|
if (start !== 0 || stop !== 0) {
|
|
|
|
|
pids = await posts.getPidsFromSet(set, repliesStart, repliesStop, reverse);
|
|
|
|
|
}
|
2021-12-06 17:00:50 -05:00
|
|
|
if (!pids.length && !topicData.mainPid) {
|
2021-11-15 18:08:09 -05:00
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-06 17:00:50 -05:00
|
|
|
if (topicData.mainPid && start === 0) {
|
|
|
|
|
pids.unshift(topicData.mainPid);
|
2021-11-15 18:08:09 -05:00
|
|
|
}
|
2022-01-18 20:31:06 -05:00
|
|
|
let postData = await posts.getPostsByPids(pids, uid);
|
2021-11-15 18:08:09 -05:00
|
|
|
if (!postData.length) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
let replies = postData;
|
2021-12-06 17:00:50 -05:00
|
|
|
if (topicData.mainPid && start === 0) {
|
2021-11-15 18:08:09 -05:00
|
|
|
postData[0].index = 0;
|
|
|
|
|
replies = postData.slice(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Topics.calculatePostIndices(replies, repliesStart);
|
2021-12-06 17:00:50 -05:00
|
|
|
await addEventStartEnd(postData, set, reverse, topicData);
|
2022-03-16 16:56:07 -04:00
|
|
|
const allPosts = postData.slice();
|
|
|
|
|
postData = await user.blocks.filter(uid, postData);
|
|
|
|
|
if (allPosts.length !== postData.length) {
|
|
|
|
|
const includedPids = new Set(postData.map(p => p.pid));
|
|
|
|
|
allPosts.reverse().forEach((p, index) => {
|
|
|
|
|
if (!includedPids.has(p.pid) && allPosts[index + 1] && !reverse) {
|
|
|
|
|
allPosts[index + 1].eventEnd = p.eventEnd;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-15 18:08:09 -05:00
|
|
|
const result = await plugins.hooks.fire('filter:topic.getPosts', {
|
2021-12-06 17:00:50 -05:00
|
|
|
topic: topicData,
|
2021-11-15 18:08:09 -05:00
|
|
|
uid: uid,
|
|
|
|
|
posts: await Topics.addPostData(postData, uid),
|
|
|
|
|
});
|
|
|
|
|
return result.posts;
|
|
|
|
|
};
|
|
|
|
|
|
2021-11-17 23:34:01 -05:00
|
|
|
async function addEventStartEnd(postData, set, reverse, topicData) {
|
2021-11-15 18:08:09 -05:00
|
|
|
if (!postData.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
postData.forEach((p, index) => {
|
2021-11-17 23:34:01 -05:00
|
|
|
if (p && p.index === 0 && reverse) {
|
|
|
|
|
p.eventStart = topicData.lastposttime;
|
|
|
|
|
p.eventEnd = Date.now();
|
|
|
|
|
} else if (p && postData[index + 1]) {
|
|
|
|
|
p.eventStart = reverse ? postData[index + 1].timestamp : p.timestamp;
|
|
|
|
|
p.eventEnd = reverse ? p.timestamp : postData[index + 1].timestamp;
|
2021-11-15 18:08:09 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const lastPost = postData[postData.length - 1];
|
|
|
|
|
if (lastPost) {
|
2021-11-17 23:34:01 -05:00
|
|
|
lastPost.eventStart = reverse ? topicData.timestamp : lastPost.timestamp;
|
|
|
|
|
lastPost.eventEnd = reverse ? lastPost.timestamp : Date.now();
|
2021-11-15 18:08:09 -05:00
|
|
|
if (lastPost.index) {
|
2021-11-17 23:34:01 -05:00
|
|
|
const nextPost = await db[reverse ? 'getSortedSetRevRangeWithScores' : 'getSortedSetRangeWithScores'](set, lastPost.index, lastPost.index);
|
|
|
|
|
if (reverse) {
|
|
|
|
|
lastPost.eventStart = nextPost.length ? nextPost[0].score : lastPost.eventStart;
|
|
|
|
|
} else {
|
|
|
|
|
lastPost.eventEnd = nextPost.length ? nextPost[0].score : lastPost.eventEnd;
|
|
|
|
|
}
|
2021-11-15 18:08:09 -05:00
|
|
|
}
|
|
|
|
|
}
|
2021-11-17 23:34:01 -05:00
|
|
|
}
|
2014-03-21 17:04:15 -04:00
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.addPostData = async function (postData, uid) {
|
2014-11-11 19:47:56 -05:00
|
|
|
if (!Array.isArray(postData) || !postData.length) {
|
2019-07-09 12:46:49 -04:00
|
|
|
return [];
|
2014-11-11 19:47:56 -05:00
|
|
|
}
|
2020-11-25 17:58:44 -05:00
|
|
|
const pids = postData.map(post => post && post.pid);
|
2014-08-30 11:56:29 -04:00
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
async function getPostUserData(field, method) {
|
2024-01-10 20:49:27 -05:00
|
|
|
const uids = _.uniq(postData
|
|
|
|
|
.filter(p => p && (activitypub.helpers.isUri(p[field]) || parseInt(p[field], 10) >= 0))
|
|
|
|
|
.map(p => p[field]));
|
2019-07-09 12:46:49 -04:00
|
|
|
const userData = await method(uids);
|
|
|
|
|
return _.zipObject(uids, userData);
|
2014-08-30 11:56:29 -04:00
|
|
|
}
|
2019-07-09 12:46:49 -04:00
|
|
|
const [
|
|
|
|
|
bookmarks,
|
|
|
|
|
voteData,
|
|
|
|
|
userData,
|
|
|
|
|
editors,
|
|
|
|
|
replies,
|
|
|
|
|
] = await Promise.all([
|
|
|
|
|
posts.hasBookmarked(pids, uid),
|
|
|
|
|
posts.getVoteStatusByPostIDs(pids, uid),
|
2021-02-04 00:01:39 -07:00
|
|
|
getPostUserData('uid', async uids => await posts.getUserInfoForPosts(uids, uid)),
|
|
|
|
|
getPostUserData('editor', async uids => await user.getUsersFields(uids, ['uid', 'username', 'userslug'])),
|
2023-06-14 22:12:24 -04:00
|
|
|
getPostReplies(postData, uid),
|
2024-01-10 20:49:27 -05:00
|
|
|
Topics.addParentPosts(postData, uid),
|
2019-07-09 12:46:49 -04:00
|
|
|
]);
|
|
|
|
|
|
2021-02-04 00:01:39 -07:00
|
|
|
postData.forEach((postObj, i) => {
|
2019-07-09 12:46:49 -04:00
|
|
|
if (postObj) {
|
2020-10-26 10:43:18 -04:00
|
|
|
postObj.user = postObj.uid ? userData[postObj.uid] : { ...userData[postObj.uid] };
|
2019-07-09 12:46:49 -04:00
|
|
|
postObj.editor = postObj.editor ? editors[postObj.editor] : null;
|
|
|
|
|
postObj.bookmarked = bookmarks[i];
|
|
|
|
|
postObj.upvoted = voteData.upvotes[i];
|
|
|
|
|
postObj.downvoted = voteData.downvotes[i];
|
|
|
|
|
postObj.votes = postObj.votes || 0;
|
|
|
|
|
postObj.replies = replies[i];
|
|
|
|
|
postObj.selfPost = parseInt(uid, 10) > 0 && parseInt(uid, 10) === postObj.uid;
|
|
|
|
|
|
|
|
|
|
// Username override for guests, if enabled
|
|
|
|
|
if (meta.config.allowGuestHandles && postObj.uid === 0 && postObj.handle) {
|
|
|
|
|
postObj.user.username = validator.escape(String(postObj.handle));
|
2020-11-29 15:38:02 -05:00
|
|
|
postObj.user.displayname = postObj.user.username;
|
2014-06-28 01:03:26 -04:00
|
|
|
}
|
2019-07-09 12:46:49 -04:00
|
|
|
}
|
|
|
|
|
});
|
2014-06-28 01:03:26 -04:00
|
|
|
|
2020-11-20 16:06:26 -05:00
|
|
|
const result = await plugins.hooks.fire('filter:topics.addPostData', {
|
2019-07-09 12:46:49 -04:00
|
|
|
posts: postData,
|
|
|
|
|
uid: uid,
|
|
|
|
|
});
|
|
|
|
|
return result.posts;
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2016-10-13 11:43:39 +02:00
|
|
|
Topics.modifyPostsByPrivilege = function (topicData, topicPrivileges) {
|
2020-11-25 17:58:44 -05:00
|
|
|
const loggedIn = parseInt(topicPrivileges.uid, 10) > 0;
|
2021-02-04 00:01:39 -07:00
|
|
|
topicData.posts.forEach((post) => {
|
2015-10-06 18:36:03 -04:00
|
|
|
if (post) {
|
2020-11-25 17:58:44 -05:00
|
|
|
post.topicOwnerPost = parseInt(topicData.uid, 10) === parseInt(post.uid, 10);
|
2016-08-09 09:50:49 -05:00
|
|
|
post.display_edit_tools = topicPrivileges.isAdminOrMod || (post.selfPost && topicPrivileges['posts:edit']);
|
|
|
|
|
post.display_delete_tools = topicPrivileges.isAdminOrMod || (post.selfPost && topicPrivileges['posts:delete']);
|
2016-08-06 20:28:55 -05:00
|
|
|
post.display_moderator_tools = post.display_edit_tools || post.display_delete_tools;
|
2015-10-06 18:36:03 -04:00
|
|
|
post.display_move_tools = topicPrivileges.isAdminOrMod && post.index !== 0;
|
2019-06-26 12:14:38 -04:00
|
|
|
post.display_post_menu = topicPrivileges.isAdminOrMod ||
|
|
|
|
|
(post.selfPost && !topicData.locked && !post.deleted) ||
|
|
|
|
|
(post.selfPost && post.deleted && parseInt(post.deleterUid, 10) === parseInt(topicPrivileges.uid, 10)) ||
|
|
|
|
|
((loggedIn || topicData.postSharing.length) && !post.deleted);
|
2016-03-08 19:06:59 +02:00
|
|
|
post.ip = topicPrivileges.isAdminOrMod ? post.ip : undefined;
|
|
|
|
|
|
2018-06-08 16:01:11 -04:00
|
|
|
posts.modifyPostByPrivilege(post, topicPrivileges);
|
2015-10-06 18:36:03 -04:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-10 20:49:27 -05:00
|
|
|
Topics.addParentPosts = async function (postData, uid) {
|
|
|
|
|
let parentPids = postData
|
|
|
|
|
.filter(p => p && p.hasOwnProperty('toPid') && (activitypub.helpers.isUri(p.toPid) || utils.isNumber(p.toPid)))
|
|
|
|
|
.map(postObj => postObj.toPid);
|
2015-09-29 16:36:37 -04:00
|
|
|
|
|
|
|
|
if (!parentPids.length) {
|
2019-07-09 12:46:49 -04:00
|
|
|
return;
|
2015-09-29 16:36:37 -04:00
|
|
|
}
|
2018-12-17 18:56:09 -05:00
|
|
|
parentPids = _.uniq(parentPids);
|
2024-01-11 10:05:02 -05:00
|
|
|
await activitypub.notes.assert(uid, parentPids.filter(pid => activitypub.helpers.isUri(pid)));
|
2019-07-09 12:46:49 -04:00
|
|
|
const parentPosts = await posts.getPostsFields(parentPids, ['uid']);
|
|
|
|
|
const parentUids = _.uniq(parentPosts.map(postObj => postObj && postObj.uid));
|
|
|
|
|
const userData = await user.getUsersFields(parentUids, ['username']);
|
|
|
|
|
|
2023-07-12 10:40:25 -04:00
|
|
|
const usersMap = _.zipObject(parentUids, userData);
|
2021-02-04 00:06:15 -07:00
|
|
|
const parents = {};
|
2021-02-04 00:01:39 -07:00
|
|
|
parentPosts.forEach((post, i) => {
|
2023-08-06 02:33:28 -04:00
|
|
|
if (usersMap[post.uid]) {
|
|
|
|
|
parents[parentPids[i]] = {
|
|
|
|
|
username: usersMap[post.uid].username,
|
|
|
|
|
displayname: usersMap[post.uid].displayname,
|
|
|
|
|
};
|
|
|
|
|
}
|
2019-07-09 12:46:49 -04:00
|
|
|
});
|
|
|
|
|
|
2021-02-04 00:01:39 -07:00
|
|
|
postData.forEach((post) => {
|
2023-08-06 02:33:28 -04:00
|
|
|
if (parents[post.toPid]) {
|
|
|
|
|
post.parent = parents[post.toPid];
|
|
|
|
|
}
|
2019-07-09 12:46:49 -04:00
|
|
|
});
|
2015-09-29 16:36:37 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-03 12:48:26 -04:00
|
|
|
Topics.calculatePostIndices = function (posts, start) {
|
2021-02-04 00:01:39 -07:00
|
|
|
posts.forEach((post, index) => {
|
2019-07-03 12:48:26 -04:00
|
|
|
if (post) {
|
2015-09-15 17:19:03 -04:00
|
|
|
post.index = start + index + 1;
|
2015-02-07 00:12:47 -05:00
|
|
|
}
|
2015-09-15 17:19:03 -04:00
|
|
|
});
|
2015-02-07 00:12:47 -05:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.getLatestUndeletedPid = async function (tid) {
|
|
|
|
|
const pid = await Topics.getLatestUndeletedReply(tid);
|
|
|
|
|
if (pid) {
|
|
|
|
|
return pid;
|
|
|
|
|
}
|
|
|
|
|
const mainPid = await Topics.getTopicField(tid, 'mainPid');
|
|
|
|
|
const mainPost = await posts.getPostFields(mainPid, ['pid', 'deleted']);
|
|
|
|
|
return mainPost.pid && !mainPost.deleted ? mainPost.pid : null;
|
2014-04-24 20:05:05 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.getLatestUndeletedReply = async function (tid) {
|
2021-02-04 00:06:15 -07:00
|
|
|
let isDeleted = false;
|
|
|
|
|
let index = 0;
|
2019-07-09 12:46:49 -04:00
|
|
|
do {
|
|
|
|
|
/* eslint-disable no-await-in-loop */
|
2024-02-07 12:28:16 -05:00
|
|
|
const pids = await db.getSortedSetRevRange(`tid:${tid}:posts`, index, index);
|
2019-07-09 12:46:49 -04:00
|
|
|
if (!pids.length) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
isDeleted = await posts.getPostField(pids[0], 'deleted');
|
|
|
|
|
if (!isDeleted) {
|
2024-01-17 12:15:58 -05:00
|
|
|
return pids[0];
|
2017-02-17 20:20:42 -07:00
|
|
|
}
|
2019-07-09 12:46:49 -04:00
|
|
|
index += 1;
|
|
|
|
|
} while (isDeleted);
|
2014-04-24 20:05:05 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.addPostToTopic = async function (tid, postData) {
|
2024-03-06 11:45:29 -05:00
|
|
|
console.log('now in addPostToTopic', tid, postData);
|
2019-07-09 12:46:49 -04:00
|
|
|
const mainPid = await Topics.getTopicField(tid, 'mainPid');
|
2024-03-06 11:45:29 -05:00
|
|
|
console.log(mainPid);
|
2024-02-08 11:55:48 -05:00
|
|
|
if (!mainPid) {
|
2019-07-09 12:46:49 -04:00
|
|
|
await Topics.setTopicField(tid, 'mainPid', postData.pid);
|
2024-03-06 11:45:29 -05:00
|
|
|
console.log('what');
|
2019-07-09 12:46:49 -04:00
|
|
|
} else {
|
|
|
|
|
const upvotes = parseInt(postData.upvotes, 10) || 0;
|
|
|
|
|
const downvotes = parseInt(postData.downvotes, 10) || 0;
|
|
|
|
|
const votes = upvotes - downvotes;
|
|
|
|
|
await db.sortedSetsAdd([
|
2021-02-03 23:59:08 -07:00
|
|
|
`tid:${tid}:posts`, `tid:${tid}:posts:votes`,
|
2019-07-09 12:46:49 -04:00
|
|
|
], [postData.timestamp, votes], postData.pid);
|
|
|
|
|
}
|
|
|
|
|
await Topics.increasePostCount(tid);
|
2021-02-03 23:59:08 -07:00
|
|
|
await db.sortedSetIncrBy(`tid:${tid}:posters`, 1, postData.uid);
|
|
|
|
|
const posterCount = await db.sortedSetCard(`tid:${tid}:posters`);
|
2020-10-24 21:14:52 -04:00
|
|
|
await Topics.setTopicField(tid, 'postercount', posterCount);
|
2019-07-09 12:46:49 -04:00
|
|
|
await Topics.updateTeaser(tid);
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.removePostFromTopic = async function (tid, postData) {
|
|
|
|
|
await db.sortedSetsRemove([
|
2021-02-03 23:59:08 -07:00
|
|
|
`tid:${tid}:posts`,
|
|
|
|
|
`tid:${tid}:posts:votes`,
|
2019-07-09 12:46:49 -04:00
|
|
|
], postData.pid);
|
|
|
|
|
await Topics.decreasePostCount(tid);
|
2021-02-03 23:59:08 -07:00
|
|
|
await db.sortedSetIncrBy(`tid:${tid}:posters`, -1, postData.uid);
|
|
|
|
|
await db.sortedSetsRemoveRangeByScore([`tid:${tid}:posters`], '-inf', 0);
|
|
|
|
|
const posterCount = await db.sortedSetCard(`tid:${tid}:posters`);
|
2020-10-24 21:14:52 -04:00
|
|
|
await Topics.setTopicField(tid, 'postercount', posterCount);
|
2019-07-09 12:46:49 -04:00
|
|
|
await Topics.updateTeaser(tid);
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.getPids = async function (tid) {
|
2021-02-04 00:06:15 -07:00
|
|
|
let [mainPid, pids] = await Promise.all([
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.getTopicField(tid, 'mainPid'),
|
2021-02-03 23:59:08 -07:00
|
|
|
db.getSortedSetRange(`tid:${tid}:posts`, 0, -1),
|
2019-07-09 12:46:49 -04:00
|
|
|
]);
|
|
|
|
|
if (parseInt(mainPid, 10)) {
|
|
|
|
|
pids = [mainPid].concat(pids);
|
|
|
|
|
}
|
|
|
|
|
return pids;
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.increasePostCount = async function (tid) {
|
|
|
|
|
incrementFieldAndUpdateSortedSet(tid, 'postcount', 1, 'topics:posts');
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.decreasePostCount = async function (tid) {
|
|
|
|
|
incrementFieldAndUpdateSortedSet(tid, 'postcount', -1, 'topics:posts');
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2024-01-13 22:26:39 -05:00
|
|
|
Topics.increaseViewCount = async function (req, tid) {
|
|
|
|
|
const allow = req.uid > 0 || (meta.config.guestsIncrementTopicViews && req.uid === 0);
|
|
|
|
|
if (allow) {
|
|
|
|
|
req.session.tids_viewed = req.session.tids_viewed || {};
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const interval = meta.config.incrementTopicViewsInterval * 60000;
|
|
|
|
|
if (!req.session.tids_viewed[tid] || req.session.tids_viewed[tid] < now - interval) {
|
|
|
|
|
const cid = await Topics.getTopicField(tid, 'cid');
|
|
|
|
|
incrementFieldAndUpdateSortedSet(tid, 'viewcount', 1, ['topics:views', `cid:${cid}:tids:views`]);
|
|
|
|
|
req.session.tids_viewed[tid] = now;
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
async function incrementFieldAndUpdateSortedSet(tid, field, by, set) {
|
2024-02-07 12:28:16 -05:00
|
|
|
const value = await db.incrObjectFieldBy(`topic:${tid}`, field, by);
|
2021-09-28 11:13:56 -04:00
|
|
|
await db[Array.isArray(set) ? 'sortedSetsAdd' : 'sortedSetAdd'](set, value, tid);
|
2014-03-21 17:04:15 -04:00
|
|
|
}
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.getTitleByPid = async function (pid) {
|
|
|
|
|
return await Topics.getTopicFieldByPid('title', pid);
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.getTopicFieldByPid = async function (field, pid) {
|
|
|
|
|
const tid = await posts.getPostField(pid, 'tid');
|
|
|
|
|
return await Topics.getTopicField(tid, field);
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.getTopicDataByPid = async function (pid) {
|
|
|
|
|
const tid = await posts.getPostField(pid, 'tid');
|
|
|
|
|
return await Topics.getTopicData(tid);
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
Topics.getPostCount = async function (tid) {
|
2021-02-03 23:59:08 -07:00
|
|
|
return await db.getObjectField(`topic:${tid}`, 'postcount');
|
2014-06-02 20:41:03 -04:00
|
|
|
};
|
2017-03-15 18:56:12 -04:00
|
|
|
|
2023-06-14 22:12:24 -04:00
|
|
|
async function getPostReplies(postData, callerUid) {
|
|
|
|
|
const pids = postData.map(p => p && p.pid);
|
2021-02-03 23:59:08 -07:00
|
|
|
const keys = pids.map(pid => `pid:${pid}:replies`);
|
2023-06-14 22:12:24 -04:00
|
|
|
const [arrayOfReplyPids, userSettings] = await Promise.all([
|
|
|
|
|
db.getSortedSetsMembers(keys),
|
|
|
|
|
user.getSettings(callerUid),
|
|
|
|
|
]);
|
2019-07-09 12:46:49 -04:00
|
|
|
|
|
|
|
|
const uniquePids = _.uniq(_.flatten(arrayOfReplyPids));
|
|
|
|
|
|
2020-07-06 15:50:19 -04:00
|
|
|
let replyData = await posts.getPostsFields(uniquePids, ['pid', 'uid', 'timestamp']);
|
2020-11-20 16:06:26 -05:00
|
|
|
const result = await plugins.hooks.fire('filter:topics.getPostReplies', {
|
2020-10-20 12:00:56 -04:00
|
|
|
uid: callerUid,
|
|
|
|
|
replies: replyData,
|
|
|
|
|
});
|
|
|
|
|
replyData = await user.blocks.filter(callerUid, result.replies);
|
2019-07-09 12:46:49 -04:00
|
|
|
|
|
|
|
|
const uids = replyData.map(replyData => replyData && replyData.uid);
|
|
|
|
|
|
|
|
|
|
const uniqueUids = _.uniq(uids);
|
|
|
|
|
|
|
|
|
|
const userData = await user.getUsersWithFields(uniqueUids, ['uid', 'username', 'userslug', 'picture'], callerUid);
|
|
|
|
|
|
2020-07-06 15:50:19 -04:00
|
|
|
const uidMap = _.zipObject(uniqueUids, userData);
|
|
|
|
|
const pidMap = _.zipObject(replyData.map(r => r.pid), replyData);
|
2023-06-14 22:12:24 -04:00
|
|
|
const postDataMap = _.zipObject(pids, postData);
|
2019-07-09 12:46:49 -04:00
|
|
|
|
2023-06-14 22:12:24 -04:00
|
|
|
const returnData = await Promise.all(arrayOfReplyPids.map(async (replyPids, idx) => {
|
2023-06-16 09:05:37 -04:00
|
|
|
const currentPost = postData[idx];
|
2020-07-06 15:50:19 -04:00
|
|
|
replyPids = replyPids.filter(pid => pidMap[pid]);
|
|
|
|
|
const uidsUsed = {};
|
|
|
|
|
const currentData = {
|
2019-07-09 12:46:49 -04:00
|
|
|
hasMore: false,
|
2023-06-14 22:12:24 -04:00
|
|
|
hasSingleImmediateReply: false,
|
2019-07-09 12:46:49 -04:00
|
|
|
users: [],
|
2023-10-05 12:48:50 -04:00
|
|
|
text: replyPids.length > 1 ? `[[topic:replies-to-this-post, ${replyPids.length}]]` : '[[topic:one-reply-to-this-post]]',
|
2019-07-09 12:46:49 -04:00
|
|
|
count: replyPids.length,
|
|
|
|
|
timestampISO: replyPids.length ? utils.toISOString(pidMap[replyPids[0]].timestamp) : undefined,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
replyPids.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
|
|
|
|
|
|
2021-02-04 00:01:39 -07:00
|
|
|
replyPids.forEach((replyPid) => {
|
2020-07-06 15:50:19 -04:00
|
|
|
const replyData = pidMap[replyPid];
|
2019-07-09 12:46:49 -04:00
|
|
|
if (!uidsUsed[replyData.uid] && currentData.users.length < 6) {
|
|
|
|
|
currentData.users.push(uidMap[replyData.uid]);
|
|
|
|
|
uidsUsed[replyData.uid] = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (currentData.users.length > 5) {
|
|
|
|
|
currentData.users.pop();
|
|
|
|
|
currentData.hasMore = true;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-14 22:12:24 -04:00
|
|
|
if (replyPids.length === 1) {
|
2023-06-16 09:05:37 -04:00
|
|
|
const currentIndex = currentPost ? currentPost.index : null;
|
2023-06-14 22:12:24 -04:00
|
|
|
const replyPid = replyPids[0];
|
|
|
|
|
// only load index of nested reply if we can't find it in the postDataMap
|
|
|
|
|
let replyPost = postDataMap[replyPid];
|
|
|
|
|
if (!replyPost) {
|
|
|
|
|
const tid = await posts.getPostField(replyPid, 'tid');
|
|
|
|
|
replyPost = {
|
|
|
|
|
index: await posts.getPidIndex(replyPid, tid, userSettings.topicPostSort),
|
2023-06-16 09:05:37 -04:00
|
|
|
tid: tid,
|
2023-06-14 22:12:24 -04:00
|
|
|
};
|
|
|
|
|
}
|
2023-06-16 09:05:37 -04:00
|
|
|
currentData.hasSingleImmediateReply =
|
|
|
|
|
(currentPost && currentPost.tid === replyPost.tid) &&
|
|
|
|
|
Math.abs(currentIndex - replyPost.index) === 1;
|
2023-06-14 22:12:24 -04:00
|
|
|
}
|
|
|
|
|
|
2019-07-09 12:46:49 -04:00
|
|
|
return currentData;
|
2023-06-14 22:12:24 -04:00
|
|
|
}));
|
2019-07-09 12:46:49 -04:00
|
|
|
|
|
|
|
|
return returnData;
|
2017-03-15 18:56:12 -04:00
|
|
|
}
|
2021-09-29 12:26:15 -04:00
|
|
|
|
|
|
|
|
Topics.syncBacklinks = async (postData) => {
|
|
|
|
|
if (!postData) {
|
|
|
|
|
throw new Error('[[error:invalid-data]]');
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-01 13:48:12 -04:00
|
|
|
|
|
|
|
|
let { content } = postData;
|
|
|
|
|
// ignore lines that start with `>`
|
|
|
|
|
content = content.split('\n').filter(line => !line.trim().startsWith('>')).join('\n');
|
2021-09-29 12:26:15 -04:00
|
|
|
// Scan post content for topic links
|
2023-06-01 13:48:12 -04:00
|
|
|
const matches = [...content.matchAll(backlinkRegex)];
|
2021-09-29 12:26:15 -04:00
|
|
|
if (!matches) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { pid, uid, tid } = postData;
|
2022-01-18 20:12:50 -05:00
|
|
|
let add = _.uniq(matches.map(match => match[1]).map(tid => parseInt(tid, 10)));
|
2021-09-29 12:26:15 -04:00
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const topicsExist = await Topics.exists(add);
|
|
|
|
|
const current = (await db.getSortedSetMembers(`pid:${pid}:backlinks`)).map(tid => parseInt(tid, 10));
|
|
|
|
|
const remove = current.filter(tid => !add.includes(tid));
|
2022-01-17 19:05:07 -05:00
|
|
|
add = add.filter((_tid, idx) => topicsExist[idx] && !current.includes(_tid) && tid !== _tid);
|
2021-09-29 12:26:15 -04:00
|
|
|
|
|
|
|
|
// Remove old backlinks
|
|
|
|
|
await db.sortedSetRemove(`pid:${pid}:backlinks`, remove);
|
|
|
|
|
|
|
|
|
|
// Add new backlinks
|
2022-01-18 20:12:50 -05:00
|
|
|
await db.sortedSetAdd(`pid:${pid}:backlinks`, add.map(() => now), add);
|
2021-09-29 12:26:15 -04:00
|
|
|
await Promise.all(add.map(async (tid) => {
|
|
|
|
|
await Topics.events.log(tid, {
|
|
|
|
|
uid,
|
|
|
|
|
type: 'backlink',
|
|
|
|
|
href: `/post/${pid}`,
|
|
|
|
|
});
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return add.length + (current - remove);
|
|
|
|
|
};
|
2014-03-21 17:04:15 -04:00
|
|
|
};
|