mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +01:00
feat: allow groups to specify which cids to show member posts from (#8875)
* feat: allow groups to specify which cids to show member posts from * docs: fix tests for openapi * fix: test breakage caused by improper conditional * feat: server-side checking of memberPostCids for validity * feat: admin panel template update to select categories to include * refactor: privilege helpers.isUserAllowedTo ... to helpers.isAllowedTo, allowing group names to be passed in
This commit is contained in:
@@ -40,6 +40,8 @@
|
||||
"details.member_count": "Member Count",
|
||||
"details.creation_date": "Creation Date",
|
||||
"details.description": "Description",
|
||||
"details.member-post-cids": "Categories to display posts from",
|
||||
"details.member-post-cids-help": "<strong>Note</strong>: Selecting no categories will assume all categories are included. Use <code>ctrl</code> and <code>shift</code> to select multiple options.",
|
||||
"details.badge_preview": "Badge Preview",
|
||||
"details.change_icon": "Change Icon",
|
||||
"details.change_label_colour": "Change Label Colour",
|
||||
|
||||
@@ -42,6 +42,11 @@ GroupFullObject:
|
||||
textColor:
|
||||
type: string
|
||||
description: A six-character hexadecimal colour code
|
||||
memberPostCids:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
example: [1, 2, 3]
|
||||
icon:
|
||||
type: string
|
||||
description: A FontAwesome icon string
|
||||
@@ -56,6 +61,32 @@ GroupFullObject:
|
||||
type: string
|
||||
descriptionParsed:
|
||||
type: string
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
imageClass:
|
||||
type: string
|
||||
members:
|
||||
type: array
|
||||
items:
|
||||
@@ -131,3 +162,8 @@ GroupDataObject:
|
||||
description: "`createtime` rendered as an ISO 8601 format"
|
||||
cover:position:
|
||||
type: string
|
||||
memberPostCids:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
example: [1, 2, 3]
|
||||
@@ -64,6 +64,11 @@ get:
|
||||
type: string
|
||||
ownerUid:
|
||||
type: number
|
||||
memberPostCids:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
example: [1, 2, 3]
|
||||
required:
|
||||
- name
|
||||
- description
|
||||
|
||||
@@ -58,6 +58,11 @@ get:
|
||||
type: string
|
||||
cover:position:
|
||||
type: string
|
||||
memberPostCids:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
example: [1, 2, 3]
|
||||
members:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -37,6 +37,11 @@ post:
|
||||
enum: [0, 1]
|
||||
createtime:
|
||||
type: number
|
||||
memberPostCids:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
example: [1, 2, 3]
|
||||
required:
|
||||
- name
|
||||
responses:
|
||||
|
||||
@@ -80,6 +80,7 @@ define('admin/manage/group', [
|
||||
userTitleEnabled: $('#group-userTitleEnabled').is(':checked'),
|
||||
private: $('#group-private').is(':checked'),
|
||||
hidden: $('#group-hidden').is(':checked'),
|
||||
memberPostCids: $('#memberPostCids').val(),
|
||||
disableJoinRequests: $('#group-disableJoinRequests').is(':checked'),
|
||||
disableLeave: $('#group-disableLeave').is(':checked'),
|
||||
},
|
||||
|
||||
@@ -75,6 +75,7 @@ function modifyGroup(group, fields) {
|
||||
group.icon = validator.escape(String(group.icon || ''));
|
||||
group.createtimeISO = utils.toISOString(group.createtime);
|
||||
group.private = ([null, undefined].includes(group.private)) ? 1 : group.private;
|
||||
group.memberPostCids = (group.memberPostCids || '').split(',').map(cid => parseInt(cid, 10)).filter(Boolean);
|
||||
|
||||
group['cover:thumb:url'] = group['cover:thumb:url'] || group['cover:url'];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const user = require('../user');
|
||||
const categories = require('../categories');
|
||||
const db = require('../database');
|
||||
const plugins = require('../plugins');
|
||||
const slugify = require('../slugify');
|
||||
@@ -119,9 +120,10 @@ Groups.get = async function (groupName, options) {
|
||||
stop = (parseInt(options.userListCount, 10) || 4) - 1;
|
||||
}
|
||||
|
||||
const [groupData, members, pending, invited, isMember, isPending, isInvited, isOwner] = await Promise.all([
|
||||
const [groupData, members, selectCategories, pending, invited, isMember, isPending, isInvited, isOwner] = await Promise.all([
|
||||
Groups.getGroupData(groupName),
|
||||
Groups.getOwnersAndMembers(groupName, options.uid, 0, stop),
|
||||
categories.buildForSelect(groupName, 'topics:read', []),
|
||||
Groups.getUsersFromSet('group:' + groupName + ':pending', ['username', 'userslug', 'picture']),
|
||||
Groups.getUsersFromSet('group:' + groupName + ':invited', ['username', 'userslug', 'picture']),
|
||||
Groups.isMember(options.uid, groupName),
|
||||
@@ -135,6 +137,10 @@ Groups.get = async function (groupName, options) {
|
||||
}
|
||||
const descriptionParsed = await plugins.fireHook('filter:parse.raw', groupData.description);
|
||||
groupData.descriptionParsed = descriptionParsed;
|
||||
groupData.categories = selectCategories.map((category) => {
|
||||
category.selected = groupData.memberPostCids.includes(category.cid);
|
||||
return category;
|
||||
});
|
||||
groupData.members = members;
|
||||
groupData.membersNextStart = stop + 1;
|
||||
groupData.pending = pending.filter(Boolean);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../database');
|
||||
const groups = require('.');
|
||||
const privileges = require('../privileges');
|
||||
const posts = require('../posts');
|
||||
|
||||
@@ -13,6 +14,10 @@ module.exports = function (Groups) {
|
||||
let groupNames = await Groups.getUserGroupMembership('groups:visible:createtime', [postData.uid]);
|
||||
groupNames = groupNames[0];
|
||||
|
||||
// Only process those groups that have the cid in its memberPostCids setting (or no setting at all)
|
||||
const groupsCids = await groups.getGroupsFields(groupNames, ['memberPostCids']);
|
||||
groupNames = groupNames.filter((groupName, idx) => !groupsCids[idx].memberPostCids.length || groupsCids[idx].memberPostCids.includes(postData.cid));
|
||||
|
||||
const keys = groupNames.map(groupName => 'group:' + groupName + ':member:pids');
|
||||
await db.sortedSetsAdd(keys, postData.timestamp, postData.pid);
|
||||
await Promise.all(groupNames.map(name => truncateMemberPosts(name)));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const winston = require('winston');
|
||||
|
||||
const categories = require('../categories');
|
||||
const plugins = require('../plugins');
|
||||
const slugify = require('../slugify');
|
||||
const db = require('../database');
|
||||
@@ -18,11 +19,10 @@ module.exports = function (Groups) {
|
||||
throw new Error('[[error:no-group]]');
|
||||
}
|
||||
|
||||
const result = await plugins.fireHook('filter:group.update', {
|
||||
({ values } = await plugins.fireHook('filter:group.update', {
|
||||
groupName: groupName,
|
||||
values: values,
|
||||
});
|
||||
values = result.values;
|
||||
}));
|
||||
|
||||
const payload = {
|
||||
description: values.description || '',
|
||||
@@ -66,6 +66,12 @@ module.exports = function (Groups) {
|
||||
if (values.hasOwnProperty('hidden')) {
|
||||
await updateVisibility(groupName, values.hidden);
|
||||
}
|
||||
|
||||
if (values.hasOwnProperty('memberPostCids')) {
|
||||
const validCids = await categories.getCidsByPrivilege('categories:cid', groupName, 'topics:read');
|
||||
payload.memberPostCids = values.memberPostCids.filter(cid => validCids.includes(cid)).join(',') || '';
|
||||
}
|
||||
|
||||
await db.setObject('group:' + groupName, payload);
|
||||
await Groups.renameGroup(groupName, values.name);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const utils = require('../utils');
|
||||
|
||||
module.exports = function (Plugins) {
|
||||
Plugins.deprecatedHooks = {
|
||||
|
||||
'filter:privileges:isUserAllowedTo': 'filter:privileges:isAllowedTo',
|
||||
};
|
||||
|
||||
Plugins.internals = {
|
||||
|
||||
@@ -144,7 +144,7 @@ module.exports = function (privileges) {
|
||||
|
||||
privileges.admin.get = async function (uid) {
|
||||
const [userPrivileges, isAdministrator] = await Promise.all([
|
||||
helpers.isUserAllowedTo(privileges.admin.userPrivilegeList, uid, 0),
|
||||
helpers.isAllowedTo(privileges.admin.userPrivilegeList, uid, 0),
|
||||
user.isAdministrator(uid),
|
||||
]);
|
||||
|
||||
@@ -157,7 +157,7 @@ module.exports = function (privileges) {
|
||||
|
||||
privileges.admin.can = async function (privilege, uid) {
|
||||
const [isUserAllowedTo, isAdministrator] = await Promise.all([
|
||||
helpers.isUserAllowedTo(privilege, uid, [0]),
|
||||
helpers.isAllowedTo(privilege, uid, [0]),
|
||||
user.isAdministrator(uid),
|
||||
]);
|
||||
return isAdministrator || isUserAllowedTo[0];
|
||||
|
||||
@@ -46,7 +46,7 @@ module.exports = function (privileges) {
|
||||
const privs = ['topics:create', 'topics:read', 'topics:tag', 'read'];
|
||||
|
||||
const [userPrivileges, isAdministrator, isModerator] = await Promise.all([
|
||||
helpers.isUserAllowedTo(privs, uid, cid),
|
||||
helpers.isAllowedTo(privs, uid, cid),
|
||||
user.isAdministrator(uid),
|
||||
user.isModerator(uid, cid),
|
||||
]);
|
||||
@@ -80,7 +80,7 @@ module.exports = function (privileges) {
|
||||
if (!cid) {
|
||||
return false;
|
||||
}
|
||||
const results = await helpers.isUserAllowedTo(privilege, uid, Array.isArray(cid) ? cid : [cid]);
|
||||
const results = await helpers.isAllowedTo(privilege, uid, Array.isArray(cid) ? cid : [cid]);
|
||||
|
||||
if (Array.isArray(results) && results.length) {
|
||||
return Array.isArray(cid) ? results : results[0];
|
||||
@@ -113,8 +113,8 @@ module.exports = function (privileges) {
|
||||
privileges.categories.getBase = async function (privilege, cids, uid) {
|
||||
return await utils.promiseParallel({
|
||||
categories: categories.getCategoriesFields(cids, ['disabled']),
|
||||
allowedTo: helpers.isUserAllowedTo(privilege, uid, cids),
|
||||
view_deleted: helpers.isUserAllowedTo('posts:view_deleted', uid, cids),
|
||||
allowedTo: helpers.isAllowedTo(privilege, uid, cids),
|
||||
view_deleted: helpers.isAllowedTo('posts:view_deleted', uid, cids),
|
||||
isAdmin: user.isAdministrator(uid),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ module.exports = function (privileges) {
|
||||
|
||||
privileges.global.get = async function (uid) {
|
||||
const [userPrivileges, isAdministrator] = await Promise.all([
|
||||
helpers.isUserAllowedTo(privileges.global.userPrivilegeList, uid, 0),
|
||||
helpers.isAllowedTo(privileges.global.userPrivilegeList, uid, 0),
|
||||
user.isAdministrator(uid),
|
||||
]);
|
||||
|
||||
@@ -88,7 +88,7 @@ module.exports = function (privileges) {
|
||||
privileges.global.can = async function (privilege, uid) {
|
||||
const [isAdministrator, isUserAllowedTo] = await Promise.all([
|
||||
user.isAdministrator(uid),
|
||||
helpers.isUserAllowedTo(privilege, uid, [0]),
|
||||
helpers.isAllowedTo(privilege, uid, [0]),
|
||||
]);
|
||||
return isAdministrator || isUserAllowedTo[0];
|
||||
};
|
||||
|
||||
@@ -26,26 +26,45 @@ helpers.isUsersAllowedTo = async function (privilege, uids, cid) {
|
||||
return result.allowed;
|
||||
};
|
||||
|
||||
helpers.isUserAllowedTo = async function (privilege, uid, cid) {
|
||||
helpers.isAllowedTo = async function (privilege, uidOrGroupName, cid) {
|
||||
let allowed;
|
||||
if (Array.isArray(privilege) && !Array.isArray(cid)) {
|
||||
allowed = await isUserAllowedToPrivileges(privilege, uid, cid);
|
||||
allowed = await isAllowedToPrivileges(privilege, uidOrGroupName, cid);
|
||||
} else if (Array.isArray(cid) && !Array.isArray(privilege)) {
|
||||
allowed = await isUserAllowedToCids(privilege, uid, cid);
|
||||
allowed = await isAllowedToCids(privilege, uidOrGroupName, cid);
|
||||
}
|
||||
if (allowed) {
|
||||
const result = await plugins.fireHook('filter:privileges:isUserAllowedTo', { allowed: allowed, privilege: privilege, uid: uid, cid: cid });
|
||||
return result.allowed;
|
||||
({ allowed } = await plugins.fireHook('filter:privileges:isUserAllowedTo', { allowed: allowed, privilege: privilege, uid: uidOrGroupName, cid: cid }));
|
||||
({ allowed } = await plugins.fireHook('filter:privileges:isAllowedTo', { allowed: allowed, privilege: privilege, uid: uidOrGroupName, cid: cid }));
|
||||
return allowed;
|
||||
}
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
};
|
||||
|
||||
async function isUserAllowedToCids(privilege, uid, cids) {
|
||||
async function isAllowedToCids(privilege, uidOrGroupName, cids) {
|
||||
if (!privilege) {
|
||||
return cids.map(() => false);
|
||||
}
|
||||
if (parseInt(uid, 10) <= 0) {
|
||||
return await isSystemGroupAllowedToCids(privilege, uid, cids);
|
||||
|
||||
// Group handling
|
||||
if (isNaN(parseInt(uidOrGroupName, 10)) && (uidOrGroupName || '').length) {
|
||||
const groupKeys = [];
|
||||
cids.forEach(function (cid) {
|
||||
groupKeys.push('cid:' + cid + ':privileges:groups:' + privilege);
|
||||
});
|
||||
const sets = await Promise.all([
|
||||
groups.isMemberOfGroups(uidOrGroupName, groupKeys),
|
||||
groups.isMemberOfGroups('registered-users', groupKeys),
|
||||
]);
|
||||
return sets[0].reduce((memo, cur, idx) => {
|
||||
memo.push(cur || sets[1][idx]);
|
||||
return memo;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// User handling
|
||||
if (parseInt(uidOrGroupName, 10) <= 0) {
|
||||
return await isSystemGroupAllowedToCids(privilege, uidOrGroupName, cids);
|
||||
}
|
||||
|
||||
const userKeys = [];
|
||||
@@ -55,12 +74,29 @@ async function isUserAllowedToCids(privilege, uid, cids) {
|
||||
groupKeys.push('cid:' + cid + ':privileges:groups:' + privilege);
|
||||
});
|
||||
|
||||
return await checkIfAllowed(uid, userKeys, groupKeys);
|
||||
return await checkIfAllowed(uidOrGroupName, userKeys, groupKeys);
|
||||
}
|
||||
|
||||
async function isUserAllowedToPrivileges(privileges, uid, cid) {
|
||||
if (parseInt(uid, 10) <= 0) {
|
||||
return await isSystemGroupAllowedToPrivileges(privileges, uid, cid);
|
||||
async function isAllowedToPrivileges(privileges, uidOrGroupName, cid) {
|
||||
// Group handling
|
||||
if (isNaN(parseInt(uidOrGroupName, 10)) && (uidOrGroupName || '').length) {
|
||||
const groupKeys = [];
|
||||
privileges.forEach(function (privilege) {
|
||||
groupKeys.push('cid:' + cid + ':privileges:groups:' + privilege);
|
||||
});
|
||||
const sets = await Promise.all([
|
||||
groups.isMemberOfGroups(uidOrGroupName, groupKeys),
|
||||
groups.isMemberOfGroups('registered-users', groupKeys),
|
||||
]);
|
||||
return sets[0].reduce((memo, cur, idx) => {
|
||||
memo.push(cur || sets[1][idx]);
|
||||
return memo;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// User handling
|
||||
if (parseInt(uidOrGroupName, 10) <= 0) {
|
||||
return await isSystemGroupAllowedToPrivileges(privileges, uidOrGroupName, cid);
|
||||
}
|
||||
|
||||
const userKeys = [];
|
||||
@@ -70,7 +106,7 @@ async function isUserAllowedToPrivileges(privileges, uid, cid) {
|
||||
groupKeys.push('cid:' + cid + ':privileges:groups:' + privilege);
|
||||
});
|
||||
|
||||
return await checkIfAllowed(uid, userKeys, groupKeys);
|
||||
return await checkIfAllowed(uidOrGroupName, userKeys, groupKeys);
|
||||
}
|
||||
|
||||
async function checkIfAllowed(uid, userKeys, groupKeys) {
|
||||
|
||||
@@ -25,11 +25,11 @@ module.exports = function (privileges) {
|
||||
isAdmin: user.isAdministrator(uid),
|
||||
isModerator: user.isModerator(uid, uniqueCids),
|
||||
isOwner: posts.isOwner(pids, uid),
|
||||
'topics:read': helpers.isUserAllowedTo('topics:read', uid, uniqueCids),
|
||||
read: helpers.isUserAllowedTo('read', uid, uniqueCids),
|
||||
'posts:edit': helpers.isUserAllowedTo('posts:edit', uid, uniqueCids),
|
||||
'posts:history': helpers.isUserAllowedTo('posts:history', uid, uniqueCids),
|
||||
'posts:view_deleted': helpers.isUserAllowedTo('posts:view_deleted', uid, uniqueCids),
|
||||
'topics:read': helpers.isAllowedTo('topics:read', uid, uniqueCids),
|
||||
read: helpers.isAllowedTo('read', uid, uniqueCids),
|
||||
'posts:edit': helpers.isAllowedTo('posts:edit', uid, uniqueCids),
|
||||
'posts:history': helpers.isAllowedTo('posts:history', uid, uniqueCids),
|
||||
'posts:view_deleted': helpers.isAllowedTo('posts:view_deleted', uid, uniqueCids),
|
||||
});
|
||||
|
||||
const isModerator = _.zipObject(uniqueCids, results.isModerator);
|
||||
|
||||
@@ -23,7 +23,7 @@ module.exports = function (privileges) {
|
||||
];
|
||||
const topicData = await topics.getTopicFields(tid, ['cid', 'uid', 'locked', 'deleted']);
|
||||
const [userPrivileges, isAdministrator, isModerator, disabled] = await Promise.all([
|
||||
helpers.isUserAllowedTo(privs, uid, topicData.cid),
|
||||
helpers.isAllowedTo(privs, uid, topicData.cid),
|
||||
user.isAdministrator(uid),
|
||||
user.isModerator(uid, topicData.cid),
|
||||
categories.getCategoryField(topicData.cid, 'disabled'),
|
||||
@@ -121,7 +121,7 @@ module.exports = function (privileges) {
|
||||
user.isModerator(uid, topicData.cid),
|
||||
user.isAdministrator(uid),
|
||||
topics.isOwner(tid, uid),
|
||||
helpers.isUserAllowedTo('topics:delete', uid, [topicData.cid]),
|
||||
helpers.isAllowedTo('topics:delete', uid, [topicData.cid]),
|
||||
]);
|
||||
|
||||
if (isAdministrator) {
|
||||
|
||||
@@ -41,7 +41,7 @@ module.exports = function (privileges) {
|
||||
return await filterIsModerator(cids, uid, cids.map(() => true));
|
||||
}
|
||||
const uniqueCids = _.uniq(cids);
|
||||
const isAllowed = await helpers.isUserAllowedTo('moderate', uid, uniqueCids);
|
||||
const isAllowed = await helpers.isAllowedTo('moderate', uid, uniqueCids);
|
||||
|
||||
const cidToIsAllowed = _.zipObject(uniqueCids, isAllowed);
|
||||
const isModerator = cids.map(cid => cidToIsAllowed[cid]);
|
||||
|
||||
@@ -95,6 +95,22 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="memberPostCids">[[groups:details.member-post-cids]]</label>
|
||||
<select multiple="true" name="memberPostCids" id="memberPostCids" class="form-control" size="15">
|
||||
{{{each group.categories}}}
|
||||
<option value="{categories.cid}"{{{ if ../selected }}} selected{{{ end }}}>
|
||||
{../level}{../name}
|
||||
</option>
|
||||
{{{end}}}
|
||||
</select>
|
||||
<p class="help-block">[[groups:details.member-post-cids-help]]</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<fieldset>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
||||
Reference in New Issue
Block a user