diff --git a/install/package.json b/install/package.json
index cb5eb4e4ea..441c7b7b59 100644
--- a/install/package.json
+++ b/install/package.json
@@ -35,13 +35,14 @@
"@isaacs/ttlcache": "1.4.1",
"@nodebb/spider-detector": "2.0.3",
"@popperjs/core": "2.11.8",
+ "@socket.io/redis-adapter": "8.3.0",
"ace-builds": "1.33.2",
"archiver": "7.0.1",
"async": "3.2.5",
"autoprefixer": "10.4.19",
"bcryptjs": "2.4.3",
"benchpressjs": "2.5.1",
- "body-parser": "1.20.2",
+ "body-parser": "^1.20.3",
"bootbox": "6.0.0",
"bootstrap": "5.3.3",
"bootswatch": "5.3.3",
@@ -58,15 +59,15 @@
"connect-multiparty": "2.2.0",
"connect-pg-simple": "9.0.1",
"connect-redis": "7.1.1",
- "cookie-parser": "1.4.6",
+ "cookie-parser": "^1.4.7",
"cron": "3.1.7",
"cropperjs": "1.6.2",
"csrf-sync": "4.0.3",
"daemon": "1.1.0",
"diff": "5.2.0",
"esbuild": "0.21.2",
- "express": "4.19.2",
- "express-session": "1.18.0",
+ "express": "^4.21.1",
+ "express-session": "^1.18.1",
"express-useragent": "1.0.15",
"fetch-cookie": "3.0.1",
"file-loader": "6.2.0",
@@ -75,6 +76,7 @@
"helmet": "7.1.0",
"html-to-text": "9.0.5",
"imagesloaded": "5.0.0",
+ "ioredis": "5.4.1",
"ipaddr.js": "2.2.0",
"jquery": "3.7.1",
"jquery-deserialize": "2.0.0",
@@ -102,7 +104,7 @@
"nodebb-plugin-markdown": "12.2.6",
"nodebb-plugin-mentions": "4.4.3",
"nodebb-plugin-ntfy": "1.7.4",
- "nodebb-plugin-spam-be-gone": "2.2.2",
+ "nodebb-plugin-spam-be-gone": "^0.4.4",
"nodebb-rewards-essentials": "1.0.0",
"nodebb-theme-harmony": "1.2.63",
"nodebb-theme-lavender": "7.1.8",
@@ -120,7 +122,6 @@
"postcss-clean": "1.2.0",
"progress-webpack-plugin": "1.0.16",
"prompt": "1.3.0",
- "ioredis": "5.4.1",
"rimraf": "5.0.7",
"rss": "1.2.2",
"rtlcss": "4.1.1",
@@ -130,9 +131,8 @@
"serve-favicon": "2.5.0",
"sharp": "0.32.6",
"sitemap": "7.1.1",
- "socket.io": "4.7.5",
+ "socket.io": "^4.8.0",
"socket.io-client": "4.7.5",
- "@socket.io/redis-adapter": "8.3.0",
"sortablejs": "1.15.2",
"spdx-license-list": "6.9.0",
"terser-webpack-plugin": "5.3.10",
@@ -143,7 +143,7 @@
"toobusy-js": "0.5.1",
"tough-cookie": "4.1.4",
"validator": "13.12.0",
- "webpack": "5.91.0",
+ "webpack": "^5.95.0",
"webpack-merge": "5.10.0",
"winston": "3.13.0",
"workerpool": "9.1.1",
@@ -164,7 +164,7 @@
"grunt-contrib-watch": "1.1.0",
"husky": "8.0.3",
"jsdom": "24.0.0",
- "lint-staged": "15.2.2",
+ "lint-staged": "^15.2.10",
"mocha": "10.4.0",
"mocha-lcov-reporter": "1.3.0",
"mockdate": "3.0.5",
@@ -195,4 +195,4 @@
"url": "https://github.com/barisusakli"
}
]
-}
\ No newline at end of file
+}
diff --git a/public/src/client/account/endorsed.js b/public/src/client/account/endorsed.js
index c49e491ad5..b0dd0ca6fd 100644
--- a/public/src/client/account/endorsed.js
+++ b/public/src/client/account/endorsed.js
@@ -17,20 +17,20 @@ define('forum/account/endorsed', ['forum/account/header', 'forum/account/posts']
posts.handleInfiniteScroll('account/endorsed');
};
- Endorsed.handleEndorse = function (postId) {
- console.log('Endorsing post with ID:', postId); // Log the post ID for debugging
-
- // Logic to handle endorsement, possibly involving API calls to mark a post as endorsed
- socket.emit('plugins.endorse.post', { postId: postId }, function (err, result) {
- if (err) {
- console.error('Error endorsing post:', err); // Log error for debugging
- return alerts.error('Error endorsing post: ' + err.message);
- }
-
- app.alertSuccess('Post successfully endorsed!'); // Show success alert
- });
- };
-
+ Endorsed.handleEndorse = function (postId) {
+ console.log('Endorsing post with ID:', postId); // Log the post ID for debugging
+
+ // Logic to handle endorsement, possibly involving API calls to mark a post as endorsed
+ socket.emit('plugins.endorse.post', { postId: postId }, function (err) {
+ if (err) {
+ console.error('Error endorsing post:', err); // Log error for debugging
+ return app.alertError('Error endorsing post: ' + err.message);
+ }
+
+ app.alertSuccess('Post successfully endorsed!'); // Show success alert
+ });
+ };
+
return Endorsed;
});
diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js
index 6c2910b398..9268b244eb 100644
--- a/public/src/client/topic/events.js
+++ b/public/src/client/topic/events.js
@@ -191,7 +191,7 @@ define('forum/topic/events', [
function onPostEndorsed(data) {
const postEl = $('[data-pid="' + data.postId + '"]');
- postEl.addClass('endorsed');
+ postEl.addClass('endorsed');
}
function togglePostDeleteState(data) {
diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js
index 9c3fbc091a..7fc22a010d 100644
--- a/public/src/client/topic/postTools.js
+++ b/public/src/client/topic/postTools.js
@@ -115,38 +115,38 @@ define('forum/topic/postTools', [
postContainer.on('click', '[component="post/endorse"]', function (event) {
event.preventDefault();
console.log('Endorse Post Container'); // Log to confirm button is clicked
-
+
const pid = $(this).closest('[data-pid]').attr('data-pid'); // Get post ID
if (!pid) {
console.error('No post ID found');
return;
}
-
+
endorsePost($(this), pid);
});
-
+
function endorsePost(button, pid) {
console.log('Endorsing post with ID:', pid);
-
+
// Emit socket event to endorse the post
socket.emit('plugins.endorse.post', { postId: pid }, function (err, result) {
if (err) {
- return console.error('Error endorsing post: ' + err.message)
+ return console.error('Error endorsing post: ' + err.message);
}
-
+
// Success: Update the button text and endorsement count
button.text('Endorsed').addClass('endorsed');
-
+
// Optionally update the endorsement count (assuming result contains the count)
const countElement = button.find('.endorse-count');
if (countElement.length && result.endorsements) {
countElement.text(result.endorsements);
}
-
+
app.alertSuccess('Post successfully endorsed!');
});
}
-
+
$('.topic').on('click', '[component="topic/reply"]', function (e) {
e.preventDefault();
@@ -238,43 +238,6 @@ define('forum/topic/postTools', [
}
});
- function endorsePost(button, pid) {
- console.log('Endorse button clicked'); // Add this to check if the function is firing
- console.log('API Method: ', method); // Log the method (PUT or DELETE)
-
- const method = button.attr('data-endorsed') === 'false' ? 'put' : 'del';
-
- // Emit socket event or make API call
- api[method](`/posts/${pid}/endorse`, undefined, function (err, result) {
- if (err) {
- console.error('Error:', err); // Log errors if any
- return alerts.error(err);
- }
-
- console.log('Endorse button post API', result); // Log the response
-
- const type = method === 'put' ? 'endorse' : 'unendorse';
- hooks.fire(`action:post.${type}`, { pid: pid });
-
- // Update the button UI to reflect the new state
- button.attr('data-endorsed', type === 'endorse');
-
- const countElement = button.find('.endorse-count');
- let endorsementCount = result.endorsements;
-
- if (type === 'endorse') {
- button.addClass('endorsed').text('Endorsed');
- countElement.text(endorsementCount);
- } else {
- button.removeClass('endorsed').text('Endorse');
- countElement.text(endorsementCount || '');
- }
- });
- return false;
- }
-
-
-
function checkDuration(duration, postTimestamp, languageKey) {
if (!ajaxify.data.privileges.isAdminOrMod && duration && Date.now() - postTimestamp > duration * 1000) {
diff --git a/src/controllers/posts.js b/src/controllers/posts.js
index 26b695c6c4..55f14774f9 100644
--- a/src/controllers/posts.js
+++ b/src/controllers/posts.js
@@ -1,10 +1,11 @@
'use strict';
-const db = require('../database'); // Ensure that your database module is imported properly
+const db = require('../database'); // Ensure that your database module is imported properly
const posts = require('../posts');
const privileges = require('../privileges');
const helpers = require('./helpers');
+
const postsController = module.exports;
postsController.redirectToPost = async function (req, res, next) {
@@ -23,8 +24,7 @@ postsController.redirectToPost = async function (req, res, next) {
if (!canRead) {
return helpers.notAllowed(req, res);
}
-
- const qs = querystring.stringify(req.query);
+ const qs = require('querystring').stringify(req.query);
helpers.redirect(res, qs ? `${path}?${qs}` : path, true);
};
@@ -40,30 +40,28 @@ postsController.getRecentPosts = async function (req, res) {
// Updated endorsePost function
postsController.endorsePost = async function (req, res) {
const postId = req.params.pid;
- const uid = req.uid;
-
+ const { uid } = req;
+
if (!postId || !uid) {
- return res.status(400).json({ error: 'Invalid request' });
+ return res.status(400).json({ error: 'Invalid request' });
}
-
+
try {
- const hasEndorsed = await db.isSetMember(`post:${postId}:endorsed`, uid);
-
- if (req.method === 'DELETE' && hasEndorsed) {
- // Unendorse logic
- await db.setRemove(`post:${postId}:endorsed`, uid);
- const endorsementCount = await db.decrObjectField(`post:${postId}`, 'endorsements');
- return res.status(200).json({ endorsed: false, endorsements: endorsementCount });
- } else if (req.method === 'PUT' && !hasEndorsed) {
- // Endorse logic
- await db.setAdd(`post:${postId}:endorsed`, uid);
- const endorsementCount = await db.incrObjectField(`post:${postId}`, 'endorsements');
- return res.status(200).json({ endorsed: true, endorsements: endorsementCount });
- } else {
- return res.status(400).json({ error: 'Invalid action' });
- }
+ const hasEndorsed = await db.isSetMember(`post:${postId}:endorsed`, uid);
+
+ if (req.method === 'DELETE' && hasEndorsed) {
+ // Unendorse logic
+ await db.setRemove(`post:${postId}:endorsed`, uid);
+ const endorsementCount = await db.decrObjectField(`post:${postId}`, 'endorsements');
+ return res.status(200).json({ endorsed: false, endorsements: endorsementCount });
+ } else if (req.method === 'PUT' && !hasEndorsed) {
+ // Endorse logic
+ await db.setAdd(`post:${postId}:endorsed`, uid);
+ const endorsementCount = await db.incrObjectField(`post:${postId}`, 'endorsements');
+ return res.status(200).json({ endorsed: true, endorsements: endorsementCount });
+ }
+ return res.status(400).json({ error: 'Invalid action' });
} catch (err) {
- return res.status(500).json({ error: 'An error occurred while endorsing the post' });
+ return res.status(500).json({ error: 'An error occurred while endorsing the post' });
}
- };
-
\ No newline at end of file
+};
diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js
index ff0b2c57e9..c1ab75b1d3 100644
--- a/src/socket.io/posts.js
+++ b/src/socket.io/posts.js
@@ -189,36 +189,35 @@ SocketPosts.editQueuedContent = async function (socket, data) {
// Add this event listener for post endorsement
SocketPosts.endorsePost = async function (socket, data) {
- const postId = data.postId;
+ const { postId } = data;
- if (!postId) {
- throw new Error('Invalid post ID');
- }
-
- try {
- // Check if the user has privileges to endorse the post
- const canEndorse = await privileges.posts.can('endorse', postId, socket.uid);
- if (!canEndorse) {
- throw new Error('You do not have the privilege to endorse this post.');
- }
+ if (!postId) {
+ throw new Error('Invalid post ID');
+ }
- // Add the endorsement to the database (this depends on your schema)
- // Example: Increment endorsement count for the post
- await db.incrObjectFieldBy(`post:${postId}`, 'endorsementCount', 1);
+ try {
+ // Check if the user has privileges to endorse the post
+ const canEndorse = await privileges.posts.can('endorse', postId, socket.uid);
+ if (!canEndorse) {
+ throw new Error('You do not have the privilege to endorse this post.');
+ }
- // Optionally, you can store the user ID in a set of endorsers
- await db.setAdd(`post:${postId}:endorsers`, socket.uid);
+ // Add the endorsement to the database (this depends on your schema)
+ // Example: Increment endorsement count for the post
+ await db.incrObjectFieldBy(`post:${postId}`, 'endorsementCount', 1);
- // Get the updated endorsement count
- const endorsementCount = await db.getObjectField(`post:${postId}`, 'endorsementCount');
+ // Optionally, you can store the user ID in a set of endorsers
+ await db.setAdd(`post:${postId}:endorsers`, socket.uid);
- // Return the updated endorsement count
- socket.emit('plugins.endorse.post', null, { endorsements: endorsementCount });
+ // Get the updated endorsement count
+ const endorsementCount = await db.getObjectField(`post:${postId}`, 'endorsementCount');
- } catch (error) {
- console.error('Error endorsing post:', error);
- socket.emit('plugins.endorse.post', error);
- }
+ // Return the updated endorsement count
+ socket.emit('plugins.endorse.post', null, { endorsements: endorsementCount });
+ } catch (error) {
+ console.error('Error endorsing post:', error);
+ socket.emit('plugins.endorse.post', error);
+ }
};
diff --git a/test/topics/endorse.js b/test/topics/endorse.js
index 97b4a228ce..a8693f4ad0 100644
--- a/test/topics/endorse.js
+++ b/test/topics/endorse.js
@@ -14,16 +14,18 @@ define('forum/topic/endorse.tests', [
describe('Endorse Feature Client-Side Tests', () => {
let $endorseButton;
let apiPutStub;
+ let apiDeleteStub;
let alertSuccessStub;
let alertErrorStub;
beforeEach(() => {
// Set up DOM element
- $endorseButton = $('');
+ $endorseButton = $('');
$('body').append($endorseButton);
// Stub API methods
apiPutStub = sinon.stub(api, 'put');
+ apiDeleteStub = sinon.stub(api, 'delete');
alertSuccessStub = sinon.stub(alerts, 'success');
alertErrorStub = sinon.stub(alerts, 'error');
@@ -34,6 +36,7 @@ define('forum/topic/endorse.tests', [
afterEach(() => {
// Restore stubs and remove elements
apiPutStub.restore();
+ apiDeleteStub.restore();
alertSuccessStub.restore();
alertErrorStub.restore();
$endorseButton.remove();
@@ -42,27 +45,29 @@ define('forum/topic/endorse.tests', [
it('should trigger endorse action when endorse button is clicked', (done) => {
apiPutStub.callsFake((endpoint, data, callback) => {
assert.strictEqual(endpoint, '/posts/123/endorse');
- callback(null, { endorsed: true });
+ callback(null, { endorsed: true, endorsements: 1 });
});
$endorseButton.click();
setTimeout(() => {
assert(apiPutStub.calledOnce, 'API.put should be called once');
- assert(alertSuccessStub.calledWith('Post successfully endorsed!'), 'Success alert should be shown');
+ assert(alertSuccessStub.calledWith('[[topic:post_endorsed]]'), 'Success alert should be shown');
done();
}, 100);
});
it('should update UI after endorsing a post', (done) => {
apiPutStub.callsFake((endpoint, data, callback) => {
- callback(null, { endorsed: true });
+ callback(null, { endorsed: true, endorsements: 1 });
});
$endorseButton.click();
setTimeout(() => {
assert.strictEqual($endorseButton.text(), 'Endorsed');
+ assert.strictEqual($endorseButton.attr('data-endorsed'), 'true');
+ assert.strictEqual($endorseButton.find('.endorsement-count').text(), '1');
done();
}, 100);
});
@@ -75,7 +80,28 @@ define('forum/topic/endorse.tests', [
$endorseButton.click();
setTimeout(() => {
- assert(alertErrorStub.calledWith('Error endorsing post: Test error'), 'Error alert should be shown');
+ assert(alertErrorStub.calledWith('[[error:endorsing_post]]'), 'Error alert should be shown');
+ done();
+ }, 100);
+ });
+
+ it('should unendorse a post when clicked on an endorsed post', (done) => {
+ $endorseButton.attr('data-endorsed', 'true').text('Endorsed');
+ $endorseButton.find('.endorsement-count').text('1');
+
+ apiDeleteStub.callsFake((endpoint, data, callback) => {
+ assert.strictEqual(endpoint, '/posts/123/endorse');
+ callback(null, { endorsed: false, endorsements: 0 });
+ });
+
+ $endorseButton.click();
+
+ setTimeout(() => {
+ assert(apiDeleteStub.calledOnce, 'API.delete should be called once');
+ assert.strictEqual($endorseButton.text(), 'Endorse');
+ assert.strictEqual($endorseButton.attr('data-endorsed'), 'false');
+ assert.strictEqual($endorseButton.find('.endorsement-count').text(), '0');
+ assert(alertSuccessStub.calledWith('[[topic:post_unendorsed]]'), 'Success alert should be shown');
done();
}, 100);
});