mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-26 16:46:12 +01:00 
			
		
		
		
	feat: consolidation of flags to reduce flagspam, #8510
Squashed commit of the following: commitc6d0939620Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 24 13:41:32 2020 -0400 fix: more tests commit32f9af2a87Merge:e509075354eae927d1Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 24 10:53:04 2020 -0400 Merge remote-tracking branch 'origin/master' into singleton-flags commite509075351Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 24 10:52:46 2020 -0400 fix: controllers-admin test commitfd5af99e30Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 17:26:55 2020 -0400 fix(tests): dummy commit to trigger travisCI commitc452a6ffcfAuthor: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 17:05:09 2020 -0400 fix(openapi): openapi spec changes commit8089a74e89Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 15:48:00 2020 -0400 fix: reversing the order of reports for display purposes commita099892b37Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 15:45:44 2020 -0400 refactor: run all flag creation calls in a single batch commitb24999682fAuthor: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 15:08:23 2020 -0400 feat: handling multiple reporters per flag, #8510 commit08c75c0200Author: Julian Lam <julian@nodebb.org> Date: Thu Jul 16 20:53:18 2020 -0400 feat: upgrade script for #8510
This commit is contained in:
		| @@ -1,8 +1,7 @@ | |||||||
| { | { | ||||||
| 	"state": "State", | 	"state": "State", | ||||||
| 	"reporter": "Reporter", | 	"reports": "Reports", | ||||||
| 	"reported-at": "Reported At", | 	"first-reported": "First Reported", | ||||||
| 	"description": "Description", |  | ||||||
| 	"no-flags": "Hooray! No flags found.", | 	"no-flags": "Hooray! No flags found.", | ||||||
| 	"assignee": "Assignee", | 	"assignee": "Assignee", | ||||||
| 	"update": "Update", | 	"update": "Update", | ||||||
|   | |||||||
| @@ -3089,6 +3089,9 @@ paths: | |||||||
|                     type: boolean |                     type: boolean | ||||||
|                   downvoted: |                   downvoted: | ||||||
|                     type: boolean |                     type: boolean | ||||||
|  |                   flagId: | ||||||
|  |                     type: number | ||||||
|  |                     description: The flag identifier, if this particular post has been flagged before | ||||||
|   "/api/topic/tid/{id}": |   "/api/topic/tid/{id}": | ||||||
|     get: |     get: | ||||||
|       tags: |       tags: | ||||||
| @@ -3735,6 +3738,9 @@ paths: | |||||||
|                               type: boolean |                               type: boolean | ||||||
|                             display_post_menu: |                             display_post_menu: | ||||||
|                               type: boolean |                               type: boolean | ||||||
|  |                             flagId: | ||||||
|  |                               type: number | ||||||
|  |                               description: The flag identifier, if this particular post has been flagged before | ||||||
|                       category: |                       category: | ||||||
|                         $ref: components/schemas/CategoryObject.yaml#/CategoryObject |                         $ref: components/schemas/CategoryObject.yaml#/CategoryObject | ||||||
|                       tagWhitelist: |                       tagWhitelist: | ||||||
| @@ -4788,6 +4794,9 @@ paths: | |||||||
|                           properties: |                           properties: | ||||||
|                             state: |                             state: | ||||||
|                               type: string |                               type: string | ||||||
|  |                             heat: | ||||||
|  |                               type: number | ||||||
|  |                               description: The number of reports that make up this flag | ||||||
|                             flagId: |                             flagId: | ||||||
|                               type: number |                               type: number | ||||||
|                             type: |                             type: | ||||||
| @@ -4796,34 +4805,8 @@ paths: | |||||||
|                               oneOf: |                               oneOf: | ||||||
|                                 - type: string |                                 - type: string | ||||||
|                                 - type: number |                                 - type: number | ||||||
|                             description: |  | ||||||
|                               type: string |  | ||||||
|                             uid: |  | ||||||
|                               type: number |  | ||||||
|                               description: A user identifier |  | ||||||
|                             datetime: |                             datetime: | ||||||
|                               type: number |                               type: number | ||||||
|                             reporter: |  | ||||||
|                               type: object |  | ||||||
|                               properties: |  | ||||||
|                                 username: |  | ||||||
|                                   type: string |  | ||||||
|                                   description: A friendly name for a given user account |  | ||||||
|                                 picture: |  | ||||||
|                                   nullable: true |  | ||||||
|                                   type: string |  | ||||||
|                                 icon:bgColor: |  | ||||||
|                                   type: string |  | ||||||
|                                   description: A six-character hexadecimal colour code assigned to the user. This |  | ||||||
|                                     value is used in conjunction with |  | ||||||
|                                     `icon:text` for the user's auto-generated |  | ||||||
|                                     icon |  | ||||||
|                                   example: "#f44336" |  | ||||||
|                                 icon:text: |  | ||||||
|                                   type: string |  | ||||||
|                                   description: A single-letter representation of a username. This is used in the |  | ||||||
|                                     auto-generated icon given to users without |  | ||||||
|                                     an avatar |  | ||||||
|                             labelClass: |                             labelClass: | ||||||
|                               type: string |                               type: string | ||||||
|                             target_readable: |                             target_readable: | ||||||
| @@ -4886,11 +4869,6 @@ paths: | |||||||
|                         type: string |                         type: string | ||||||
|                       targetId: |                       targetId: | ||||||
|                         type: number |                         type: number | ||||||
|                       description: |  | ||||||
|                         type: string |  | ||||||
|                       uid: |  | ||||||
|                         type: number |  | ||||||
|                         description: A user identifier |  | ||||||
|                       datetime: |                       datetime: | ||||||
|                         type: number |                         type: number | ||||||
|                       datetimeISO: |                       datetimeISO: | ||||||
| @@ -5006,34 +4984,45 @@ paths: | |||||||
|                                     `icon:text` for the user's auto-generated |                                     `icon:text` for the user's auto-generated | ||||||
|                                     icon |                                     icon | ||||||
|                                   example: "#f44336" |                                   example: "#f44336" | ||||||
|                       reporter: |                       reports: | ||||||
|                         type: object |                         type: array | ||||||
|                         properties: |                         items: | ||||||
|                           username: |                           type: object | ||||||
|                             type: string |                           properties: | ||||||
|                             description: A friendly name for a given user account |                             value: | ||||||
|                           userslug: |                               type: string | ||||||
|                             type: string |                             timestamp: | ||||||
|                             description: An URL-safe variant of the username (i.e. lower-cased, spaces |                               type: number | ||||||
|                               removed, etc.) |                             timestampISO: | ||||||
|                           picture: |                               type: string | ||||||
|                             nullable: true |                             reporter: | ||||||
|                           reputation: |                               type: object | ||||||
|                             type: number |                               properties: | ||||||
|                           uid: |                                 username: | ||||||
|                             type: number |                                   type: string | ||||||
|                             description: A user identifier |                                   description: A friendly name for a given user account | ||||||
|                           icon:text: |                                 userslug: | ||||||
|                             type: string |                                   type: string | ||||||
|                             description: A single-letter representation of a username. This is used in the |                                   description: An URL-safe variant of the username (i.e. lower-cased, spaces | ||||||
|                               auto-generated icon given to users without an |                                     removed, etc.) | ||||||
|                               avatar |                                 picture: | ||||||
|                           icon:bgColor: |                                   nullable: true | ||||||
|                             type: string |                                 reputation: | ||||||
|                             description: A six-character hexadecimal colour code assigned to the user. This |                                   type: number | ||||||
|                               value is used in conjunction with `icon:text` for |                                 uid: | ||||||
|                               the user's auto-generated icon |                                   type: number | ||||||
|                             example: "#f44336" |                                   description: A user identifier | ||||||
|  |                                 icon:text: | ||||||
|  |                                   type: string | ||||||
|  |                                   description: A single-letter representation of a username. This is used in the | ||||||
|  |                                     auto-generated icon given to users without an | ||||||
|  |                                     avatar | ||||||
|  |                                 icon:bgColor: | ||||||
|  |                                   type: string | ||||||
|  |                                   description: A six-character hexadecimal colour code assigned to the user. This | ||||||
|  |                                     value is used in conjunction with `icon:text` for | ||||||
|  |                                     the user's auto-generated icon | ||||||
|  |                                   example: "#f44336" | ||||||
|                       type_path: |                       type_path: | ||||||
|                         type: string |                         type: string | ||||||
|                       assignees: |                       assignees: | ||||||
|   | |||||||
| @@ -6,6 +6,11 @@ define('forum/flags/list', ['components', 'Chart'], function (components, Chart) | |||||||
| 	Flags.init = function () { | 	Flags.init = function () { | ||||||
| 		Flags.enableFilterForm(); | 		Flags.enableFilterForm(); | ||||||
|  |  | ||||||
|  | 		components.get('flags/list').on('click', '[data-flag-id]', function () { | ||||||
|  | 			var flagId = this.getAttribute('data-flag-id'); | ||||||
|  | 			ajaxify.go('flags/' + flagId); | ||||||
|  | 		}); | ||||||
|  |  | ||||||
| 		var graphWrapper = $('#flags-daily-wrapper'); | 		var graphWrapper = $('#flags-daily-wrapper'); | ||||||
| 		var graphFooter = graphWrapper.siblings('.panel-footer'); | 		var graphFooter = graphWrapper.siblings('.panel-footer'); | ||||||
| 		$('#flags-daily-wrapper').one('shown.bs.collapse', function () { | 		$('#flags-daily-wrapper').one('shown.bs.collapse', function () { | ||||||
|   | |||||||
							
								
								
									
										149
									
								
								src/flags.js
									
									
									
									
									
								
							
							
						
						
									
										149
									
								
								src/flags.js
									
									
									
									
									
								
							| @@ -84,32 +84,28 @@ Flags.init = async function () { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| Flags.get = async function (flagId) { | Flags.get = async function (flagId) { | ||||||
| 	const [base, history, notes] = await Promise.all([ | 	const [base, history, notes, reports] = await Promise.all([ | ||||||
| 		db.getObject('flag:' + flagId), | 		db.getObject('flag:' + flagId), | ||||||
| 		Flags.getHistory(flagId), | 		Flags.getHistory(flagId), | ||||||
| 		Flags.getNotes(flagId), | 		Flags.getNotes(flagId), | ||||||
|  | 		Flags.getReports(flagId), | ||||||
| 	]); | 	]); | ||||||
| 	if (!base) { | 	if (!base) { | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const [userObj, targetObj] = await Promise.all([ |  | ||||||
| 		user.getUserFields(base.uid, ['username', 'userslug', 'picture', 'reputation']), |  | ||||||
| 		Flags.getTarget(base.type, base.targetId, 0), |  | ||||||
| 	]); |  | ||||||
|  |  | ||||||
| 	const flagObj = { | 	const flagObj = { | ||||||
| 		state: 'open', | 		state: 'open', | ||||||
| 		assignee: null, | 		assignee: null, | ||||||
| 		...base, | 		...base, | ||||||
| 		description: validator.escape(base.description), |  | ||||||
| 		datetimeISO: utils.toISOString(base.datetime), | 		datetimeISO: utils.toISOString(base.datetime), | ||||||
| 		target_readable: base.type.charAt(0).toUpperCase() + base.type.slice(1) + ' ' + base.targetId, | 		target_readable: base.type.charAt(0).toUpperCase() + base.type.slice(1) + ' ' + base.targetId, | ||||||
| 		target: targetObj, | 		target: await Flags.getTarget(base.type, base.targetId, 0), | ||||||
| 		history: history, | 		history: history, | ||||||
| 		notes: notes, | 		notes: notes, | ||||||
| 		reporter: userObj, | 		reports: reports, | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const data = await plugins.fireHook('filter:flags.get', { | 	const data = await plugins.fireHook('filter:flags.get', { | ||||||
| 		flag: flagObj, | 		flag: flagObj, | ||||||
| 	}); | 	}); | ||||||
| @@ -160,24 +156,19 @@ Flags.list = async function (filters, uid) { | |||||||
| 	const pageCount = Math.ceil(flagIds.length / flagsPerPage); | 	const pageCount = Math.ceil(flagIds.length / flagsPerPage); | ||||||
| 	flagIds = flagIds.slice((filters.page - 1) * flagsPerPage, filters.page * flagsPerPage); | 	flagIds = flagIds.slice((filters.page - 1) * flagsPerPage, filters.page * flagsPerPage); | ||||||
|  |  | ||||||
| 	const flags = await Promise.all(flagIds.map(async (flagId) => { | 	const reportCounts = await db.sortedSetsCard(flagIds.map(flagId => `flag:${flagId}:reports`)); | ||||||
|  |  | ||||||
|  | 	const flags = await Promise.all(flagIds.map(async (flagId, idx) => { | ||||||
| 		let flagObj = await db.getObject('flag:' + flagId); | 		let flagObj = await db.getObject('flag:' + flagId); | ||||||
| 		const userObj = await user.getUserFields(flagObj.uid, ['username', 'picture']); |  | ||||||
| 		flagObj = { | 		flagObj = { | ||||||
| 			state: 'open', | 			state: 'open', | ||||||
| 			assignee: null, | 			assignee: null, | ||||||
|  | 			heat: reportCounts[idx], | ||||||
| 			...flagObj, | 			...flagObj, | ||||||
| 			reporter: { |  | ||||||
| 				username: userObj.username, |  | ||||||
| 				picture: userObj.picture, |  | ||||||
| 				'icon:bgColor': userObj['icon:bgColor'], |  | ||||||
| 				'icon:text': userObj['icon:text'], |  | ||||||
| 			}, |  | ||||||
| 		}; | 		}; | ||||||
| 		flagObj.labelClass = Flags._constants.state_class[flagObj.state]; | 		flagObj.labelClass = Flags._constants.state_class[flagObj.state]; | ||||||
|  |  | ||||||
| 		return Object.assign(flagObj, { | 		return Object.assign(flagObj, { | ||||||
| 			description: validator.escape(String(flagObj.description)), |  | ||||||
| 			target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId, | 			target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId, | ||||||
| 			datetimeISO: utils.toISOString(flagObj.datetime), | 			datetimeISO: utils.toISOString(flagObj.datetime), | ||||||
| 		}); | 		}); | ||||||
| @@ -242,6 +233,24 @@ Flags.getNote = async function (flagId, datetime) { | |||||||
| 	return notes[0]; | 	return notes[0]; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | Flags.getFlagIdByTarget = async function (type, id) { | ||||||
|  | 	let method; | ||||||
|  | 	switch (type) { | ||||||
|  | 		case 'post': | ||||||
|  | 			method = posts.getPostField; | ||||||
|  | 			break; | ||||||
|  |  | ||||||
|  | 		case 'user': | ||||||
|  | 			method = user.getUserField; | ||||||
|  | 			break; | ||||||
|  |  | ||||||
|  | 		default: | ||||||
|  | 			throw new Error('[[error:invalid-data]]'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return await method(id, 'flagId'); | ||||||
|  | }; | ||||||
|  |  | ||||||
| async function modifyNotes(notes) { | async function modifyNotes(notes) { | ||||||
| 	const uids = []; | 	const uids = []; | ||||||
| 	notes = notes.map(function (note) { | 	notes = notes.map(function (note) { | ||||||
| @@ -277,11 +286,13 @@ Flags.create = async function (type, id, uid, reason, timestamp) { | |||||||
| 		timestamp = Date.now(); | 		timestamp = Date.now(); | ||||||
| 		doHistoryAppend = true; | 		doHistoryAppend = true; | ||||||
| 	} | 	} | ||||||
| 	const [flagExists, targetExists,, targetUid, targetCid] = await Promise.all([ | 	const [flagExists, targetExists,, targetFlagged, targetUid, targetCid] = await Promise.all([ | ||||||
| 		// Sanity checks | 		// Sanity checks | ||||||
| 		Flags.exists(type, id, uid), | 		Flags.exists(type, id, uid), | ||||||
| 		Flags.targetExists(type, id), | 		Flags.targetExists(type, id), | ||||||
| 		Flags.canFlag(type, id, uid), | 		Flags.canFlag(type, id, uid), | ||||||
|  | 		Flags.targetFlagged(type, id), | ||||||
|  |  | ||||||
| 		// Extra data for zset insertion | 		// Extra data for zset insertion | ||||||
| 		Flags.getTargetUid(type, id), | 		Flags.getTargetUid(type, id), | ||||||
| 		Flags.getTargetCid(type, id), | 		Flags.getTargetCid(type, id), | ||||||
| @@ -292,45 +303,91 @@ Flags.create = async function (type, id, uid, reason, timestamp) { | |||||||
| 		throw new Error('[[error:invalid-data]]'); | 		throw new Error('[[error:invalid-data]]'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const flagId = await db.incrObjectField('global', 'nextFlagId'); | 	// If the flag already exists, just add the report | ||||||
|  | 	if (targetFlagged) { | ||||||
|  | 		const flagId = await Flags.getFlagIdByTarget(type, id); | ||||||
|  | 		await Promise.all([ | ||||||
|  | 			Flags.addReport(flagId, uid, reason, timestamp), | ||||||
|  | 			Flags.update(flagId, uid, { state: 'open' }), | ||||||
|  | 		]); | ||||||
|  |  | ||||||
| 	await db.setObject('flag:' + flagId, { | 		return await Flags.get(flagId); | ||||||
| 		flagId: flagId, | 	} | ||||||
| 		type: type, |  | ||||||
| 		targetId: id, | 	const flagId = await db.incrObjectField('global', 'nextFlagId'); | ||||||
| 		description: reason, | 	const batched = []; | ||||||
| 		uid: uid, |  | ||||||
| 		datetime: timestamp, | 	batched.push( | ||||||
| 	}); | 		db.setObject.bind(db, 'flag:' + flagId, { | ||||||
| 	await db.sortedSetAdd('flags:datetime', timestamp, flagId); // by time, the default | 			flagId: flagId, | ||||||
| 	await db.sortedSetAdd('flags:byReporter:' + uid, timestamp, flagId); // by reporter | 			type: type, | ||||||
| 	await db.sortedSetAdd('flags:byType:' + type, timestamp, flagId);	// by flag type | 			targetId: id, | ||||||
| 	await db.sortedSetAdd('flags:hash', flagId, [type, id, uid].join(':')); // save zset for duplicate checking | 			datetime: timestamp, | ||||||
| 	await db.sortedSetIncrBy('flags:byTarget', 1, [type, id].join(':'));	// by flag target (score is count) | 		}), | ||||||
| 	await analytics.increment('flags'); // some fancy analytics | 		Flags.addReport.bind(Flags, flagId, uid, reason, timestamp), | ||||||
|  | 		db.sortedSetAdd.bind(db, 'flags:datetime', timestamp, flagId), // by time, the default | ||||||
|  | 		db.sortedSetAdd.bind(db, 'flags:byType:' + type, timestamp, flagId),	// by flag type | ||||||
|  | 		db.sortedSetAdd.bind(db, 'flags:hash', flagId, [type, id, uid].join(':')), // save zset for duplicate checking | ||||||
|  | 		db.sortedSetIncrBy.bind(db, 'flags:byTarget', 1, [type, id].join(':')),	// by flag target (score is count) | ||||||
|  | 		analytics.increment.bind(analytics, 'flags') // some fancy analytics | ||||||
|  | 	); | ||||||
|  |  | ||||||
| 	if (targetUid) { | 	if (targetUid) { | ||||||
| 		await db.sortedSetAdd('flags:byTargetUid:' + targetUid, timestamp, flagId); // by target uid | 		batched.push(db.sortedSetAdd.bind(db, 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (targetCid) { | 	if (targetCid) { | ||||||
| 		await db.sortedSetAdd('flags:byCid:' + targetCid, timestamp, flagId); // by target cid | 		batched.push(db.sortedSetAdd.bind(db, 'flags:byCid:' + targetCid, timestamp, flagId)); // by target cid | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (type === 'post') { | 	if (type === 'post') { | ||||||
| 		await db.sortedSetAdd('flags:byPid:' + id, timestamp, flagId);	// by target pid | 		batched.push( | ||||||
|  | 			db.sortedSetAdd.bind(db, 'flags:byPid:' + id, timestamp, flagId),	// by target pid | ||||||
|  | 			posts.setPostField.bind(posts, id, 'flagId', flagId) | ||||||
|  | 		); | ||||||
|  |  | ||||||
| 		if (targetUid) { | 		if (targetUid) { | ||||||
| 			await user.incrementUserFlagsBy(targetUid, 1); | 			batched.push(user.incrementUserFlagsBy.bind(user, targetUid, 1)); | ||||||
| 		} | 		} | ||||||
|  | 	} else if (type === 'user') { | ||||||
|  | 		batched.push(user.setUserField.bind(user, id, 'flagId', flagId)); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Run all the database calls in one single batched call... | ||||||
|  | 	await Promise.all(batched.map(async method => await method())); | ||||||
|  |  | ||||||
| 	if (doHistoryAppend) { | 	if (doHistoryAppend) { | ||||||
| 		await Flags.update(flagId, uid, { state: 'open' }); | 		Flags.update(flagId, uid, { state: 'open' }); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return await Flags.get(flagId); | 	return await Flags.get(flagId); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | Flags.getReports = async function (flagId) { | ||||||
|  | 	const [reports, reporterUids] = await Promise.all([ | ||||||
|  | 		db.getSortedSetRevRangeWithScores(`flag:${flagId}:reports`, 0, -1), | ||||||
|  | 		db.getSortedSetRevRange(`flag:${flagId}:reporters`, 0, -1), | ||||||
|  | 	]); | ||||||
|  |  | ||||||
|  | 	await Promise.all(reports.map(async (report, idx) => { | ||||||
|  | 		report.timestamp = report.score; | ||||||
|  | 		report.timestampISO = new Date(report.score).toISOString(); | ||||||
|  | 		delete report.score; | ||||||
|  | 		report.reporter = await user.getUserFields(reporterUids[idx], ['username', 'userslug', 'picture', 'reputation']); | ||||||
|  | 	})); | ||||||
|  |  | ||||||
|  | 	return reports; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | Flags.addReport = async function (flagId, uid, reason, timestamp) { | ||||||
|  | 	// adds to reporters/report zsets | ||||||
|  | 	await Promise.all([ | ||||||
|  | 		db.sortedSetAdd(`flags:byReporter:${uid}`, timestamp, flagId), | ||||||
|  | 		db.sortedSetAdd(`flag:${flagId}:reports`, timestamp, reason), | ||||||
|  | 		db.sortedSetAdd(`flag:${flagId}:reporters`, timestamp, uid), | ||||||
|  | 	]); | ||||||
|  | }; | ||||||
|  |  | ||||||
| Flags.exists = async function (type, id, uid) { | Flags.exists = async function (type, id, uid) { | ||||||
| 	return await db.isSortedSetMember('flags:hash', [type, id, uid].join(':')); | 	return await db.isSortedSetMember('flags:hash', [type, id, uid].join(':')); | ||||||
| }; | }; | ||||||
| @@ -386,6 +443,10 @@ Flags.targetExists = async function (type, id) { | |||||||
| 	throw new Error('[[error:invalid-data]]'); | 	throw new Error('[[error:invalid-data]]'); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | Flags.targetFlagged = async function (type, id) { | ||||||
|  | 	return await db.sortedSetScore('flags:byTarget', [type, id].join(':')) >= 1; | ||||||
|  | }; | ||||||
|  |  | ||||||
| Flags.getTargetUid = async function (type, id) { | Flags.getTargetUid = async function (type, id) { | ||||||
| 	if (type === 'post') { | 	if (type === 'post') { | ||||||
| 		return await posts.getPostField(id, 'uid'); | 		return await posts.getPostField(id, 'uid'); | ||||||
| @@ -443,7 +504,7 @@ Flags.update = async function (flagId, uid, changeset) { | |||||||
| 					tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId)); | 					tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId)); | ||||||
| 					tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId)); | 					tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId)); | ||||||
| 					if (changeset[prop] === 'resolved' || changeset[prop] === 'rejected') { | 					if (changeset[prop] === 'resolved' || changeset[prop] === 'rejected') { | ||||||
| 						tasks.push(notifications.rescind('flag:' + current.type + ':' + current.targetId + ':uid:' + current.uid)); | 						tasks.push(notifications.rescind('flag:' + current.type + ':' + current.targetId)); | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} else if (prop === 'assignee') { | 			} else if (prop === 'assignee') { | ||||||
| @@ -545,11 +606,11 @@ Flags.notify = async function (flagObj, uid) { | |||||||
|  |  | ||||||
| 		notifObj = await notifications.create({ | 		notifObj = await notifications.create({ | ||||||
| 			type: 'new-post-flag', | 			type: 'new-post-flag', | ||||||
| 			bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reporter.username + ', ' + titleEscaped + ']]', | 			bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reports[flagObj.reports.length - 1].reporter.username + ', ' + titleEscaped + ']]', | ||||||
| 			bodyLong: flagObj.description, | 			bodyLong: flagObj.description, | ||||||
| 			pid: flagObj.targetId, | 			pid: flagObj.targetId, | ||||||
| 			path: '/flags/' + flagObj.flagId, | 			path: '/flags/' + flagObj.flagId, | ||||||
| 			nid: 'flag:post:' + flagObj.targetId + ':uid:' + uid, | 			nid: 'flag:post:' + flagObj.targetId, | ||||||
| 			from: uid, | 			from: uid, | ||||||
| 			mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId, | 			mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId, | ||||||
| 			topicTitle: title, | 			topicTitle: title, | ||||||
| @@ -558,10 +619,10 @@ Flags.notify = async function (flagObj, uid) { | |||||||
| 	} else if (flagObj.type === 'user') { | 	} else if (flagObj.type === 'user') { | ||||||
| 		notifObj = await notifications.create({ | 		notifObj = await notifications.create({ | ||||||
| 			type: 'new-user-flag', | 			type: 'new-user-flag', | ||||||
| 			bodyShort: '[[notifications:user_flagged_user, ' + flagObj.reporter.username + ', ' + flagObj.target.username + ']]', | 			bodyShort: '[[notifications:user_flagged_user, ' + flagObj.reports[flagObj.reports.length - 1].reporter.username + ', ' + flagObj.target.username + ']]', | ||||||
| 			bodyLong: flagObj.description, | 			bodyLong: flagObj.description, | ||||||
| 			path: '/flags/' + flagObj.flagId, | 			path: '/flags/' + flagObj.flagId, | ||||||
| 			nid: 'flag:user:' + flagObj.targetId + ':uid:' + uid, | 			nid: 'flag:user:' + flagObj.targetId, | ||||||
| 			from: uid, | 			from: uid, | ||||||
| 			mergeId: 'notifications:user_flagged_user|' + flagObj.targetId, | 			mergeId: 'notifications:user_flagged_user|' + flagObj.targetId, | ||||||
| 		}); | 		}); | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								src/upgrades/1.14.3/consolidate_flags.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/upgrades/1.14.3/consolidate_flags.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | const db = require('../../database'); | ||||||
|  | const batch = require('../../batch'); | ||||||
|  | const posts = require('../../posts'); | ||||||
|  | const user = require('../../user'); | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  | 	name: 'Consolidate multiple flags reports, going forward', | ||||||
|  | 	timestamp: Date.UTC(2020, 6, 16), | ||||||
|  | 	method: async function () { | ||||||
|  | 		const progress = this.progress; | ||||||
|  |  | ||||||
|  | 		let flags = await db.getSortedSetRange('flags:datetime', 0, -1); | ||||||
|  | 		flags = flags.map(flagId => `flag:${flagId}`); | ||||||
|  | 		flags = await db.getObjectsFields(flags, ['flagId', 'type', 'targetId', 'uid', 'description', 'datetime']); | ||||||
|  | 		progress.total = flags.length; | ||||||
|  |  | ||||||
|  | 		await batch.processArray(flags, async function (subset) { | ||||||
|  | 			progress.incr(subset.length); | ||||||
|  |  | ||||||
|  | 			await Promise.all(subset.map(async (flagObj) => { | ||||||
|  | 				const methods = []; | ||||||
|  | 				switch (flagObj.type) { | ||||||
|  | 					case 'post': | ||||||
|  | 						methods.push(posts.setPostField.bind(posts, flagObj.targetId, 'flagId', flagObj.flagId)); | ||||||
|  | 						break; | ||||||
|  |  | ||||||
|  | 					case 'user': | ||||||
|  | 						methods.push(user.setUserField.bind(user, flagObj.targetId, 'flagId', flagObj.flagId)); | ||||||
|  | 						break; | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				methods.push( | ||||||
|  | 					db.sortedSetAdd.bind(db, `flag:${flagObj.flagId}:reports`, flagObj.datetime, flagObj.description), | ||||||
|  | 					db.sortedSetAdd.bind(db, `flag:${flagObj.flagId}:reporters`, flagObj.datetime, flagObj.uid) | ||||||
|  | 				); | ||||||
|  |  | ||||||
|  | 				await Promise.all(methods.map(async method => method())); | ||||||
|  | 			})); | ||||||
|  | 		}, { | ||||||
|  | 			progress: progress, | ||||||
|  | 			batch: 500, | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
| @@ -712,7 +712,9 @@ describe('Admin Controllers', function () { | |||||||
| 				request(nconf.get('url') + '/api/flags/' + flagId, { jar: moderatorJar, json: true }, function (err, res, body) { | 				request(nconf.get('url') + '/api/flags/' + flagId, { jar: moderatorJar, json: true }, function (err, res, body) { | ||||||
| 					assert.ifError(err); | 					assert.ifError(err); | ||||||
| 					assert(body); | 					assert(body); | ||||||
| 					assert.equal(body.reporter.username, 'regular'); | 					assert(body.reports); | ||||||
|  | 					assert(Array.isArray(body.reports)); | ||||||
|  | 					assert.equal(body.reports[0].reporter.username, 'regular'); | ||||||
| 					done(); | 					done(); | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
|   | |||||||
| @@ -48,10 +48,10 @@ describe('Flags', function () { | |||||||
| 				assert.ifError(err); | 				assert.ifError(err); | ||||||
| 				var compare = { | 				var compare = { | ||||||
| 					flagId: 1, | 					flagId: 1, | ||||||
| 					uid: 1, |  | ||||||
| 					targetId: 1, | 					targetId: 1, | ||||||
| 					type: 'post', | 					type: 'post', | ||||||
| 					description: 'Test flag', | 					state: 'open', | ||||||
|  | 					target_readable: 'Post 1', | ||||||
| 				}; | 				}; | ||||||
| 				assert(flagData); | 				assert(flagData); | ||||||
| 				for (var key in compare) { | 				for (var key in compare) { | ||||||
| @@ -124,11 +124,10 @@ describe('Flags', function () { | |||||||
| 				assert.ifError(err); | 				assert.ifError(err); | ||||||
| 				var compare = { | 				var compare = { | ||||||
| 					flagId: 1, | 					flagId: 1, | ||||||
| 					uid: 1, |  | ||||||
| 					targetId: 1, | 					targetId: 1, | ||||||
| 					type: 'post', | 					type: 'post', | ||||||
| 					description: 'Test flag', |  | ||||||
| 					state: 'open', | 					state: 'open', | ||||||
|  | 					target_readable: 'Post 1', | ||||||
| 				}; | 				}; | ||||||
| 				assert(flagData); | 				assert(flagData); | ||||||
| 				for (var key in compare) { | 				for (var key in compare) { | ||||||
| @@ -378,14 +377,14 @@ describe('Flags', function () { | |||||||
| 			await sleep(2000); | 			await sleep(2000); | ||||||
|  |  | ||||||
| 			let userNotifs = await User.notifications.getAll(adminUid); | 			let userNotifs = await User.notifications.getAll(adminUid); | ||||||
| 			assert(userNotifs.includes('flag:post:' + result.postData.pid + ':uid:' + uid1)); | 			assert(userNotifs.includes('flag:post:' + result.postData.pid)); | ||||||
|  |  | ||||||
| 			await Flags.update(flagId, adminUid, { | 			await Flags.update(flagId, adminUid, { | ||||||
| 				state: 'resolved', | 				state: 'resolved', | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
| 			userNotifs = await User.notifications.getAll(adminUid); | 			userNotifs = await User.notifications.getAll(adminUid); | ||||||
| 			assert(!userNotifs.includes('flag:post:' + result.postData.pid + ':uid:' + uid1)); | 			assert(!userNotifs.includes('flag:post:' + result.postData.pid)); | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user