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); });