mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-30 18:46:01 +01:00 
			
		
		
		
	feat: closes #12453, filter events by user/group
This commit is contained in:
		| @@ -9,5 +9,9 @@ | ||||
| 	"filter-type": "Event Type", | ||||
| 	"filter-start": "Start 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" | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| 'use strict'; | ||||
|  | ||||
|  | ||||
| define('admin/advanced/events', ['bootbox', 'alerts'], function (bootbox, alerts) { | ||||
| define('admin/advanced/events', ['bootbox', 'alerts', 'autocomplete'], function (bootbox, alerts, autocomplete) { | ||||
| 	const Events = {}; | ||||
|  | ||||
| 	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); | ||||
| 	}; | ||||
|  | ||||
|   | ||||
| @@ -130,7 +130,11 @@ async function listPlugins() { | ||||
|  | ||||
| async function listEvents(count = 10) { | ||||
| 	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...`)); | ||||
| 	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})`); | ||||
|   | ||||
| @@ -3,6 +3,8 @@ | ||||
| const db = require('../../database'); | ||||
| const events = require('../../events'); | ||||
| const pagination = require('../../pagination'); | ||||
| const user = require('../../user'); | ||||
| const groups = require('../../groups'); | ||||
|  | ||||
| const eventsController = module.exports; | ||||
|  | ||||
| @@ -11,18 +13,35 @@ eventsController.get = async function (req, res) { | ||||
| 	const itemsPerPage = parseInt(req.query.perPage, 10) || 20; | ||||
| 	const start = (page - 1) * itemsPerPage; | ||||
| 	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 | ||||
| 	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(); | ||||
| 	from = from && from.setHours(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) | ||||
| 	from = from && from.setUTCHours(0, 0, 0, 0); // 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 [eventCount, eventData, counts] = await Promise.all([ | ||||
| 		db.sortedSetCount(`events:time${currentFilter ? `:${currentFilter}` : ''}`, from || '-inf', to), | ||||
| 		events.getEvents(currentFilter, start, stop, from || '-inf', to), | ||||
| 		events.getEventCount({ | ||||
| 			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}` : ''}`)), | ||||
| 	]); | ||||
|  | ||||
|   | ||||
							
								
								
									
										114
									
								
								src/events.js
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								src/events.js
									
									
									
									
									
								
							| @@ -87,30 +87,114 @@ events.log = async function (data) { | ||||
| 	const eid = await db.incrObjectField('global', 'nextEid'); | ||||
| 	data.timestamp = Date.now(); | ||||
| 	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([ | ||||
| 		db.sortedSetsAdd([ | ||||
| 			'events:time', | ||||
| 			`events:time:${data.type}`, | ||||
| 		], data.timestamp, eid), | ||||
| 		db.sortedSetsAdd(setKeys, data.timestamp, eid), | ||||
| 		db.setObject(`event:${eid}`, data), | ||||
| 	]); | ||||
| 	plugins.hooks.fire('action:events.log', { data: data }); | ||||
| }; | ||||
|  | ||||
| events.getEvents = async function (filter, start, stop, from, to) { | ||||
| 	// from/to optional | ||||
| 	if (from === undefined) { | ||||
| 		from = '-inf'; | ||||
| // filter, start, stop, from(optional), to(optional), uids(optional) | ||||
| events.getEvents = async function (options) { | ||||
| 	// backwards compatibility | ||||
| 	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) { | ||||
| 		to = '+inf'; | ||||
| 	// from/to optional | ||||
| 	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); | ||||
| }; | ||||
|  | ||||
| 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) => { | ||||
| 	let eventsData = await db.getObjects(eids.map(eid => `event:${eid}`)); | ||||
| 	eventsData = eventsData.filter(Boolean); | ||||
| @@ -163,7 +247,11 @@ async function addUserData(eventsData, field, objectName) { | ||||
| events.deleteEvents = async function (eids) { | ||||
| 	const keys = eids.map(eid => `event:${eid}`); | ||||
| 	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([ | ||||
| 		db.deleteAll(keys), | ||||
| 		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> | ||||
| 						<input type="date" id="end" name="end" value="{query.end}" class="form-control" /> | ||||
| 					</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"> | ||||
| 						<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" /> | ||||
|   | ||||
| @@ -687,7 +687,7 @@ describe('socket.io', () => { | ||||
| 			await socketUser.reset.send({ uid: 0 }, 'regular@test.com'); | ||||
| 			const [count, eventsData] = await Promise.all([ | ||||
| 				db.sortedSetCount('reset:issueDate', 0, Date.now()), | ||||
| 				events.getEvents('', 0, 0), | ||||
| 				events.getEvents({ filter: '', start: 0, stop: 0 }), | ||||
| 			]); | ||||
| 			assert.strictEqual(count, 2); | ||||
|  | ||||
| @@ -705,7 +705,7 @@ describe('socket.io', () => { | ||||
| 			); | ||||
| 			const [count, eventsData] = await Promise.all([ | ||||
| 				db.sortedSetCount('reset:issueDate', 0, Date.now()), | ||||
| 				events.getEvents('', 0, 0), | ||||
| 				events.getEvents({ filter: '', start: 0, stop: 0 }), | ||||
| 			]); | ||||
| 			assert.strictEqual(count, 2); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user