From c4042c70decd628e5b880bd109515b47e4e16164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 16 Dec 2021 22:25:39 -0500 Subject: [PATCH] feat: #9506, allow seeing and editing your queued posts allow regular users access to post queue allow regular users to edit their queued post/topic title allow regular users to remove their post from post queue ability to send a notification to user without removing from post queue allow accessing single post queue items from notifications --- public/language/en-GB/modules.json | 2 ++ public/language/en-GB/notifications.json | 1 + public/language/en-GB/post-queue.json | 5 +++- public/openapi/read.yaml | 2 ++ public/src/client/post-queue.js | 38 +++++++++++++++++++----- src/controllers/mods.js | 30 +++++++++++-------- src/middleware/header.js | 1 + src/posts/queue.js | 22 +++++++++----- src/routes/index.js | 2 +- src/socket.io/posts.js | 34 ++++++++++++++------- 10 files changed, 96 insertions(+), 41 deletions(-) diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json index ded95a7130..02fa2adc97 100644 --- a/public/language/en-GB/modules.json +++ b/public/language/en-GB/modules.json @@ -72,6 +72,8 @@ "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", "cover.dragging_title": "Cover Photo Positioning", "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", diff --git a/public/language/en-GB/notifications.json b/public/language/en-GB/notifications.json index 1c84cb641e..67dc361e46 100644 --- a/public/language/en-GB/notifications.json +++ b/public/language/en-GB/notifications.json @@ -54,6 +54,7 @@ "users-csv-exported": "Users csv exported, click to download", "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", diff --git a/public/language/en-GB/post-queue.json b/public/language/en-GB/post-queue.json index bfaa367870..9ab4fc3ba7 100644 --- a/public/language/en-GB/post-queue.json +++ b/public/language/en-GB/post-queue.json @@ -14,5 +14,8 @@ "reply": "Reply", "topic": "Topic", "accept": "Accept", - "reject": "Reject" + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User" } \ No newline at end of file diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index 2adc245b18..a00a5bb27d 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -238,6 +238,8 @@ paths: $ref: 'read/flags/flagId.yaml' /api/post-queue: $ref: 'read/post-queue.yaml' + "/api/post-queue/{id}": + $ref: 'read/post-queue.yaml' /api/ip-blacklist: $ref: 'read/ip-blacklist.yaml' /api/registration-queue: diff --git a/public/src/client/post-queue.js b/public/src/client/post-queue.js index 9181769052..0be5043450 100644 --- a/public/src/client/post-queue.js +++ b/public/src/client/post-queue.js @@ -2,8 +2,8 @@ define('forum/post-queue', [ - 'categoryFilter', 'categorySelector', 'api', 'alerts', -], function (categoryFilter, categorySelector, api, alerts) { + 'categoryFilter', 'categorySelector', 'api', 'alerts', 'bootbox', +], function (categoryFilter, categorySelector, api, alerts, bootbox) { const PostQueue = {}; PostQueue.init = function () { @@ -13,23 +13,45 @@ define('forum/post-queue', [ privilege: 'moderate', }); - $('.posts-list').on('click', '[data-action]', function () { + $('.posts-list').on('click', '[data-action]', async function () { + function getMessage() { + return new Promise((resolve) => { + const modal = bootbox.dialog({ + title: '[[post-queue:notify-user]]', + message: '', + buttons: { + OK: { + label: '[[modules:bootbox.send]]', + callback: function () { + const val = modal.find('textarea').val(); + if (val) { + resolve(val); + } + }, + }, + }, + }); + }); + } const parent = $(this).parents('[data-id]'); const action = $(this).attr('data-action'); const id = parent.attr('data-id'); const listContainer = parent.get(0).parentNode; - if (!['accept', 'reject'].some(function (valid) { - return action === valid; - })) { + if (!['accept', 'reject', 'notify'].includes(action)) { return; } - socket.emit('posts.' + action, { id: id }, function (err) { + socket.emit('posts.' + action, { + id: id, + message: action === 'notify' ? await getMessage() : undefined, + }, function (err) { if (err) { return alerts.error(err); } - parent.remove(); + if (action === 'accept' || action === 'reject') { + parent.remove(); + } if (listContainer.childElementCount === 0) { ajaxify.refresh(); diff --git a/src/controllers/mods.js b/src/controllers/mods.js index cf305f6680..fe05f29019 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -1,5 +1,7 @@ 'use strict'; +const validator = require('validator'); + const user = require('../user'); const posts = require('../posts'); const flags = require('../flags'); @@ -149,29 +151,25 @@ modsController.flags.detail = async function (req, res, next) { }; modsController.postQueue = async function (req, res, next) { - // Admins, global mods, and individual mods only - const isPrivileged = await user.isPrivileged(req.uid); - if (!isPrivileged) { + if (!req.loggedIn) { return next(); } + const { id } = req.params; const { cid } = req.query; const page = parseInt(req.query.page, 10) || 1; const postsPerPage = 20; - let postData = await posts.getQueuedPosts(); - const [isAdminOrGlobalMod, moderatedCids, categoriesData] = await Promise.all([ - user.isAdminOrGlobalMod(req.uid), + let postData = await posts.getQueuedPosts({ id: id }); + const [isAdmin, isGlobalMod, moderatedCids, categoriesData] = await Promise.all([ + user.isAdministrator(req.uid), + user.isGlobalModerator(req.uid), user.getModeratedCids(req.uid), helpers.getSelectedCategory(cid), ]); - if (cid && !moderatedCids.includes(Number(cid)) && !isAdminOrGlobalMod) { - return next(); - } - postData = postData.filter(p => p && (!categoriesData.selectedCids.length || categoriesData.selectedCids.includes(p.category.cid)) && - (isAdminOrGlobalMod || moderatedCids.includes(Number(p.category.cid)))); + (isAdmin || isGlobalMod || moderatedCids.includes(Number(p.category.cid)) || req.uid === p.user.uid)); ({ posts: postData } = await plugins.hooks.fire('filter:post-queue.get', { posts: postData, @@ -182,13 +180,19 @@ modsController.postQueue = async function (req, res, next) { const start = (page - 1) * postsPerPage; const stop = start + postsPerPage - 1; postData = postData.slice(start, stop + 1); - + const crumbs = [{ text: '[[pages:post-queue]]', url: id ? '/post-queue' : undefined }]; + if (id && postData.length) { + const text = postData[0].data.tid ? '[[post-queue:reply]]' : '[[post-queue:topic]]'; + crumbs.push({ text: text }); + } res.render('post-queue', { title: '[[pages:post-queue]]', posts: postData, + isAdmin: isAdmin, + canAccept: isAdmin || isGlobalMod || !!moderatedCids.length, ...categoriesData, allCategoriesUrl: `post-queue${helpers.buildQueryString(req.query, 'cid', '')}`, pagination: pagination.create(page, pageCount), - breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:post-queue]]' }]), + breadcrumbs: helpers.buildBreadcrumbs(crumbs), }); }; diff --git a/src/middleware/header.js b/src/middleware/header.js index 28a8c36119..6304f967ba 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -64,6 +64,7 @@ middleware.renderHeader = async function renderHeader(req, res, data) { 'brand:logo:display': meta.config['brand:logo'] ? '' : 'hide', allowRegistration: registrationType === 'normal', searchEnabled: plugins.hooks.hasListeners('filter:search.query'), + postQueueEnabled: !!meta.config.postQueue, config: res.locals.config, relative_path, bodyClass: data.bodyClass, diff --git a/src/posts/queue.js b/src/posts/queue.js index 7e5790e166..c882b9f180 100644 --- a/src/posts/queue.js +++ b/src/posts/queue.js @@ -42,7 +42,9 @@ module.exports = function (Posts) { }); cache.set('post-queue', _.cloneDeep(postData)); } - + if (filter.id) { + postData = postData.filter(p => p.id === filter.id); + } if (options.metadata) { await Promise.all(postData.map(p => addMetaData(p))); } @@ -161,7 +163,7 @@ module.exports = function (Posts) { mergeId: 'post-queue', bodyShort: '[[notifications:post_awaiting_review]]', bodyLong: bodyLong, - path: '/post-queue', + path: `/post-queue/${id}`, }); await notifications.push(notifObj, uids); return { @@ -235,7 +237,7 @@ module.exports = function (Posts) { Posts.removeFromQueue = async function (id) { const data = await getParsedObject(id); if (!data) { - return; + return null; } await removeQueueNotification(id); await db.sortedSetRemove('post:queue', id); @@ -247,7 +249,7 @@ module.exports = function (Posts) { Posts.submitFromQueue = async function (id) { const data = await getParsedObject(id); if (!data) { - return; + return null; } if (data.type === 'topic') { const result = await createTopic(data.data); @@ -260,6 +262,10 @@ module.exports = function (Posts) { return data; }; + Posts.getFromQueue = async function (id) { + return await getParsedObject(id); + }; + async function getParsedObject(id) { const data = await db.getObject(`post:queue:${id}`); if (!data) { @@ -288,7 +294,7 @@ module.exports = function (Posts) { } Posts.editQueuedContent = async function (uid, editData) { - const canEditQueue = await Posts.canEditQueue(uid, editData); + const canEditQueue = await Posts.canEditQueue(uid, editData, 'edit'); if (!canEditQueue) { throw new Error('[[error:no-privileges]]'); } @@ -309,7 +315,7 @@ module.exports = function (Posts) { cache.del('post-queue'); }; - Posts.canEditQueue = async function (uid, editData) { + Posts.canEditQueue = async function (uid, editData, action) { const [isAdminOrGlobalMod, data] = await Promise.all([ user.isAdminOrGlobalMod(uid), getParsedObject(editData.id), @@ -317,8 +323,8 @@ module.exports = function (Posts) { if (!data) { return false; } - - if (isAdminOrGlobalMod) { + const selfPost = parseInt(uid, 10) === parseInt(data.uid, 10); + if (isAdminOrGlobalMod || ((action === 'reject' || action === 'edit') && selfPost)) { return true; } diff --git a/src/routes/index.js b/src/routes/index.js index 402384b0d3..3a7e72cdf6 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -46,7 +46,7 @@ _mounts.main = (app, middleware, controllers) => { _mounts.mod = (app, middleware, controllers) => { setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list); setupPageRoute(app, '/flags/:flagId', middleware, [], controllers.mods.flags.detail); - setupPageRoute(app, '/post-queue', middleware, [], controllers.mods.postQueue); + setupPageRoute(app, '/post-queue/:id?', middleware, [], controllers.mods.postQueue); }; _mounts.globalMod = (app, middleware, controllers) => { diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index cf5f7f9f47..a1f9b06c8d 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -1,5 +1,7 @@ 'use strict'; +const validator = require('validator'); + const db = require('../database'); const posts = require('../posts'); const privileges = require('../privileges'); @@ -100,29 +102,41 @@ SocketPosts.getReplies = async function (socket, pid) { }; SocketPosts.accept = async function (socket, data) { - const result = await acceptOrReject(posts.submitFromQueue, socket, data); - await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`); + await canEditQueue(socket, data, 'accept'); + const result = await posts.submitFromQueue(data.id); + if (result && socket.uid !== parseInt(result.uid, 10)) { + await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`); + } }; SocketPosts.reject = async function (socket, data) { - const result = await acceptOrReject(posts.removeFromQueue, socket, data); - await sendQueueNotification('post-queue-rejected', result.uid, '/'); + await canEditQueue(socket, data, 'reject'); + const result = await posts.removeFromQueue(data.id); + if (result && socket.uid !== parseInt(result.uid, 10)) { + await sendQueueNotification('post-queue-rejected', result.uid, '/'); + } +}; + +SocketPosts.notify = async function (socket, data) { + await canEditQueue(socket, data, 'notify'); + const result = await posts.getFromQueue(data.id); + if (result) { + await sendQueueNotification('post-queue-notify', result.uid, `/post-queue/${data.id}`, validator.escape(String(data.message))); + } }; -async function acceptOrReject(method, socket, data) { - const canEditQueue = await posts.canEditQueue(socket.uid, data); +async function canEditQueue(socket, data, action) { + const canEditQueue = await posts.canEditQueue(socket.uid, data, action); if (!canEditQueue) { throw new Error('[[error:no-privileges]]'); } - return await method(data.id); } -async function sendQueueNotification(type, targetUid, path) { +async function sendQueueNotification(type, targetUid, path, notificationText) { const notifData = { type: type, nid: `${type}-${targetUid}-${path}`, - bodyShort: type === 'post-queue-accepted' ? - '[[notifications:post-queue-accepted]]' : '[[notifications:post-queue-rejected]]', + bodyShort: `[[notifications:post-queue-notify, ${notificationText}]]` || `[[notifications:${type}]]`, path: path, }; if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) {