mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 02:55:58 +01:00 
			
		
		
		
	feat: closes #12453, filter events by user/group
This commit is contained in:
		| @@ -9,5 +9,9 @@ | |||||||
| 	"filter-type": "Event Type", | 	"filter-type": "Event Type", | ||||||
| 	"filter-start": "Start Date", | 	"filter-start": "Start Date", | ||||||
| 	"filter-end": "End Date", | 	"filter-end": "End Date", | ||||||
|  | 	"filter-user": "Filter by User", | ||||||
|  | 	"filter-user.placeholder": "Type user name to filter...", | ||||||
|  | 	"filter-group": "Filter by Group", | ||||||
|  | 	"filter-group.placeholder": "Type group name to filter...", | ||||||
| 	"filter-per-page": "Per Page" | 	"filter-per-page": "Per Page" | ||||||
| } | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
|  |  | ||||||
|  |  | ||||||
| define('admin/advanced/events', ['bootbox', 'alerts'], function (bootbox, alerts) { | define('admin/advanced/events', ['bootbox', 'alerts', 'autocomplete'], function (bootbox, alerts, autocomplete) { | ||||||
| 	const Events = {}; | 	const Events = {}; | ||||||
|  |  | ||||||
| 	Events.init = function () { | 	Events.init = function () { | ||||||
| @@ -30,6 +30,21 @@ define('admin/advanced/events', ['bootbox', 'alerts'], function (bootbox, alerts | |||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
|  | 		$('#user-group-select').on('change', function () { | ||||||
|  | 			const val = $(this).val(); | ||||||
|  | 			$('#username').toggleClass('hidden', val !== 'username'); | ||||||
|  | 			if (val !== 'username') { | ||||||
|  | 				$('#username').val(''); | ||||||
|  | 			} | ||||||
|  | 			$('#group').toggleClass('hidden', val !== 'group'); | ||||||
|  | 			if (val !== 'group') { | ||||||
|  | 				$('#group').val(''); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		autocomplete.user($('#username')); | ||||||
|  | 		autocomplete.group($('#group')); | ||||||
|  |  | ||||||
| 		$('#apply').on('click', Events.refresh); | 		$('#apply').on('click', Events.refresh); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -130,7 +130,11 @@ async function listPlugins() { | |||||||
|  |  | ||||||
| async function listEvents(count = 10) { | async function listEvents(count = 10) { | ||||||
| 	await db.init(); | 	await db.init(); | ||||||
| 	const eventData = await events.getEvents('', 0, count - 1); | 	const eventData = await events.getEvents({ | ||||||
|  | 		filter: '', | ||||||
|  | 		start: 0, | ||||||
|  | 		stop: count - 1, | ||||||
|  | 	}); | ||||||
| 	console.log(chalk.bold(`\nDisplaying last ${count} administrative events...`)); | 	console.log(chalk.bold(`\nDisplaying last ${count} administrative events...`)); | ||||||
| 	eventData.forEach((event) => { | 	eventData.forEach((event) => { | ||||||
| 		console.log(`  * ${chalk.green(String(event.timestampISO))} ${chalk.yellow(String(event.type))}${event.text ? ` ${event.text}` : ''} (uid: ${event.uid ? event.uid : 0})`); | 		console.log(`  * ${chalk.green(String(event.timestampISO))} ${chalk.yellow(String(event.type))}${event.text ? ` ${event.text}` : ''} (uid: ${event.uid ? event.uid : 0})`); | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ | |||||||
| const db = require('../../database'); | const db = require('../../database'); | ||||||
| const events = require('../../events'); | const events = require('../../events'); | ||||||
| const pagination = require('../../pagination'); | const pagination = require('../../pagination'); | ||||||
|  | const user = require('../../user'); | ||||||
|  | const groups = require('../../groups'); | ||||||
|  |  | ||||||
| const eventsController = module.exports; | const eventsController = module.exports; | ||||||
|  |  | ||||||
| @@ -11,18 +13,35 @@ eventsController.get = async function (req, res) { | |||||||
| 	const itemsPerPage = parseInt(req.query.perPage, 10) || 20; | 	const itemsPerPage = parseInt(req.query.perPage, 10) || 20; | ||||||
| 	const start = (page - 1) * itemsPerPage; | 	const start = (page - 1) * itemsPerPage; | ||||||
| 	const stop = start + itemsPerPage - 1; | 	const stop = start + itemsPerPage - 1; | ||||||
|  | 	let uids; | ||||||
|  | 	if (req.query.username) { | ||||||
|  | 		uids = [await user.getUidByUsername(req.query.username)]; | ||||||
|  | 	} else if (req.query.group) { | ||||||
|  | 		uids = await groups.getMembers(req.query.group, 0, -1); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Limit by date | 	// Limit by date | ||||||
| 	let from = req.query.start ? new Date(req.query.start) || undefined : undefined; | 	let from = req.query.start ? new Date(req.query.start) || undefined : undefined; | ||||||
| 	let to = req.query.end ? new Date(req.query.end) || undefined : new Date(); | 	let to = req.query.end ? new Date(req.query.end) || undefined : new Date(); | ||||||
| 	from = from && from.setHours(0, 0, 0, 0); // setHours returns a unix timestamp (Number, not Date) | 	from = from && from.setUTCHours(0, 0, 0, 0); // setHours returns a unix timestamp (Number, not Date) | ||||||
| 	to = to && to.setHours(23, 59, 59, 999); // setHours returns a unix timestamp (Number, not Date) | 	to = to && to.setUTCHours(23, 59, 59, 999); // setHours returns a unix timestamp (Number, not Date) | ||||||
|  |  | ||||||
| 	const currentFilter = req.query.type || ''; | 	const currentFilter = req.query.type || ''; | ||||||
|  |  | ||||||
| 	const [eventCount, eventData, counts] = await Promise.all([ | 	const [eventCount, eventData, counts] = await Promise.all([ | ||||||
| 		db.sortedSetCount(`events:time${currentFilter ? `:${currentFilter}` : ''}`, from || '-inf', to), | 		events.getEventCount({ | ||||||
| 		events.getEvents(currentFilter, start, stop, from || '-inf', to), | 			filter: currentFilter, | ||||||
|  | 			uids, | ||||||
|  | 			from: from || '-inf', | ||||||
|  | 			to, | ||||||
|  | 		}), | ||||||
|  | 		events.getEvents({ | ||||||
|  | 			filter: currentFilter, | ||||||
|  | 			uids, | ||||||
|  | 			start, | ||||||
|  | 			stop, | ||||||
|  | 			from: from || '-inf', | ||||||
|  | 			to, | ||||||
|  | 		}), | ||||||
| 		db.sortedSetsCard([''].concat(events.types).map(type => `events:time${type ? `:${type}` : ''}`)), | 		db.sortedSetsCard([''].concat(events.types).map(type => `events:time${type ? `:${type}` : ''}`)), | ||||||
| 	]); | 	]); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										114
									
								
								src/events.js
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								src/events.js
									
									
									
									
									
								
							| @@ -87,30 +87,114 @@ events.log = async function (data) { | |||||||
| 	const eid = await db.incrObjectField('global', 'nextEid'); | 	const eid = await db.incrObjectField('global', 'nextEid'); | ||||||
| 	data.timestamp = Date.now(); | 	data.timestamp = Date.now(); | ||||||
| 	data.eid = eid; | 	data.eid = eid; | ||||||
|  | 	const setKeys = [ | ||||||
|  | 		'events:time', | ||||||
|  | 		`events:time:${data.type}`, | ||||||
|  | 	]; | ||||||
|  | 	if (data.hasOwnProperty('uid') && data.uid) { | ||||||
|  | 		setKeys.push(`events:time:uid:${data.uid}`); | ||||||
|  | 	} | ||||||
| 	await Promise.all([ | 	await Promise.all([ | ||||||
| 		db.sortedSetsAdd([ | 		db.sortedSetsAdd(setKeys, data.timestamp, eid), | ||||||
| 			'events:time', |  | ||||||
| 			`events:time:${data.type}`, |  | ||||||
| 		], data.timestamp, eid), |  | ||||||
| 		db.setObject(`event:${eid}`, data), | 		db.setObject(`event:${eid}`, data), | ||||||
| 	]); | 	]); | ||||||
| 	plugins.hooks.fire('action:events.log', { data: data }); | 	plugins.hooks.fire('action:events.log', { data: data }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| events.getEvents = async function (filter, start, stop, from, to) { | // filter, start, stop, from(optional), to(optional), uids(optional) | ||||||
| 	// from/to optional | events.getEvents = async function (options) { | ||||||
| 	if (from === undefined) { | 	// backwards compatibility | ||||||
| 		from = '-inf'; | 	if (arguments.length > 1) { | ||||||
|  | 		// eslint-disable-next-line prefer-rest-params | ||||||
|  | 		const args = Array.prototype.slice.call(arguments); | ||||||
|  | 		options = { | ||||||
|  | 			filter: args[0], | ||||||
|  | 			start: args[1], | ||||||
|  | 			stop: args[2], | ||||||
|  | 			from: args[3], | ||||||
|  | 			to: args[4], | ||||||
|  | 		}; | ||||||
| 	} | 	} | ||||||
| 	if (to === undefined) { | 	// from/to optional | ||||||
| 		to = '+inf'; | 	const from = options.hasOwnProperty('from') ? options.from : '-inf'; | ||||||
|  | 	const to = options.hasOwnProperty('to') ? options.to : '+inf'; | ||||||
|  | 	const { filter, start, stop, uids } = options; | ||||||
|  | 	let eids = []; | ||||||
|  |  | ||||||
|  | 	if (Array.isArray(uids)) { | ||||||
|  | 		if (filter === '') { | ||||||
|  | 			eids = await db.getSortedSetRevRangeByScore( | ||||||
|  | 				uids.map(uid => `events:time:uid:${uid}`), | ||||||
|  | 				start, | ||||||
|  | 				stop === -1 ? -1 : stop - start + 1, | ||||||
|  | 				to, | ||||||
|  | 				from | ||||||
|  | 			); | ||||||
|  | 		} else { | ||||||
|  | 			eids = await Promise.all( | ||||||
|  | 				uids.map( | ||||||
|  | 					uid => db.getSortedSetRevIntersect({ | ||||||
|  | 						sets: [`events:time:uid:${uid}`, `events:time:${filter}`], | ||||||
|  | 						start: 0, | ||||||
|  | 						stop: -1, | ||||||
|  | 						weights: [1, 0], | ||||||
|  | 						withScores: true, | ||||||
|  | 					}) | ||||||
|  | 				) | ||||||
|  | 			); | ||||||
|  |  | ||||||
|  | 			eids = _.flatten(eids) | ||||||
|  | 				.filter( | ||||||
|  | 					i => (from === '-inf' || i.score >= from) && (to === '+inf' || i.score <= to) | ||||||
|  | 				) | ||||||
|  | 				.sort((a, b) => b.score - a.score) | ||||||
|  | 				.slice(start, stop + 1) | ||||||
|  | 				.map(i => i.value); | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		eids = await db.getSortedSetRevRangeByScore( | ||||||
|  | 			`events:time${filter ? `:${filter}` : ''}`, | ||||||
|  | 			start, | ||||||
|  | 			stop === -1 ? -1 : stop - start + 1, | ||||||
|  | 			to, | ||||||
|  | 			from | ||||||
|  | 		); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const eids = await db.getSortedSetRevRangeByScore(`events:time${filter ? `:${filter}` : ''}`, start, stop === -1 ? -1 : stop - start + 1, to, from); |  | ||||||
| 	return await events.getEventsByEventIds(eids); | 	return await events.getEventsByEventIds(eids); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | events.getEventCount = async (options) => { | ||||||
|  | 	const { filter, uids, from, to } = options; | ||||||
|  |  | ||||||
|  | 	if (Array.isArray(uids)) { | ||||||
|  | 		if (filter === '') { | ||||||
|  | 			const counts = await Promise.all( | ||||||
|  | 				uids.map(uid => db.sortedSetCount(`events:time:uid:${uid}`, from, to)) | ||||||
|  | 			); | ||||||
|  | 			return counts.reduce((prev, cur) => prev + cur, 0); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		const eids = await Promise.all( | ||||||
|  | 			uids.map( | ||||||
|  | 				uid => db.getSortedSetRevIntersect({ | ||||||
|  | 					sets: [`events:time:uid:${uid}`, `events:time:${filter}`], | ||||||
|  | 					start: 0, | ||||||
|  | 					stop: -1, | ||||||
|  | 					weights: [1, 0], | ||||||
|  | 					withScores: true, | ||||||
|  | 				}) | ||||||
|  | 			) | ||||||
|  | 		); | ||||||
|  |  | ||||||
|  | 		return _.flatten(eids).filter( | ||||||
|  | 			i => (from === '-inf' || i.score >= from) && (to === '+inf' || i.score <= to) | ||||||
|  | 		).length; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return await db.sortedSetCount(`events:time${filter ? `:${filter}` : ''}`, from || '-inf', to); | ||||||
|  | }; | ||||||
|  |  | ||||||
| events.getEventsByEventIds = async (eids) => { | events.getEventsByEventIds = async (eids) => { | ||||||
| 	let eventsData = await db.getObjects(eids.map(eid => `event:${eid}`)); | 	let eventsData = await db.getObjects(eids.map(eid => `event:${eid}`)); | ||||||
| 	eventsData = eventsData.filter(Boolean); | 	eventsData = eventsData.filter(Boolean); | ||||||
| @@ -163,7 +247,11 @@ async function addUserData(eventsData, field, objectName) { | |||||||
| events.deleteEvents = async function (eids) { | events.deleteEvents = async function (eids) { | ||||||
| 	const keys = eids.map(eid => `event:${eid}`); | 	const keys = eids.map(eid => `event:${eid}`); | ||||||
| 	const eventData = await db.getObjectsFields(keys, ['type']); | 	const eventData = await db.getObjectsFields(keys, ['type']); | ||||||
| 	const sets = _.uniq(['events:time'].concat(eventData.map(e => `events:time:${e.type}`))); | 	const sets = _.uniq( | ||||||
|  | 		['events:time'] | ||||||
|  | 			.concat(eventData.map(e => `events:time:${e.type}`)) | ||||||
|  | 			.concat(eventData.map(e => `events:time:uid:${e.uid}`)) | ||||||
|  | 	); | ||||||
| 	await Promise.all([ | 	await Promise.all([ | ||||||
| 		db.deleteAll(keys), | 		db.deleteAll(keys), | ||||||
| 		db.sortedSetRemove(sets, eids), | 		db.sortedSetRemove(sets, eids), | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								src/upgrades/3.8.0/events-uid-filter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/upgrades/3.8.0/events-uid-filter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | /* eslint-disable no-await-in-loop */ | ||||||
|  |  | ||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const db = require('../../database'); | ||||||
|  | const batch = require('../../batch'); | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  | 	name: 'Add user filter to acp events', | ||||||
|  | 	timestamp: Date.UTC(2024, 3, 1), | ||||||
|  | 	method: async function () { | ||||||
|  | 		const { progress } = this; | ||||||
|  |  | ||||||
|  | 		await batch.processSortedSet(`events:time`, async (eids) => { | ||||||
|  | 			const eventData = await db.getObjects(eids.map(eid => `event:${eid}`)); | ||||||
|  | 			const bulkAdd = []; | ||||||
|  | 			eventData.forEach((event) => { | ||||||
|  | 				if (event && event.hasOwnProperty('uid') && event.uid && event.eid) { | ||||||
|  | 					bulkAdd.push( | ||||||
|  | 						[`events:time:uid:${event.uid}`, event.timestamp || Date.now(), event.eid] | ||||||
|  | 					); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 			await db.sortedSetAddBulk(bulkAdd); | ||||||
|  | 			progress.incr(eids.length); | ||||||
|  | 		}, { | ||||||
|  | 			batch: 500, | ||||||
|  | 			progress, | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
| @@ -50,6 +50,14 @@ | |||||||
| 						<label class="form-label" for="end">[[admin/advanced/events:filter-end]]</label> | 						<label class="form-label" for="end">[[admin/advanced/events:filter-end]]</label> | ||||||
| 						<input type="date" id="end" name="end" value="{query.end}" class="form-control" /> | 						<input type="date" id="end" name="end" value="{query.end}" class="form-control" /> | ||||||
| 					</div> | 					</div> | ||||||
|  | 					<div class="mb-3 d-flex flex-column gap-3"> | ||||||
|  | 						<select id="user-group-select" class="form-select"> | ||||||
|  | 							<option value="username" {{{ if (query.username != "") }}}selected{{{ end }}}>[[admin/advanced/events:filter-user]]</option> | ||||||
|  | 							<option value="group" {{{ if (query.group != "") }}}selected{{{ end }}}>[[admin/advanced/events:filter-group]]</option> | ||||||
|  | 						</select> | ||||||
|  | 						<input type="text" id="username" name="username" value="{query.username}" class="form-control {{{ if (query.group != "") }}}hidden{{{ end }}}" placeholder="[[admin/advanced/events:filter-user.placeholder]]"/> | ||||||
|  | 						<input type="text" id="group" name="group" value="{query.group}" class="form-control {{{ if (query.group == "") }}}hidden{{{ end }}} {{{ if (query.username != "") }}}hidden{{{ end }}}" placeholder="[[admin/advanced/events:filter-group.placeholder]]" /> | ||||||
|  | 					</div> | ||||||
| 					<div class="mb-3"> | 					<div class="mb-3"> | ||||||
| 						<label class="form-label" for="perPage">[[admin/advanced/events:filter-per-page]]</label> | 						<label class="form-label" for="perPage">[[admin/advanced/events:filter-per-page]]</label> | ||||||
| 						<input type="text" id="perPage" name="perPage" value="{query.perPage}" class="form-control" /> | 						<input type="text" id="perPage" name="perPage" value="{query.perPage}" class="form-control" /> | ||||||
|   | |||||||
| @@ -687,7 +687,7 @@ describe('socket.io', () => { | |||||||
| 			await socketUser.reset.send({ uid: 0 }, 'regular@test.com'); | 			await socketUser.reset.send({ uid: 0 }, 'regular@test.com'); | ||||||
| 			const [count, eventsData] = await Promise.all([ | 			const [count, eventsData] = await Promise.all([ | ||||||
| 				db.sortedSetCount('reset:issueDate', 0, Date.now()), | 				db.sortedSetCount('reset:issueDate', 0, Date.now()), | ||||||
| 				events.getEvents('', 0, 0), | 				events.getEvents({ filter: '', start: 0, stop: 0 }), | ||||||
| 			]); | 			]); | ||||||
| 			assert.strictEqual(count, 2); | 			assert.strictEqual(count, 2); | ||||||
|  |  | ||||||
| @@ -705,7 +705,7 @@ describe('socket.io', () => { | |||||||
| 			); | 			); | ||||||
| 			const [count, eventsData] = await Promise.all([ | 			const [count, eventsData] = await Promise.all([ | ||||||
| 				db.sortedSetCount('reset:issueDate', 0, Date.now()), | 				db.sortedSetCount('reset:issueDate', 0, Date.now()), | ||||||
| 				events.getEvents('', 0, 0), | 				events.getEvents({ filter: '', start: 0, stop: 0 }), | ||||||
| 			]); | 			]); | ||||||
| 			assert.strictEqual(count, 2); | 			assert.strictEqual(count, 2); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user