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
isekai-main
Barış Soner Uşaklı 3 years ago
parent b8896d1328
commit c4042c70de

@ -72,6 +72,8 @@
"bootbox.ok": "OK", "bootbox.ok": "OK",
"bootbox.cancel": "Cancel", "bootbox.cancel": "Cancel",
"bootbox.confirm": "Confirm", "bootbox.confirm": "Confirm",
"bootbox.submit": "Submit",
"bootbox.send": "Send",
"cover.dragging_title": "Cover Photo Positioning", "cover.dragging_title": "Cover Photo Positioning",
"cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"",

@ -54,6 +54,7 @@
"users-csv-exported": "Users csv exported, click to download", "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-accepted": "Your queued post has been accepted. Click here to see your post.",
"post-queue-rejected": "Your queued post has been rejected.", "post-queue-rejected": "Your queued post has been rejected.",
"post-queue-notify": "Queued post received a notification:<br/>\"%1\"",
"email-confirmed": "Email Confirmed", "email-confirmed": "Email Confirmed",
"email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",

@ -14,5 +14,8 @@
"reply": "Reply", "reply": "Reply",
"topic": "Topic", "topic": "Topic",
"accept": "Accept", "accept": "Accept",
"reject": "Reject" "reject": "Reject",
"remove": "Remove",
"notify": "Notify",
"notify-user": "Notify User"
} }

@ -238,6 +238,8 @@ paths:
$ref: 'read/flags/flagId.yaml' $ref: 'read/flags/flagId.yaml'
/api/post-queue: /api/post-queue:
$ref: 'read/post-queue.yaml' $ref: 'read/post-queue.yaml'
"/api/post-queue/{id}":
$ref: 'read/post-queue.yaml'
/api/ip-blacklist: /api/ip-blacklist:
$ref: 'read/ip-blacklist.yaml' $ref: 'read/ip-blacklist.yaml'
/api/registration-queue: /api/registration-queue:

@ -2,8 +2,8 @@
define('forum/post-queue', [ define('forum/post-queue', [
'categoryFilter', 'categorySelector', 'api', 'alerts', 'categoryFilter', 'categorySelector', 'api', 'alerts', 'bootbox',
], function (categoryFilter, categorySelector, api, alerts) { ], function (categoryFilter, categorySelector, api, alerts, bootbox) {
const PostQueue = {}; const PostQueue = {};
PostQueue.init = function () { PostQueue.init = function () {
@ -13,23 +13,45 @@ define('forum/post-queue', [
privilege: 'moderate', 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: '<textarea class="form-control"></textarea>',
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 parent = $(this).parents('[data-id]');
const action = $(this).attr('data-action'); const action = $(this).attr('data-action');
const id = parent.attr('data-id'); const id = parent.attr('data-id');
const listContainer = parent.get(0).parentNode; const listContainer = parent.get(0).parentNode;
if (!['accept', 'reject'].some(function (valid) { if (!['accept', 'reject', 'notify'].includes(action)) {
return action === valid;
})) {
return; 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) { if (err) {
return alerts.error(err); return alerts.error(err);
} }
if (action === 'accept' || action === 'reject') {
parent.remove(); parent.remove();
}
if (listContainer.childElementCount === 0) { if (listContainer.childElementCount === 0) {
ajaxify.refresh(); ajaxify.refresh();

@ -1,5 +1,7 @@
'use strict'; 'use strict';
const validator = require('validator');
const user = require('../user'); const user = require('../user');
const posts = require('../posts'); const posts = require('../posts');
const flags = require('../flags'); const flags = require('../flags');
@ -149,29 +151,25 @@ modsController.flags.detail = async function (req, res, next) {
}; };
modsController.postQueue = async function (req, res, next) { modsController.postQueue = async function (req, res, next) {
// Admins, global mods, and individual mods only if (!req.loggedIn) {
const isPrivileged = await user.isPrivileged(req.uid);
if (!isPrivileged) {
return next(); return next();
} }
const { id } = req.params;
const { cid } = req.query; const { cid } = req.query;
const page = parseInt(req.query.page, 10) || 1; const page = parseInt(req.query.page, 10) || 1;
const postsPerPage = 20; const postsPerPage = 20;
let postData = await posts.getQueuedPosts(); let postData = await posts.getQueuedPosts({ id: id });
const [isAdminOrGlobalMod, moderatedCids, categoriesData] = await Promise.all([ const [isAdmin, isGlobalMod, moderatedCids, categoriesData] = await Promise.all([
user.isAdminOrGlobalMod(req.uid), user.isAdministrator(req.uid),
user.isGlobalModerator(req.uid),
user.getModeratedCids(req.uid), user.getModeratedCids(req.uid),
helpers.getSelectedCategory(cid), helpers.getSelectedCategory(cid),
]); ]);
if (cid && !moderatedCids.includes(Number(cid)) && !isAdminOrGlobalMod) {
return next();
}
postData = postData.filter(p => p && postData = postData.filter(p => p &&
(!categoriesData.selectedCids.length || categoriesData.selectedCids.includes(p.category.cid)) && (!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 } = await plugins.hooks.fire('filter:post-queue.get', {
posts: postData, posts: postData,
@ -182,13 +180,19 @@ modsController.postQueue = async function (req, res, next) {
const start = (page - 1) * postsPerPage; const start = (page - 1) * postsPerPage;
const stop = start + postsPerPage - 1; const stop = start + postsPerPage - 1;
postData = postData.slice(start, stop + 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', { res.render('post-queue', {
title: '[[pages:post-queue]]', title: '[[pages:post-queue]]',
posts: postData, posts: postData,
isAdmin: isAdmin,
canAccept: isAdmin || isGlobalMod || !!moderatedCids.length,
...categoriesData, ...categoriesData,
allCategoriesUrl: `post-queue${helpers.buildQueryString(req.query, 'cid', '')}`, allCategoriesUrl: `post-queue${helpers.buildQueryString(req.query, 'cid', '')}`,
pagination: pagination.create(page, pageCount), pagination: pagination.create(page, pageCount),
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:post-queue]]' }]), breadcrumbs: helpers.buildBreadcrumbs(crumbs),
}); });
}; };

@ -64,6 +64,7 @@ middleware.renderHeader = async function renderHeader(req, res, data) {
'brand:logo:display': meta.config['brand:logo'] ? '' : 'hide', 'brand:logo:display': meta.config['brand:logo'] ? '' : 'hide',
allowRegistration: registrationType === 'normal', allowRegistration: registrationType === 'normal',
searchEnabled: plugins.hooks.hasListeners('filter:search.query'), searchEnabled: plugins.hooks.hasListeners('filter:search.query'),
postQueueEnabled: !!meta.config.postQueue,
config: res.locals.config, config: res.locals.config,
relative_path, relative_path,
bodyClass: data.bodyClass, bodyClass: data.bodyClass,

@ -42,7 +42,9 @@ module.exports = function (Posts) {
}); });
cache.set('post-queue', _.cloneDeep(postData)); cache.set('post-queue', _.cloneDeep(postData));
} }
if (filter.id) {
postData = postData.filter(p => p.id === filter.id);
}
if (options.metadata) { if (options.metadata) {
await Promise.all(postData.map(p => addMetaData(p))); await Promise.all(postData.map(p => addMetaData(p)));
} }
@ -161,7 +163,7 @@ module.exports = function (Posts) {
mergeId: 'post-queue', mergeId: 'post-queue',
bodyShort: '[[notifications:post_awaiting_review]]', bodyShort: '[[notifications:post_awaiting_review]]',
bodyLong: bodyLong, bodyLong: bodyLong,
path: '/post-queue', path: `/post-queue/${id}`,
}); });
await notifications.push(notifObj, uids); await notifications.push(notifObj, uids);
return { return {
@ -235,7 +237,7 @@ module.exports = function (Posts) {
Posts.removeFromQueue = async function (id) { Posts.removeFromQueue = async function (id) {
const data = await getParsedObject(id); const data = await getParsedObject(id);
if (!data) { if (!data) {
return; return null;
} }
await removeQueueNotification(id); await removeQueueNotification(id);
await db.sortedSetRemove('post:queue', id); await db.sortedSetRemove('post:queue', id);
@ -247,7 +249,7 @@ module.exports = function (Posts) {
Posts.submitFromQueue = async function (id) { Posts.submitFromQueue = async function (id) {
const data = await getParsedObject(id); const data = await getParsedObject(id);
if (!data) { if (!data) {
return; return null;
} }
if (data.type === 'topic') { if (data.type === 'topic') {
const result = await createTopic(data.data); const result = await createTopic(data.data);
@ -260,6 +262,10 @@ module.exports = function (Posts) {
return data; return data;
}; };
Posts.getFromQueue = async function (id) {
return await getParsedObject(id);
};
async function getParsedObject(id) { async function getParsedObject(id) {
const data = await db.getObject(`post:queue:${id}`); const data = await db.getObject(`post:queue:${id}`);
if (!data) { if (!data) {
@ -288,7 +294,7 @@ module.exports = function (Posts) {
} }
Posts.editQueuedContent = async function (uid, editData) { Posts.editQueuedContent = async function (uid, editData) {
const canEditQueue = await Posts.canEditQueue(uid, editData); const canEditQueue = await Posts.canEditQueue(uid, editData, 'edit');
if (!canEditQueue) { if (!canEditQueue) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
@ -309,7 +315,7 @@ module.exports = function (Posts) {
cache.del('post-queue'); cache.del('post-queue');
}; };
Posts.canEditQueue = async function (uid, editData) { Posts.canEditQueue = async function (uid, editData, action) {
const [isAdminOrGlobalMod, data] = await Promise.all([ const [isAdminOrGlobalMod, data] = await Promise.all([
user.isAdminOrGlobalMod(uid), user.isAdminOrGlobalMod(uid),
getParsedObject(editData.id), getParsedObject(editData.id),
@ -317,8 +323,8 @@ module.exports = function (Posts) {
if (!data) { if (!data) {
return false; return false;
} }
const selfPost = parseInt(uid, 10) === parseInt(data.uid, 10);
if (isAdminOrGlobalMod) { if (isAdminOrGlobalMod || ((action === 'reject' || action === 'edit') && selfPost)) {
return true; return true;
} }

@ -46,7 +46,7 @@ _mounts.main = (app, middleware, controllers) => {
_mounts.mod = (app, middleware, controllers) => { _mounts.mod = (app, middleware, controllers) => {
setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list); setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list);
setupPageRoute(app, '/flags/:flagId', middleware, [], controllers.mods.flags.detail); 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) => { _mounts.globalMod = (app, middleware, controllers) => {

@ -1,5 +1,7 @@
'use strict'; 'use strict';
const validator = require('validator');
const db = require('../database'); const db = require('../database');
const posts = require('../posts'); const posts = require('../posts');
const privileges = require('../privileges'); const privileges = require('../privileges');
@ -100,29 +102,41 @@ SocketPosts.getReplies = async function (socket, pid) {
}; };
SocketPosts.accept = async function (socket, data) { SocketPosts.accept = async function (socket, data) {
const result = await acceptOrReject(posts.submitFromQueue, socket, data); 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}`); await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`);
}
}; };
SocketPosts.reject = async function (socket, data) { SocketPosts.reject = async function (socket, data) {
const result = await acceptOrReject(posts.removeFromQueue, socket, data); 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, '/'); 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) { async function canEditQueue(socket, data, action) {
const canEditQueue = await posts.canEditQueue(socket.uid, data); const canEditQueue = await posts.canEditQueue(socket.uid, data, action);
if (!canEditQueue) { if (!canEditQueue) {
throw new Error('[[error:no-privileges]]'); 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 = { const notifData = {
type: type, type: type,
nid: `${type}-${targetUid}-${path}`, nid: `${type}-${targetUid}-${path}`,
bodyShort: type === 'post-queue-accepted' ? bodyShort: `[[notifications:post-queue-notify, ${notificationText}]]` || `[[notifications:${type}]]`,
'[[notifications:post-queue-accepted]]' : '[[notifications:post-queue-rejected]]',
path: path, path: path,
}; };
if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) { if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) {

Loading…
Cancel
Save