code:
'use strict';

module.exports = {
	'0.0.1': require(__dirname+'/migration-0.0.1.js'), //add bypasses to database
	'0.0.2': require(__dirname+'/migration-0.0.2.js'), //rename ip field in posts
	'0.0.3': require(__dirname+'/migration-0.0.3.js'), //move files from /img to /file/
	'0.0.4': require(__dirname+'/migration-0.0.4.js'), //rename some fields for board lock mode and unlisting
	'0.0.5': require(__dirname+'/migration-0.0.5.js'), //add bumplimit to board settings
	'0.0.6': require(__dirname+'/migration-0.0.6.js'), //add blocked countries to board settings
	'0.0.7': require(__dirname+'/migration-0.0.7.js'), //sage only email without force anon for some reason
	'0.0.8': require(__dirname+'/migration-0.0.8.js'), //option to auto reset triggers after hour is over
	'0.0.9': require(__dirname+'/migration-0.0.9.js'), //ip changes
	'0.0.10': require(__dirname+'/migration-0.0.10.js'), //add links to modlog for new logs
	'0.0.11': require(__dirname+'/migration-0.0.11.js'), //rename captcha "text" field to "answer" since we support multiple captcha types now
	'0.0.12': require(__dirname+'/migration-0.0.12.js'), //yotsuba b -> yotsuba-b
	'0.0.13': require(__dirname+'/migration-0.0.13.js'), //add r9k mode (files)
	'0.0.14': require(__dirname+'/migration-0.0.14.js'), //add option for disable .onion file posts to board settings
	'0.0.15': require(__dirname+'/migration-0.0.15.js'), //messages r9k option
	'0.0.16': require(__dirname+'/migration-0.0.16.js'), //separate tph/pph triggers
}


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Creating bypass collection');
	await db.createCollection('bypass');
	console.log('Creating bypass collection index');
	await db.collection('bypass').createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 });
};

repo_name: dolphin/jschan

license: mit


code:
'use strict';

const hashIp = require(__dirname+'/../helpers/haship.js');

module.exports = async(db, redis) => {
	console.log('update moglog postids to postlinks');
	await db.collection('modlog').updateMany({},
		[{
			$addFields: {
				postLinks: [
					{
						$arrayToObject: {
							$map: {
								input: "$postIds",
								as: 'postId',
								in: {
									k: 'postId',
									v: '$$postId'
								}
							}
						}
					}
				]
			}
		}
	]);
	await db.collection('modlog').updateMany({}, {
		'$unset': {
			'postIds': ''
		},
		'$set': {
			'showLinks': false
		}
	});
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Expiring existing captchas, so new ones get new answer format');
	await db.collection('captcha').deleteMany({});
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Renamed "yotsuba b" to "yotsuba-b" in any board settings');
	await db.collection('boards').updateMany({
		'settings.theme': 'yotsuba b',
	}, {
		'$set': {
			'settings.theme': 'yotsuba-b',
		}
	});
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Adding file r9k setting to boards db');
	await db.collection('boards').updateMany({}, {
		'$set': {
			'settings.fileR9KMode': 0,
		}
	});
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Adding disableOnionFilePosting setting');
	await db.collection('boards').updateMany({}, {
		'$set': {
			'settings.disableOnionFilePosting': false,
		}
	});
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Adding message r9k option to boards db');
	await db.collection('boards').updateMany({}, {
		'$set': {
			'settings.messageR9KMode': 0,
		}
	});
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Allow tph/pph separate triggers and resets');
	await db.collection('boards').updateMany({}, {
		'$rename': {
			'settings.triggerAction' : 'settings.pphTriggerAction',
		}
	});
	await db.collection('boards').updateMany({}, {
		'$unset': {
			'settings.resetTrigger' : '',
			'preTriggerMode': '',
		}
	});
	await db.collection('boards').updateMany({}, {
		'$set': {
			'settings.tphTriggerAction' : 0,
			'settings.captchaReset' : 0,
			'settings.lockReset' : 0,
		}
	});
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Renaming IP fields on posts');
	await db.collection('posts').updateMany({}, {
		'$rename': {
			'ip.hash': 'ip.single'
		}
	});
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Moving user uploads from /img/ to /file/');
	require('fs').renameSync(__dirname+'/../static/img/', __dirname+'/../static/file/')
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Renaming some settings fields on boards');
	await db.collection('boards').updateMany({}, {
		'$rename': {
			'settings.locked': 'settings.lockMode',
			'settings.unlisted': 'settings.unlistedLocal',
			'settings.webring': 'settings.unlistedWebring'
		}
	});
	console.log('upadting renamed fields to proper values')
	await db.collection('boards').updateMany({
		'settings.lockMode': true,
	}, {
		'$set': {
			'settings.lockMode': 2,
		}
	});
	await db.collection('boards').updateMany({
		'settings.lockMode': false,
	}, {
		'$set': {
			'settings.lockMode': 0,
		}
	});
	await db.collection('boards').updateMany({
		'settings.triggerAction': 3,
	}, {
		'$set': {
			'settings.triggerAction': 4,
		}
	});
	console.log('clearing boards cache');
	await redis.deletePattern('board:*')
};


code:
'use strict';

const { globalLimits } = require(__dirname+'/../configs/main.js');

module.exports = async(db, redis) => {
	console.log('Adding bumplimit field to boards on posts');
	await db.collection('boards').updateMany({}, {
		'$set': {
			'settings.bumpLimit': globalLimits.bumpLimit.max,
		}
	});
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
	console.log('Cleared globalsettings cache');
	await redis.deletePattern('globalsettings');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('Adding blockedCountries field to boards');
	await db.collection('boards').updateMany({}, {
		'$set': {
			'settings.blockedCountries': [],
		}
	});
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('add sageOnlyEmail option to boards');
	await db.collection('boards').updateMany({}, {
		'$set': {
			'settings.sageOnlyEmail': false,
		}
	});
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

module.exports = async(db, redis) => {
	console.log('add resetTrigger option to boards');
	await db.collection('boards').updateMany({}, {
		'$set': {
			'settings.resetTrigger': false,
		}
	});
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

const hashIp = require(__dirname+'/../helpers/haship.js');

module.exports = async(db, redis) => {
	console.log('change bans index');
	await db.collection('bans').dropIndex("ip_1_board_1");
	await db.collection('bans').createIndex({ 'ip.single': 1 , 'board': 1 });
	console.log('adjusting ip on posts and clearing reports');
	const promises = []
	await db.collection('posts').find().forEach(doc => {
		promises.push(db.collection('posts').updateOne({
			'_id':doc._id
		}, {
			'$set':{
				'ip.raw': doc.ip.single,
				'ip.single': hashIp(doc.ip.single),
				'ip.qrange': hashIp(doc.ip.qrange),
				'ip.hrange': hashIp(doc.ip.hrange),
				'reports': [], //easier than fixing reports
				'globalreports': [], //easier than fixing reports
			}
		}))
	});
	console.log('adjusting ip in modlogs')
	await db.collection('modlog').find().forEach(doc => {
		promises.push(db.collection('modlog').updateOne({
			'_id':doc._id
		}, {
			'$set':{
				'ip': {
					'raw': doc.ip,
					'single': hashIp(doc.ip)
				}
			}
		}))
	});
	console.log('adjust ip in bans, set null type and remove saved posts')
	await db.collection('bans').find().forEach(doc => {
		promises.push(db.collection('bans').updateOne({
			'_id':doc._id
		}, {
			'$set':{
				'ip': {
					'raw': doc.ip,
					'single': hashIp(doc.ip)
				},
				'type': null,
				'posts': null //easier than fixing all saved posts
			}
		}))
	});
	await Promise.all(promises);
	console.log('Cleared boards cache');
	await redis.deletePattern('board:*');
};


code:
'use strict';

const { Posts, Boards, Modlogs } = require(__dirname+'/../../db/')
	, Mongo = require(__dirname+'/../../db/db.js')
	, banPoster = require(__dirname+'/banposter.js')
	, deletePosts = require(__dirname+'/deletepost.js')
	, spoilerPosts = require(__dirname+'/spoilerpost.js')
	, stickyPosts = require(__dirname+'/stickyposts.js')
	, bumplockPosts = require(__dirname+'/bumplockposts.js')
	, lockPosts = require(__dirname+'/lockposts.js')
	, cyclePosts = require(__dirname+'/cycleposts.js')
	, deletePostsFiles = require(__dirname+'/deletepostsfiles.js')
	, reportPosts = require(__dirname+'/reportpost.js')
	, dismissReports = require(__dirname+'/dismissreport.js')
	, movePosts = require(__dirname+'/moveposts.js')
	, { remove } = require('fs-extra')
	, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
	, getAffectedBoards = require(__dirname+'/../../helpers/affectedboards.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, buildQueue = require(__dirname+'/../../queue.js')
	, { postPasswordSecret } = require(__dirname+'/../../configs/main.js')
	, threadRegex = /\/[a-z0-9]+\/(?:manage\/)?thread\/(\d+)\.html/i
	, { createHash, timingSafeEqual } = require('crypto');

module.exports = async (req, res, next) => {

	let redirect = req.headers.referer;
	if (!redirect) {
		if (!req.params.board) {
			redirect = '/globalmanage/recent.html';
		} else {
			redirect = `/${req.params.board}/${req.path.endsWith('modactions') ? 'manage/reports' : 'index'}.html`;
		}
	}

	//if user isnt staff, and they put an action that requires password, e.g. delete/spoiler, then filter posts to only matching password
	if (res.locals.permLevel >= 4 && res.locals.actions.numPasswords > 0) {
		let passwordPosts = [];
		if (req.body.postpassword && req.body.postpassword.length > 0) {
			//hash their input and make it a buffer
			const inputPasswordHash = createHash('sha256').update(postPasswordSecret + req.body.postpassword).digest('base64');
			const inputPasswordBuffer = Buffer.from(inputPasswordHash);
			passwordPosts = res.locals.posts.filter(post => {
				if (post.password != null) { //null password doesnt matter for timing attack, it cant be deleted by non-staff
					const postBuffer = Buffer.from(post.password);
					//returns true and passes filter if passwod matched. constant time compare
					return timingSafeEqual(inputPasswordBuffer, postBuffer);
				}
			});
		}
		//no posts matched password, reject
		if (passwordPosts.length === 0) {
			return dynamicResponse(req, res, 403, 'message', {
				'title': 'Forbidden',
				'error': 'Password did not match any selected posts',
				redirect,
			});
		}
		res.locals.posts = passwordPosts
	}

	//affected boards, list and page numbers
	const deleting = req.body.delete || req.body.delete_ip_board || req.body.delete_ip_global || req.body.delete_ip_thread;
	let { boardThreadMap, beforePages, threadBoards } = await getAffectedBoards(res.locals.posts, deleting);

	if (deleting
		&& req.params.board
		&& req.headers.referer
		&& boardThreadMap[req.params.board]) {
		const threadRefMatch = req.headers.referer.match(threadRegex);
		if (threadRefMatch && boardThreadMap[req.params.board].directThreads.has(+threadRefMatch[1])) {
			redirect = `/${req.params.board}/${req.path.endsWith('modactions') ? 'manage/' : ''}index.html`;
		}
	}

	const messages = [];
	const modlogActions = []
	const combinedQuery = {};
	let aggregateNeeded = false;
	// if getting global banned, board ban doesnt matter
	if (req.body.ban || req.body.global_ban || req.body.report_ban || req.body.global_report_ban) {
		const { message, action, query } = await banPoster(req, res, next);
		if (req.body.ban) {
			modlogActions.push('Ban');
		} else if (req.body.global_ban) {
			modlogActions.push('Global Ban');
		}
		if (req.body.report_ban) {
			modlogActions.push('Ban reporter');
		} else if (req.body.global_report_ban) {
			modlogActions.push('Global ban reporter');
		}
		if (action) {
			combinedQuery[action] = { ...combinedQuery[action], ...query}
		}
		messages.push(message);
	}
	if (deleting) {
		const postsBefore = res.locals.posts.length;
		if (req.body.delete_ip_board || req.body.delete_ip_global || req.body.delete_ip_thread) {
			const deletePostIps = res.locals.posts.map(x => x.ip.single);
			const deletePostMongoIds = res.locals.posts.map(x => x._id)
			let query = {
				'_id': {
					'$nin': deletePostMongoIds
				},
				'ip.single': {
					'$in': deletePostIps
				}
			};
			if (req.body.delete_ip_thread) {
				const ips_threads = [...boardThreadMap[req.params.board].threads];
				query['board'] = req.params.board;
				query['$or'] = [
					{
						'thread': {
							'$in': ips_threads
						}
					},
					{
						'postId': {
							'$in': ips_threads
						}
					}
				];
			} else if (req.body.delete_ip_board) {
				query['board'] = req.params.board;
			}
			const deleteIpPosts = await Posts.db.find(query).toArray();
			res.locals.posts = res.locals.posts.concat(deleteIpPosts);
		}
		if (res.locals.posts.length > postsBefore) {
			//recalc for extra fetched posts
			const updatedAffected = await getAffectedBoards(res.locals.posts, deleting);
			boardThreadMap = updatedAffected.boardThreadMap;
			beforePages = updatedAffected.beforePages;
			threadBoards = updatedAffected.threadBoards;
		}
		if (req.body.delete_file) {
			const { message } = await deletePostsFiles(res.locals.posts, false); //delete files, not just unlink
			messages.push(message);
		}
		const { action, message } = await deletePosts(res.locals.posts, req.body.delete_ip_global ? null : req.params.board);
		messages.push(message);
		if (action) {
			if (req.body.delete) {
				modlogActions.push('Delete');
			} else if (req.body.delete_ip_board) {
				modlogActions.push('Delete by IP');
			} else if (req.body.delete_ip_global) {
				modlogActions.push('Global delete by IP');
			}
			aggregateNeeded = true;
		}
	} else if (req.body.move) {
		if (boardThreadMap[req.params.board].directThreads.size > 0) {
			const threadIds = [...boardThreadMap[req.params.board].directThreads];
			const fetchMovePosts = await Posts.db.find({
				'board': req.params.board,
				'thread': {
					'$in': threadIds
				}
			}).toArray();
			res.locals.posts = res.locals.posts.concat(fetchMovePosts);
		}
		const { message, action } = await movePosts(req, res);
		if (action) {
			modlogActions.push('Moved');
			aggregateNeeded = true;
		}
		messages.push(message);
	} else {
		// if it was getting deleted/moved, dont do these actions
		if (req.body.unlink_file || req.body.delete_file) {
			const { message, action, query } = await deletePostsFiles(res.locals.posts, req.body.unlink_file);
			if (action) {
				if (req.body.unlink_file) {
					modlogActions.push('Unlink files');
				} else if (req.body.delete_file) {
					modlogActions.push('Delete files');
				}
				aggregateNeeded = true;
				combinedQuery[action] = { ...combinedQuery[action], ...query}
			}
			messages.push(message);
		} else if (req.body.spoiler) {
			const { message, action, query } = spoilerPosts(res.locals.posts);
			if (action) {
				modlogActions.push('Spoiler files');
				combinedQuery[action] = { ...combinedQuery[action], ...query}
			}
			messages.push(message);
		}
		//lock, sticky, bumplock, cyclic
		if (req.body.bumplock) {
			const { message, action, query } = bumplockPosts(res.locals.posts);
			if (action) {
				modlogActions.push('Bumplock');
				combinedQuery[action] = { ...combinedQuery[action], ...query}
			}
			messages.push(message);
		}
		if (req.body.lock) {
			const { message, action, query } = lockPosts(res.locals.posts);
			if (action) {
				modlogActions.push('Lock');
				combinedQuery[action] = { ...combinedQuery[action], ...query}
			}
			messages.push(message);
		}
		if (req.body.sticky) {
			const { message, action, query } = stickyPosts(res.locals.posts);
			if (action) {
				modlogActions.push('Sticky');
				combinedQuery[action] = { ...combinedQuery[action], ...query}
			}
			messages.push(message);
		}
		if (req.body.cyclic) {
			const { message, action, query } = cyclePosts(res.locals.posts);
			if (action) {
				modlogActions.push('Cycle');
				combinedQuery[action] = { ...combinedQuery[action], ...query}
			}
			messages.push(message);
		}
		// cannot report and dismiss at same time
		if (req.body.report || req.body.global_report) {
			const { message, action, query } = reportPosts(req, res);
			if (action) {
				//no modlog entry for making reports
				combinedQuery[action] = { ...combinedQuery[action], ...query}
			}
			messages.push(message);
		} else if (req.body.dismiss || req.body.global_dismiss) {
			const { message, action, query } = dismissReports(req, res);
			if (action) {
				if (req.body.dismiss) {
					modlogActions.push('Dismiss reports');
				} else if (req.body.global_dismiss) {
					modlogActions.push('Dismiss global reports');
				}
				combinedQuery[action] = { ...combinedQuery[action], ...query}
			}
			messages.push(message);
		}
	}
	if (Object.keys(combinedQuery).length > 0) {
		await Posts.db.updateMany({
			'_id': {
				'$in': res.locals.posts.map(p => Mongo.ObjectId(p._id))
			}
		}, combinedQuery);
	}

	let buildBoards = {};
	//get all affected boards for templates if necessary. can be multiple boards from global actions
	if (modlogActions.length > 0 || res.locals.actions.numBuild > 0) {
		buildBoards = (await Boards.db.find({
			'_id': {
				'$in': threadBoards
			}
		}).toArray()).reduce((acc, curr) => {
			if (!acc[curr._id]) {
				acc[curr._id] = curr;
			}
			return acc;
		}, buildBoards);
	}

	const parallelPromises = [];

	//modlog
	if (modlogActions.length > 0) {
		const modlog = {};
		const logDate = new Date(); //all events current date
		const message = req.body.log_message || null;
		let logUser;
		if (res.locals.permLevel < 4) { //if staff
			logUser = req.session.user;
		} else {
			logUser = 'Unregistered User';
		}
		for (let i = 0; i < res.locals.posts.length; i++) {
			const post = res.locals.posts[i];
			if (!modlog[post.board]) {
				//per board actions, all actions combined to one event
				modlog[post.board] = {
					showLinks: !deleting,
					postLinks: [],
					actions: modlogActions,
					date: logDate,
					showUser: !req.body.hide_name || logUser === 'Unregistered User' ? true : false,
					message: message,
					user: logUser,
					ip: {
						single: res.locals.ip.single,
						raw: res.locals.ip.raw
					}
				};
			}
			modlog[post.board].postLinks.push({
				postId: post.postId,
				thread: req.body.move ? req.body.move_to_thread : post.thread,
			});
		}
		const modlogDocuments = [];
		for (let i = 0; i < threadBoards.length; i++) {
			const boardName = threadBoards[i];
			const boardLog = modlog[boardName];
			//make it into documents for the db
			modlogDocuments.push({
				...boardLog,
				'board': boardName
			});
		}
		if (modlogDocuments.length > 0) {
			//insert the modlog docs
			await Modlogs.insertMany(modlogDocuments);
			for (let i = 0; i < threadBoards.length; i++) {
				const board = buildBoards[threadBoards[i]];
				buildQueue.push({
					'task': 'buildModLog',
					'options': {
						'board': board,
					}
				});
				buildQueue.push({
					'task': 'buildModLogList',
					'options': {
						'board': board,
					}
				});
			}
		}
	}

	//if there are actions that can cause some rebuilding
	if (res.locals.actions.numBuild > 0) {

		//make it into an OR query for the db
		const queryOrs = [];
		for (let i = 0; i < threadBoards.length; i++) {
			const threadBoard = threadBoards[i];
			//convert this to an array while we are here
			boardThreadMap[threadBoard].threads = [...boardThreadMap[threadBoard].threads];
			boardThreadMap[threadBoard].directThreads = [...boardThreadMap[threadBoard].directThreads];
			queryOrs.push({
				'board': threadBoard,
				'postId': {
					'$in': boardThreadMap[threadBoard].threads
				}
			})
		}

		//fetch threads per board that we only checked posts for
		let threadsEachBoard = [];
		if (queryOrs.length > 0) {
			threadsEachBoard = await Posts.db.find({
				'thread': null,
				'$or': queryOrs
			}).toArray();
		}

		//combine it with what we already had
		const selectedThreads = res.locals.posts.filter(post => post.thread === null)
		threadsEachBoard = threadsEachBoard.concat(selectedThreads)

		//get the oldest and newest thread for each board to determine how to delete
		let threadBounds = threadsEachBoard.reduce((acc, curr) => {
			if (!acc[curr.board] || curr.bumped < acc[curr.board].bumped) {
				acc[curr.board] = { oldest: null, newest: null};
			}
			if (!acc[curr.board].oldest || curr.bumped < acc[curr.board].oldest.bumped) {
				acc[curr.board].oldest = curr;
			}
			if (!acc[curr.board].newest || curr.bumped > acc[curr.board].newest.bumped) {
				acc[curr.board].newest = curr;
			}
			return acc;
		}, {});

		if (aggregateNeeded) {
			//recalculate replies and image counts if necessary
			const selectedPosts = res.locals.posts.filter(p => p.thread !== null);
			if (selectedPosts.length > 0) {
/* ignore
                               let threadOrs = selectedPosts.map(p => ({ board: p.board, postId: p.thread }));
                                let replyOrs = selectedPosts.map(p => ({ board: p.board, thread: p.thread }));
                                const [ threads, threadReplyAggregates] = await Promise.all([
                                        Posts.db.find({ '$or': threadOrs }), //i think this is in threadsEachBoard already
                                        Posts.getThreadAggregates(threadOrs)
                                ]);
*/
                                let replyOrs = selectedPosts.map(p => ({ board: p.board, thread: p.thread }));
				const threadReplyAggregates = await Posts.getThreadAggregates(replyOrs);
				const bulkWrites = [];
				const threads = threadsEachBoard;
				for (let i = 0; i < threads.length; i++) {
					const replyAggregate = threadReplyAggregates.find(ra => ra._id.thread === threads[i].postId && ra._id.board === threads[i].board);
					if (!replyAggregate) {
						//thread no longer has any reply post/files, set to 0 and reset bump date to post date.
						//sage replies and bumplock wouldnt matter in that case
						bulkWrites.push({
							'updateOne': {
								'filter': {
									'postId': threads[i].postId,
									'board': threads[i].board
								},
								'update': {
									'$set': {
										'replyposts': 0,
										'replyfiles': 0,
										'bumped': threads[i].date
									},
								}
							}
						});
						//threadbound already fixed for this
					} else {
						if (replyAggregate.bumped < threadBounds[replyAggregate._id.board].oldest.bumped) {
							threadBounds[replyAggregate._id.board].oldest = { bumped: replyAggregate.bumped };
						} else if (replyAggregate.bumped < threadBounds[replyAggregate._id.board].newest.bumped) {
							threadBounds[replyAggregate._id.board].newest = { bumped: replyAggregate.bumped };
						}
						//use results from first aggregate for threads with replies still existing
						const aggregateSet = {
							'replyposts': replyAggregate.replyposts,
							'replyfiles': replyAggregate.replyfiles,
						}
						if (!threads[i].bumplocked) {
							aggregateSet['bumped'] = replyAggregate.bumped;
						}
						bulkWrites.push({
							'updateOne': {
								'filter': {
									'postId': replyAggregate._id.thread,
									'board': replyAggregate._id.board
								},
								'update': {
									'$set': aggregateSet,
								}
							}
						});
					}
				}
				if (bulkWrites.length > 0) {
					await Posts.db.bulkWrite(bulkWrites);
				}
			}
			//afterwards, fix webring and board list latest post activity now. based on last bump date of a non bumplocked thread
			await Posts.fixLatest(threadBoards);
		}
		for (let i = 0; i < threadBoards.length; i++) {
			const boardName = threadBoards[i];
			const bounds = threadBounds[boardName];
			const board = buildBoards[boardName];
			//rebuild impacted threads
			if (req.body.move) {
				buildQueue.push({
					'task': 'buildThread',
					'options': {
						'threadId': req.body.move_to_thread,
						'board': board,
					}
				});
			}
			for (let j = 0; j < boardThreadMap[boardName].threads.length; j++) {
				buildQueue.push({
					'task': 'buildThread',
					'options': {
						'threadId': boardThreadMap[boardName].threads[j],
						'board': board,
					}
				});
			}
			//refresh any pages affected
			const afterPages = Math.ceil((await Posts.getPages(boardName)) / 10);
			let catalogRebuild = true;
			if ((beforePages[boardName] && beforePages[boardName] !== afterPages) || req.body.move) { //handle moves here since dates would change and not work in old/new page calculations
				if (afterPages < beforePages[boardName]) {
					//amount of pages changed, rebuild all pages and delete  any further pages (if pages amount decreased)
					for (let k = beforePages[boardName]; k > afterPages; k--) {
						//deleting html for pages that no longer should exist
						parallelPromises.push(remove(`${uploadDirectory}/html/${boardName}/${k}.html`));
						parallelPromises.push(remove(`${uploadDirectory}/json/${boardName}/${k}.json`));
					}
				}
				buildQueue.push({
					'task': 'buildBoardMultiple',
					'options': {
						'board': board,
						'startpage': 1,
						'endpage': afterPages,
					}
				});
			} else {
				//number of pages did not change, only possibly building existing pages
				const threadPageOldest = await Posts.getThreadPage(boardName, bounds.oldest);
				const threadPageNewest = bounds.oldest.postId === bounds.newest.postId ? threadPageOldest : await Posts.getThreadPage(boardName, bounds.newest);
				if (deleting) {
					if (boardThreadMap[boardName].directThreads.length === 0) {
						//only deleting posts from threads, so thread order wont change, thus we dont delete all pages after
						buildQueue.push({
							'task': 'buildBoardMultiple',
							'options': {
								'board': board,
								'startpage': threadPageNewest,
								'endpage': threadPageOldest,
							}
						});
					} else {
						//deleting threads, so we delete all pages after
						buildQueue.push({
							'task': 'buildBoardMultiple',
							'options': {
								'board': board,
								'startpage': threadPageNewest,
								'endpage': afterPages,
							}
						});
					}
				} else if (req.body.sticky) { //else if -- if deleting, other actions are not executed/irrelevant
					//rebuild current and newer pages
					buildQueue.push({
						'task': 'buildBoardMultiple',
						'options': {
							'board': board,
							'startpage': 1,
							'endpage': threadPageOldest,
						}
					});
				} else if (req.body.lock || req.body.bumplock || req.body.cyclic || req.body.unlink_file) {
					buildQueue.push({
						'task': 'buildBoardMultiple',
						'options': {
							'board': board,
							'startpage': threadPageNewest,
							'endpage': threadPageOldest,
						}
					});
				} else if (req.body.spoiler || req.body.ban || req.body.global_ban) {
					buildQueue.push({
						'task': 'buildBoardMultiple',
						'options': {
							'board': board,
							'startpage': threadPageNewest,
							'endpage': afterPages,
						}
					});
					if (boardThreadMap[boardName].directThreads.length === 0) {
						catalogRebuild = false;
						//these actions dont affect the catalog tile since not on an OP and dont change reply/image counts
					}
				}
			}
			if (catalogRebuild) {
				//the actions will affect the catalog, so we better rebuild it
				buildQueue.push({
					'task': 'buildCatalog',
					'options': {
						'board': board,
					}
				});
			}
		}
	}

	if (parallelPromises.length > 0) {
		//since queue changes, this just removing old html files
		await Promise.all(parallelPromises);
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'messages': messages,
		redirect,
	});

}


code:
'use strict';

const { Bans, Modlogs } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, hashIp = require(__dirname+'/../../helpers/dynamic.js')
	, buildQueue = require(__dirname+'/../../queue.js')
	, { isIP } = require('net')
	, { ipHashPermLevel, defaultBanDuration } = require(__dirname+'/../../configs/main.js');

module.exports = async (req, res, redirect) => {

	const actionDate = new Date();

	const banPromise = Bans.insertOne({
		//note: raw ip and type single because of
		'type': 'single',
		'ip': {
			'single': isIP(req.body.ip) ? hashIp(req.body.ip) : req.body.ip,
			'raw':  req.body.ip,
		},
		'reason': req.body.ban_reason || req.body.log_message || 'No reason specified',
		'board': req.params.board || null,
		'posts': null,
		'issuer': req.session.user,
		'date': actionDate,
		'expireAt': new Date(actionDate.getTime() + (req.body.ban_duration || defaultBanDuration)),
		'allowAppeal': req.body.no_appeal ? false : true,
		'appeal': null,
		'seen': false,
	});

	const modlogPromise = Modlogs.insertOne({
		'board': req.params.board || null,
		'showLinks': false,
		'postLinks': [],
		'actions': [(req.params.board ? 'Ban' : 'Global Ban')],
		'date': actionDate,
		'showUser': !req.body.hide_name || res.locals.permLevel >= 4 ? true : false,
		'message': req.body.log_message || null,
		'user': res.locals.permLevel < 4 ? req.session.user : 'Unregistered User',
		'ip': {
			'single': res.locals.ip.single,
			'raw': res.locals.ip.raw
		}
	});

	await Promise.all([banPromise, modlogPromise]);

	if (req.params.board) {
		buildQueue.push({
			'task': 'buildModLog',
			'options': {
				'board': res.locals.board,
			}
		});
		buildQueue.push({
			'task': 'buildModLogList',
			'options': {
				'board': res.locals.board,
			}
		});
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': 'Added ban',
		redirect,
	});

}


code:
'use strict';

const { News } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, buildQueue = require(__dirname+'/../../queue.js')
	, { prepareMarkdown } = require(__dirname+'/../../helpers/posting/markdown.js')
	, messageHandler = require(__dirname+'/../../helpers/posting/message.js');

module.exports = async (req, res, next) => {

	const message = prepareMarkdown(req.body.message, false);
	const { message: markdownNews } = await messageHandler(message, null, null);

	const post = {
		'title': req.body.title,
		'message': {
			'raw': message,
			'markdown': markdownNews
		},
		'date': new Date(),
	};

	await News.insertOne(post);

	buildQueue.push({
		'task': 'buildNews',
		'options': {}
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': 'Added newspost',
		'redirect': '/globalmanage/news.html'
	});

}


code:
'use strict';

const { Bans } = require(__dirname+'/../../db/');

module.exports = async (req, res, next) => {

	return Bans.appeal(res.locals.ip.single, req.body.checkedbans, req.body.message).then(r => r.modifiedCount);

}


code:
'use strict';

const { Bans } = require(__dirname+'/../../db/')
	, { defaultBanDuration } = require(__dirname+'/../../configs/main.js');

module.exports = async (req, res, next) => {

	const banDate = new Date();
	const banExpiry = new Date(banDate.getTime() + (req.body.ban_duration || defaultBanDuration)); //uses config default if missing or malformed
	const banReason = req.body.ban_reason || req.body.log_message || 'No reason specified';
	const allowAppeal = (req.body.no_appeal || req.body.ban_q || req.body.ban_h) ? false : true; //dont allow appeals for range bans

	const bans = [];

	if (req.body.ban || req.body.global_ban) {
		const banBoard = req.body.global_ban ? null : req.params.board;
		const ipPosts = res.locals.posts.reduce((acc, post) => {
			if (!acc[post.ip.single]) {
				acc[post.ip.single] = [];
			}
			acc[post.ip.single].push(post);
			return acc;
		}, {});
		for (let ip in ipPosts) {
			const thisIpPosts = ipPosts[ip];
			let type = 'single';
			let banIp = {
				single: ip,
				raw: thisIpPosts[0].ip.raw
			};
			if (req.body.ban_h) {
				type = 'half';
				banIp.single = thisIpPosts[0].ip.hrange;
			} else if (req.body.ban_q) {
				type = 'quarter';
				banIp.single = thisIpPosts[0].ip.qrange;
			}
			bans.push({
				type,
				'ip': banIp,
				'reason': banReason,
				'board': banBoard,
				'posts': req.body.preserve_post ? thisIpPosts : null,
				'issuer': req.session.user,
				'date': banDate,
				'expireAt': banExpiry,
				allowAppeal,
				'appeal': null,
				'seen': false,
			});
		}
	}

	if (req.body.report_ban || req.body.global_report_ban){
		const banBoard = req.body.global_report_ban ? null : req.params.board;
		res.locals.posts.forEach(post => {
			let ips = [];
			if (req.body.report_ban) {
				const matches = post.reports.map(r => {
					if (req.body.checkedreports.includes(r.id.toString())) {
						return r.ip;
					}
				});
				ips = ips.concat(matches);
			}
			if (req.body.global_report_ban) {
				const matches = post.globalreports.map(r => {
					if (req.body.checkedreports.includes(r.id.toString())) {
						return r.ip;
					}
				});
				ips = ips.concat(matches);
			}
			[...new Set(ips)].forEach(ip => {
				bans.push({
					'ip': ip,
					'type': 'single',
					'reason': banReason,
					'board': banBoard,
					'posts': null,
					'issuer': req.session.user,
					'date': banDate,
					'expireAt': banExpiry,
					allowAppeal,
					'appeal': null,
					'seen': false
				});
			});
		});
	}

	const numBans = await Bans.insertMany(bans).then(result => result.insertedCount);

	const query = {
        message: `Added ${numBans} bans`,
	};

	if ((req.body.ban || req.body.global_ban ) && req.body.ban_reason) {
		res.locals.actions.numBuild++;//there was a ban reason, so we may need to rebuild some pages, since banning is usually not a building action
		query['action'] = '$set';
		query['query'] = {
			'banmessage': req.body.ban_reason
		};
	}

	return query;

}


code:
'use strict';

const { Bypass } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { secureCookies, blockBypass } = require(__dirname+'/../../configs/main.js')
	, production = process.env.NODE_ENV === 'production';

module.exports = async (req, res, next) => {

	const bypass = await Bypass.getBypass();
	const bypassId = bypass.insertedId;
	res.locals.blockBypass = bypass.ops[0];

	res.cookie('bypassid', bypassId.toString(), {
		'maxAge': blockBypass.expireAfterTime,
		'secure': production && secureCookies && (req.headers['x-forwarded-proto'] === 'https'),
		'sameSite': 'strict',
		'signed': true,
	})

	return dynamicResponse(req, res, 200, 'message', {
		'minimal': req.body.minimal,
		'title': 'Success',
		'message': 'Completed block bypass, you may go back and make your post.',
	});

}


code:
'use strict';

const { NumberInt } = require(__dirname+'/../../db/db.js')

module.exports = (posts) => {

	const filteredposts = posts.filter(post => {
		return !post.thread
	})

	if (filteredposts.length === 0) {
		return {
			message: 'No thread(s) to bumplock',
		};
	}

	return {
		message: `Toggled bumplock for ${filteredposts.length} thread(s)`,
		action: '$bit',
		query: {
			'bumplocked': {
				'xor': NumberInt(1)
			},
		}
	};

}


code:
'use strict';

const { Boards, Posts, Accounts } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { globalLimits } = require(__dirname+'/../../configs/main.js')
	, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
	, buildQueue = require(__dirname+'/../../queue.js')
	, { remove } = require('fs-extra')
	, deletePosts = require(__dirname+'/deletepost.js')
	, { prepareMarkdown } = require(__dirname+'/../../helpers/posting/markdown.js')
	, messageHandler = require(__dirname+'/../../helpers/posting/message.js')
	, { countryCodes } = require(__dirname+'/../../helpers/countries.js')
	, validCountryCodes = new Set(countryCodes)
	, trimSetting = (setting, oldSetting) => {
		return setting && setting.trim().length > 0 ? setting : oldSetting;
	}
	, numberSetting = (setting, oldSetting) => {
		return typeof setting === 'number' && setting !== oldSetting ? setting : oldSetting;
	}
	, booleanSetting = (setting) => {
		return setting != null;
	}
	, arraySetting = (setting, oldSetting, limit) => {
		return setting !== null ? setting.split(/\r?\n/).filter(n => n).slice(0,limit) : oldSettings;
	};

module.exports = async (req, res, next) => {

	//oldsettings before changes
	const oldSettings = res.locals.board.settings;

	//array of promises we might need
	const promises = [];

	const announcement = req.body.announcement === null ? null : prepareMarkdown(req.body.announcement, false);
	let markdownAnnouncement = oldSettings.announcement.markdown;
	if (announcement !== oldSettings.announcement.raw) {
		({ message: markdownAnnouncement } = await messageHandler(announcement, req.params.board, null))
	}

	let moderators = req.body.moderators != null ? req.body.moderators.split(/\r?\n/).filter(n => n && !(n == res.locals.board.owner)).slice(0,10) : [];
	if (moderators.length === 0 && oldSettings.moderators.length > 0) {
		//remove all mods if mod list being emptied
		promises.push(Accounts.removeModBoard(oldSettings.moderators, req.params.board));
	} else if (moderators !== oldSettings.moderators) {
		if (moderators.length > 0) {
			//make sure moderators actually have existing accounts
			const validCount = await Accounts.countUsers(moderators);
			if (validCount !== moderators.length) {
				//some usernames were not valid, reset to old setting
				moderators = oldSettings.moderators;
			} else {
				//all accounts exist, check added/removed
				const modsRemoved = oldSettings.moderators.filter(m => !moderators.includes(m));
				const modsAdded = moderators.filter(m => !oldSettings.moderators.includes(m));
				if (modsRemoved.length > 0) {
					promises.push(Accounts.removeModBoard(modsRemoved, req.params.board));
				}
				if (modsAdded.length > 0) {
					promises.push(Accounts.addModBoard(modsAdded, req.params.board));
				}
			}
		}
	}

	if (req.body.countries) {
		req.body.countries = [...new Set(req.body.countries)] //prevents submitting multiple of same code, not like it matters, but meh
			.filter(code => validCountryCodes.has(code))
			.slice(0,250);
	}

	const newSettings = {
		moderators,
		'name': trimSetting(req.body.name, oldSettings.name),
		'description': trimSetting(req.body.description, oldSettings.description),
		'defaultName': trimSetting(req.body.default_name, oldSettings.defaultName),
		'theme': req.body.theme || oldSettings.theme,
		'codeTheme': req.body.code_theme || oldSettings.codeTheme,
		'sfw': booleanSetting(req.body.sfw),
		'unlistedLocal': booleanSetting(req.body.unlisted_local),
		'unlistedWebring': booleanSetting(req.body.unlisted_webring),
		'early404': booleanSetting(req.body.early404),
		'ids': booleanSetting(req.body.ids),
		'flags': booleanSetting(req.body.flags),
		'forceAnon': booleanSetting(req.body.force_anon),
		'sageOnlyEmail': booleanSetting(req.body.sage_only_email),
		'userPostDelete': booleanSetting(req.body.user_post_delete),
		'userPostSpoiler': booleanSetting(req.body.user_post_spoiler),
		'userPostUnlink': booleanSetting(req.body.user_post_unlink),
		'forceThreadMessage': booleanSetting(req.body.force_thread_message),
		'forceThreadFile': booleanSetting(req.body.force_thread_file),
		'forceReplyMessage': booleanSetting(req.body.force_reply_message),
		'forceReplyFile': booleanSetting(req.body.force_reply_file),
		'forceThreadSubject': booleanSetting(req.body.force_thread_subject),
		'disableReplySubject': booleanSetting(req.body.disable_reply_subject),
		'captchaMode': numberSetting(req.body.captcha_mode, oldSettings.captchaMode),
		'tphTrigger': numberSetting(req.body.tph_trigger, oldSettings.tphTrigger),
		'tphTriggerAction': numberSetting(req.body.tph_trigger_action, oldSettings.tphTriggerAction),
		'pphTrigger': numberSetting(req.body.pph_trigger, oldSettings.pphTrigger),
		'pphTriggerAction': numberSetting(req.body.pph_trigger_action, oldSettings.pphTriggerAction),
		'captchaReset': numberSetting(req.body.captcha_reset, oldSettings.captchaReset),
		'lockReset': numberSetting(req.body.lock_reset, oldSettings.lockReset),
		'threadLimit': numberSetting(req.body.thread_limit, oldSettings.threadLimit),
		'replyLimit': numberSetting(req.body.reply_limit, oldSettings.replyLimit),
		'bumpLimit': numberSetting(req.body.bump_limit, oldSettings.bumpLimit),
		'maxFiles': numberSetting(req.body.max_files, oldSettings.maxFiles),
		'minThreadMessageLength': numberSetting(req.body.min_thread_message_length, oldSettings.minThreadMessageLength),
		'minReplyMessageLength': numberSetting(req.body.min_reply_message_length, oldSettings.minReplyMessageLength),
		'maxThreadMessageLength': numberSetting(req.body.max_thread_message_length, oldSettings.maxThreadMessageLength),
		'maxReplyMessageLength': numberSetting(req.body.max_reply_message_length, oldSettings.maxReplyMessageLength),
		'lockMode': numberSetting(req.body.lock_mode, oldSettings.lockMode),
		'messageR9KMode': numberSetting(req.body.message_r9k_mode, oldSettings.messageR9KMode),
		'fileR9KMode': numberSetting(req.body.file_r9k_mode, oldSettings.fileR9KMode),
		'filterMode': numberSetting(req.body.filter_mode, oldSettings.filterMode),
		'filterBanDuration': numberSetting(req.body.ban_duration, oldSettings.filterBanDuration),
		'tags': arraySetting(req.body.tags, oldSettings.tags, 10),
		'filters': arraySetting(req.body.filters, oldSettings.filters, 50),
		'blockedCountries': req.body.countries || [],
		'disableOnionFilePosting': booleanSetting(req.body.disable_onion_file_posting),
		'strictFiltering': booleanSetting(req.body.strict_filtering),
		'customCss': globalLimits.customCss.enabled ? (req.body.custom_css !== null ? req.body.custom_css : oldSettings.customCss) : null,
		'announcement': {
			'raw': announcement !== null ? announcement : oldSettings.announcement.raw,
			'markdown': req.body.announcement !== null ? markdownAnnouncement : oldSettings.announcement.markdown,
		},
		'allowedFileTypes': {
			'animatedImage': booleanSetting(req.body.files_allow_animated_image),
			'image': booleanSetting(req.body.files_allow_image),
			'video': booleanSetting(req.body.files_allow_video),
			'audio': booleanSetting(req.body.files_allow_audio),
			'other': booleanSetting(req.body.files_allow_other),
		},
	};

	//settings changed in the db
	await Boards.updateOne(req.params.board, {
		'$set':  {
			'settings': newSettings,
		}
	});

	//update this in locals incase is used in later parts
	res.locals.board.settings = newSettings;

	//pages in new vs old settings
	const oldMaxPage = Math.ceil(oldSettings.threadLimit/10);
	const newMaxPage = Math.ceil(newSettings.threadLimit/10);

	let rebuildThreads = false
		, rebuildBoard = false
		, rebuildCatalog = false
		, rebuildOther = false;

	if (newSettings.captchaMode > oldSettings.captchaMode) {
		if (oldSettings.captchaMode === 0) {
			rebuildBoard = true;
			rebuildCatalog = true;
		}
		if (newSettings.captchaMode === 2) {
			rebuildThreads = true;
		}
	} else if (newSettings.captchaMode < oldSettings.captchaMode) {
		if (oldSettings.captchaMode === 2) {
			rebuildThreads = true;
		}
		if (newSettings.captchaMode === 0) {
			rebuildBoard = true;
			rebuildCatalog = true;
		}
	}

	//do rebuilding and pruning if max number of pages is changed and any threads are pruned
	if (newMaxPage < oldMaxPage) {
		//prune old threads
		const prunedThreads = await Posts.pruneThreads(res.locals.board);
		if (prunedThreads.length > 0) {
			await deletePosts(prunedThreads, req.params.board);
			//remove board page html/json for pages > newMaxPage
			for (let i = newMaxPage+1; i <= oldMaxPage; i++) {
				promises.push(remove(`${uploadDirectory}/html/${req.params.board}/${i}.html`));
				promises.push(remove(`${uploadDirectory}/json/${req.params.board}/${i}.json`));
			}
			//rebuild all board pages for page nav numbers, and catalog
			rebuildBoard = true;
			rebuildCatalog = true;
		}
	}

	if (newSettings.name !== oldSettings.name
		|| newSettings.description !== oldSettings.description
		|| newSettings.theme !== oldSettings.theme
		|| newSettings.codeTheme !== oldSettings.codeTheme
		|| newSettings.announcement.raw !== oldSettings.announcement.raw
		|| newSettings.customCss !== oldSettings.customCss) {
		rebuildThreads = true;
		rebuildBoard = true;
		rebuildCatalog = true;
		rebuildOther = true;
	}

	if (rebuildThreads) {
		promises.push(remove(`${uploadDirectory}/html/${req.params.board}/thread/`));
	}
	if (rebuildBoard) {
		buildQueue.push({
			'task': 'buildBoardMultiple',
			'options': {
				'board': res.locals.board,
				'startpage': 1,
				'endpage': newMaxPage
			}
		});
	}
	if (rebuildCatalog) {
		buildQueue.push({
			'task': 'buildCatalog',
			'options': {
				'board': res.locals.board,
			}
		});
	}
	if (rebuildOther) {
		promises.push(remove(`${uploadDirectory}/html/${req.params.board}/logs/`));
		buildQueue.push({
			'task': 'buildModLogList',
			'options': {
				'board': res.locals.board,
			}
		});
		buildQueue.push({
			'task': 'buildBanners',
			'options': {
				'board': res.locals.board,
			}
		});
	}

	//finish the promises in parallel e.g. removing files
	if (promises.length > 0) {
		await Promise.all(promises);
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': 'Updated settings.',
		'redirect': `/${req.params.board}/manage/settings.html`
	});

}


code:
'use strict';

const { Boards, Posts, Accounts } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
	, buildQueue = require(__dirname+'/../../queue.js')
	, cache = require(__dirname+'/../../redis.js')
	, { remove } = require('fs-extra');

module.exports = async (req, res, next) => {

	const promises = [];
	const oldSettings = await cache.get('globalsettings');
	const newSettings = {
//		'captchaMode': typeof req.body.captcha_mode === 'number' && req.body.captcha_mode !== oldSettings.captchaMode ? req.body.captcha_mode : oldSettings.captchaMode,
		'filters': req.body.filters !== null ? req.body.filters.split(/\r?\n/).filter(n => n) : oldSettings.filters,
		'filterMode': typeof req.body.filter_mode === 'number' && req.body.filter_mode !== oldSettings.filterMode ? req.body.filter_mode : oldSettings.filterMode,
		'filterBanDuration': typeof req.body.ban_duration === 'number' && req.body.ban_duration !== oldSettings.filterBanDuration ? req.body.ban_duration : oldSettings.filterBanDuration,
	};

	cache.set('globalsettings', newSettings);

	let rebuildThreads = false
		, rebuildBoard = false
		, rebuildCatalog = false;

/*
//todo: implement removing pages/rebuilding for all affected boards i.e. query for ones with settings.catchaMode < newSettings.captchaMode
	if (newSettings.captchaMode > oldSettings.captchaMode) {
		rebuildBoard = true;
		rebuildCatalog = true;
		if (newSettings.captchaMode == 2) {
			rebuildThreads = true; //thread captcha enabled, removes threads
		}
		const affectedBoards = //query here
		for (let i = 0; i < affectedBoards.length; i++) {
			const board = affectedBoards[i];
			if (rebuildThreads) {
				promises.push(remove(`${uploadDirectory}/html/${board._id}/thread/`));
			}
			if (rebuildBoard) {
				buildQueue.push({
					'task': 'buildBoardMultiple',
					'options': {
						board,
						'startpage': 1,
						'endpage': null //no endpage will use whatver maxpage of board is
					}
				});
			}
			if (rebuildCatalog) {
				buildQueue.push({
					'task': 'buildCatalog',
					'options': {
						board,
					}
				});
			}
		}
	}
*/

	//finish the promises in parallel e.g. removing files
	if (promises.length > 0) {
		await Promise.all(promises);
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': 'Updated settings.',
		'redirect': '/globalmanage/settings.html'
	});

}


code:
'use strict';

const bcrypt = require('bcrypt')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { Accounts } = require(__dirname+'/../../db/');

module.exports = async (req, res, next) => {

	const username = req.body.username.toLowerCase();
	const password = req.body.password;
	const newPassword = req.body.newpassword;

	//fetch an account
	const account = await Accounts.findOne(username);

	//if the account doesnt exist, reject
	if (!account) {
		return dynamicResponse(req, res, 403, 'message', {
			'title': 'Forbidden',
			'message': 'Incorrect username or password',
			'redirect': '/changepassword.html'
		});
	}

	// bcrypt compare input to saved hash
	const passwordMatch = await bcrypt.compare(password, account.passwordHash);

	//if hashes matched
	if (passwordMatch === false) {
		return dynamicResponse(req, res, 403, 'message', {
			'title': 'Forbidden',
			'message': 'Incorrect username or password',
			'redirect': '/changepassword.html'
		});
	}

	//change the password
	await Accounts.changePassword(username, newPassword);

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': 'Changed password',
		'redirect': '/login.html'
	});

}


code:
'use strict';

const { Boards, Accounts } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
	, restrictedURIs = new Set(['captcha', 'forms', 'randombanner', 'all'])
	, { ensureDir } = require('fs-extra')
	, { boardDefaults } = require(__dirname+'/../../configs/main.js');

module.exports = async (req, res, next) => {

	const { name, description } = req.body
		, uri = req.body.uri.toLowerCase()
		, tags = req.body.tags.split(/\r?\n/).filter(n => n)
		, owner = req.session.user;

	if (restrictedURIs.has(uri)) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad Request',
			'message': 'That URI is not available for board creation',
			'redirect': '/create.html'
		});
	}

	const board = await Boards.findOne(uri);

	// if board exists reject
	if (board != null) {
		return dynamicResponse(req, res, 409, 'message', {
			'title': 'Conflict',
			'message': 'Board with this URI already exists',
			'redirect': '/create.html'
		});
	}

	//todo: add a settings for defaults
	const newBoard = {
		'_id': uri,
		owner,
		'banners': [],
		'sequence_value': 1,
		'pph': 0,
		'ips': 0,
		'lastPostTimestamp': null,
		'settings': {
			name,
			description,
			tags,
			'moderators': [],
			...boardDefaults
		}
	}

	await Promise.all([
		Boards.insertOne(newBoard),
		Accounts.addOwnedBoard(owner, uri),
		ensureDir(`${uploadDirectory}/html/${uri}`),
		ensureDir(`${uploadDirectory}/json/${uri}`),
		ensureDir(`${uploadDirectory}/banner/${uri}`)
	]);

	return res.redirect(`/${uri}/index.html`);

}


code:
'use strict';

const { NumberInt } = require(__dirname+'/../../db/db.js')

module.exports = (posts) => {

	const filteredposts = posts.filter(post => {
		return !post.thread
	})

	if (filteredposts.length === 0) {
		return {
			message: 'No thread(s) to cycle',
		};
	}

	return {
		message: `Toggled Cyclical mode for ${filteredposts.length} thread(s)`,
		action: '$bit',
		query: {
			'cyclic': {
				'xor': NumberInt(1)
			},
		}
	};

}


code:
'use strict';

const { Accounts } = require(__dirname+'/../../db/')

module.exports = async (username) => {

	//this definitely needs to be its own file (v:
	await Accounts.deleteOne(username);

}


code:
'use strict';

const { remove } = require('fs-extra')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
	, { Boards } = require(__dirname+'/../../db/')
	, buildQueue = require(__dirname+'/../../queue.js');

module.exports = async (req, res, next) => {

	const redirect = `/${req.params.board}/manage/banners.html`;

	//delete file of all selected banners
	await Promise.all(req.body.checkedbanners.map(async filename => {
		remove(`${uploadDirectory}/banner/${req.params.board}/${filename}`);
	}));

	//remove from db
	const amount = await Boards.removeBanners(req.params.board, req.body.checkedbanners);

	//update res locals banners in memory
	res.locals.board.banners = res.locals.board.banners.filter(banner => {
		 return !req.body.checkedbanners.includes(banner);
	});

	//rebuild public banners page
	buildQueue.push({
        'task': 'buildBanners',
		'options': {
			'board': res.locals.board,
		}
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': `Deleted banners.`,
		'redirect': redirect
	});
}


code:
'use strict';

const { Accounts, Boards, Stats, Posts, Bans, Modlogs } = require(__dirname+'/../../db/')
	, deletePosts = require(__dirname+'/deletepost.js')
	, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
	, { remove } = require('fs-extra');

module.exports = async (uri, board) => {

	//delete board
	await Boards.deleteOne(uri);
	//get all posts (should probably project to get files for deletin and anything else necessary)
	const allPosts = await Posts.allBoardPosts(uri);
	if (allPosts.length > 0) {
		//delete posts and decrement images
		await deletePosts(allPosts, uri, true);
	}
	await Promise.all([
		Accounts.removeOwnedBoard(board.owner, uri), //remove board from owner account
		board.settings.moderators.length > 0 ? Accounts.removeModBoard(board.settings.moderators) : void 0, //remove board from mods accounts
		Modlogs.deleteBoard(uri), //modlogs for the board
		Bans.deleteBoard(uri), //bans for the board
		Stats.deleteBoard(uri), //stats for the board
		remove(`${uploadDirectory}/html/${uri}/`), //html
		remove(`${uploadDirectory}/json/${uri}/`), //json
		remove(`${uploadDirectory}/banner/${uri}/`) //banners
	]);

}


code:
'use strict';

const { News } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, buildQueue = require(__dirname+'/../../queue.js')

module.exports = async (req, res, next) => {

	await News.deleteMany(req.body.checkednews);

	buildQueue.push({
		'task': 'buildNews',
		'options': {}
	});

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': 'Deleted news',
		'redirect': '/globalmanage/news.html'
	});

}


code:
'use strict';

const uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
	, { remove } = require('fs-extra')
	, Mongo = require(__dirname+'/../../db/db.js')
	, { Posts, Files } = require(__dirname+'/../../db/')
	, quoteHandler = require(__dirname+'/../../helpers/posting/quotes.js')
	, { markdown } = require(__dirname+'/../../helpers/posting/markdown.js')
	, { pruneImmediately } = require(__dirname+'/../../configs/main.js')
	, pruneFiles = require(__dirname+'/../../schedules/prune.js')
	, sanitize = require('sanitize-html')
	, sanitizeOptions = require(__dirname+'/../../helpers/posting/sanitizeoptions.js');

module.exports = async (posts, board, all=false) => {

	//filter to threads
	const threads = posts.filter(x => x.thread == null);

	if (threads.length > 0) {
		//delete the html/json for threads
		await Promise.all(threads.map(thread => {
			remove(`${uploadDirectory}/html/${thread.board}/thread/${thread.postId}.html`)
			remove(`${uploadDirectory}/json/${thread.board}/thread/${thread.postId}.json`)
		}));
	}

	//get posts from all threads
	let threadPosts = []
	if (all === false && threads.length > 0) {
		if (board) {
			//if this is board-specific, we can use a single query
			const threadPostIds = threads.map(thread => thread.postId);
			threadPosts = await Posts.getMultipleThreadPosts(board, threadPostIds);
		} else {
			//otherwise we fetch posts from threads on different boards separarely
//TODO: use big board:$or/postid:$in query so this can be tackled in a single db query
			await Promise.all(threads.map(async thread => {
				//for each thread, fetch all posts from the matching board and thread matching the threads postId
				const currentThreadPosts = await Posts.getThreadPosts(thread.board, thread.postId);
				threadPosts = threadPosts.concat(currentThreadPosts);
			}));
		}
	}

	//combine them all into one array, there may be duplicates but it shouldnt matter
	const allPosts = posts.concat(threadPosts);

	//get files for ref counting, backlinks for post re-markup, mongoids for deleting
	const { postFiles, postBacklinks, postMongoIds } = allPosts.reduce((acc, post) => {
		if (post.files.length > 0) {
			acc.postFiles = acc.postFiles.concat(post.files);
		}
		if (post.backlinks.length > 0) {
			acc.postBacklinks = acc.postBacklinks.concat(post.backlinks);
		}
		acc.postMongoIds.push(post._id);
		return acc;
	}, { postFiles: [], postBacklinks: [], postMongoIds: [] });

	if (postFiles.length > 0) {
		const fileNames = postFiles.map(x => x.filename)//[...new Set(postFiles.map(x => x.filename))];
        await Files.decrement(fileNames);
		if (pruneImmediately) {
			await pruneFiles(fileNames);
		}
	}

	const bulkWrites = [];
	const backlinkRebuilds = new Set();
	if (all === false) { //no need to rebuild quotes when deleting all posts for a board
		const deleteThreadMap = {};
		for (let i = 0; i < threads.length; i++) {
			const thread = threads[i];
			//if exists, add to set, else make the set
			if (!deleteThreadMap[thread.board]) {
				 deleteThreadMap[thread.board] = new Set();
			}
			deleteThreadMap[thread.board].add(thread.postId);
		}
		for (let j = 0; j < allPosts.length; j++) {
			const post = allPosts[j];
			backlinkRebuilds.delete(post._id); //make sure we dont try and remarkup this post since its getting deleted.
			if (post.thread != null && !deleteThreadMap[post.board] || !deleteThreadMap[post.board].has(post.thread)) {
				//get backlinks for posts to remarkup
				for (let i = 0; i < post.backlinks.length; i++) {
					backlinkRebuilds.add(post.backlinks[i]._id);
				}
				//remove dead backlinks to this post
				if (post.quotes.length > 0) {
					bulkWrites.push({
						'updateMany': {
							'filter': {
								'_id': {
									'$in': post.quotes.map(q => q._id)
								}
							},
							'update': {
								'$pull': {
									'backlinks': {
										'postId': post.postId
									}
								}
							}
						}
					});
				}
			}
		}
	}

	//deleting before remarkup so quotes are accurate
	const deletedPosts = await Posts.deleteMany(postMongoIds).then(result => result.deletedCount);

	if (all === false) {
		//get posts that quoted deleted posts so we can remarkup them
		if (backlinkRebuilds.size > 0) {
			const remarkupPosts = await Posts.globalGetPosts([...backlinkRebuilds]);
			await Promise.all(remarkupPosts.map(async post => { //doing these all at once
				if (post.nomarkup && post.nomarkup.length > 0) { //is this check even necessary? how would it have a quote with no message
					//redo the markup
					let message = markdown(post.nomarkup);
					const { quotedMessage, threadQuotes, crossQuotes } = await quoteHandler.process(post.board, message, post.thread);
					message = sanitize(quotedMessage, sanitizeOptions.after);
					bulkWrites.push({
						'updateOne': {
		                	'filter': {
		                    	'_id': post._id
		                	},
		                	'update': {
		                    	'$set': {
		                        	'quotes': threadQuotes,
									'crossquotes': crossQuotes,
									'message': message
		                    	}
		                	}
		            	}
		        	});
				}
			}));
		}
	}

	//bulkwrite it all
	if (bulkWrites.length > 0) {
		await Posts.db.bulkWrite(bulkWrites);
	}

	//hooray!
	return { action: deletedPosts > 0, message:`Deleted ${threads.length > 0 ? (threads.length + ' thread' + (threads.length > 1 ? 's' : '')) : ''} ${threads.length > 0 && deletedPosts-threads.length > 0 ? 'and' : ''} ${deletedPosts-threads.length > 0 ? (deletedPosts-threads.length + ' post' + (deletedPosts-threads.length > 1 ? 's' : '')) : ''}` };

}


code:
'use strict';

const { Files } = require(__dirname+'/../../db/')
	, { pruneImmediately } = require(__dirname+'/../../configs/main.js')
	, pruneFiles = require(__dirname+'/../../schedules/prune.js')
	, deletePostFiles = require(__dirname+'/../../helpers/files/deletepostfiles.js');

module.exports = async (posts, unlinkOnly) => {

	//get filenames from all the posts
	let files = [];
	for (let i = 0; i < posts.length; i++) {
		const post = posts[i];
		if (post.files.length > 0) {
			files = files.concat(post.files.map(file => {
				return {
					filename: file.filename,
					hash: file.hash,
					thumbextension: file.thumbextension
				};
			}));
		}
	}
	files = [...new Set(files)];

	if (files.length == 0) {
		return {
			 message: 'No files found'
		};
	}

	if (files.length > 0) {
		const fileNames = files.map(x => x.filename);
        await Files.decrement(fileNames);
		if (pruneImmediately) {
			await pruneFiles(fileNames);
		}
	}

	if (unlinkOnly) {
		return {
			message:`Unlinked ${files.length} file(s) across ${posts.length} post(s)`,
			action:'$set',
			query: {
				'files': []
			}
		};
	} else {
		//delete all the files
		await deletePostFiles(files);
		return {
			message:`Deleted ${files.length} file(s) from server`,
			//NOTE: only deletes from selected posts. other posts with same image will 404
			action:'$set',
			query: {
				'files': []
			}
		};
	}

}


code:
'use strict';

const { Bans } = require(__dirname+'/../../db/');

module.exports = async (req, res, next) => {

	return Bans.denyAppeal(req.params.board, req.body.checkedbans).then(result => result.modifiedCount);

}


code:
'use strict';

module.exports = (posts) => {

	const filteredposts = posts.filter(post => {
		return post.globalreports.length > 0
	})

	if (filteredposts.length === 0) {
		return {
			message: 'No global report(s) to dismiss'
		}
	}

	return {
		message: 'Dismissed global report(s)',
		action: '$set',
		query: {
			'globalreports': []
		}
	};

}


code:
'use strict';

module.exports = (req, res) => {

	const filteredposts = res.locals.posts.filter(post => {
		return (req.body.global_dismiss && post.globalreports.length > 0)
			|| (req.body.dismiss && post.reports.length > 0)
	});

	if (filteredposts.length === 0) {
		return {
			message: 'No report(s) to dismiss'
		}
	}

	const ret = {
		message: 'Dismissed reports',
		action: '$set',
		query: {}
	};
	ret.query[`${req.body.global_dismiss ? 'global' : ''}reports`] = [];

	return ret;

}


code:
'use strict';

const { Accounts, Boards } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, cache = require(__dirname+'/../../redis.js')

module.exports = async (req, res, next) => {

	//edit the accounts
	let amount = 0;
	if (req.body.delete_account) {
		const accountsWithBoards = await Accounts.getOwnedOrModBoards(req.body.checkedaccounts);
		if (accountsWithBoards.length > 0) {
			const bulkWrites = [];
			for (let i = 0; i < accountsWithBoards.length; i++) {
				const acc = accountsWithBoards[i];
				if (acc.modBoards.length > 0) {
					//remove from moderators of any boards they are mod on
					bulkWrites.push({
						'updateMany': {
							'filter': {
								'_id': {
									'$in': acc.modBoards
								}
							},
							'update': {
								'$pull': {
									'settings.moderators': acc._id
								}
							}
						}
					});
					cache.del(acc.modBoards.map(b => `board:${b}`));
				}
				if (acc.ownedBoards.length > 0) {
					//remove from moderators of any boards they are mod on
					bulkWrites.push({
						'updateMany': {
							'filter': {
								'_id': {
									'$in': acc.ownedBoards
								}
							},
							'update': {
								'$set': {
									'owner': null //board has no owner
								}
							}
						}
					});
					cache.del(acc.ownedBoards.map(b => `board:${b}`));
//todo: use list of board with no owners for claims
				}
			}
			await Boards.db.bulkWrite(bulkWrites);
		}
		amount = await Accounts.deleteMany(req.body.checkedaccounts).then(res => res.deletedCount);
	} else {
		amount = await Accounts.setLevel(req.body.checkedaccounts, req.body.auth_level).then(res => res.modifiedCount);
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': `${req.body.delete_account ? 'Deleted' : 'Edited'} ${amount} accounts`,
		'redirect': '/globalmanage/accounts.html'
	});

}


code:
'use strict';

const { Posts, Bans, Modlogs } = require(__dirname+'/../../db/')
	, { createHash } = require('crypto')
	, Mongo = require(__dirname+'/../../db/db.js')
	, getTripCode = require(__dirname+'/../../helpers/posting/tripcode.js')
	, { prepareMarkdown } = require(__dirname+'/../../helpers/posting/markdown.js')
	, messageHandler = require(__dirname+'/../../helpers/posting/message.js')
	, nameHandler = require(__dirname+'/../../helpers/posting/name.js')
	, { previewReplies, strictFiltering } = require(__dirname+'/../../configs/main.js')
	, cache = require(__dirname+'/../../redis.js')
	, buildQueue = require(__dirname+'/../../queue.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { buildThread } = require(__dirname+'/../../helpers/tasks.js')
	, { remove } = require('fs-extra');

module.exports = async (req, res, next) => {

/*
todo: handle some more situations
- last activity date
- correct bump date when editing thread or last post in a thread
- allow for regular users (OP ONLY) and option for staff to disable in board settings
*/

	const { board, post } = res.locals;

	//filters
	if (res.locals.permLevel > 1) { //global staff bypass filters for edit
		const globalSettings = await cache.get('globalsettings');
		if (globalSettings && globalSettings.filters.length > 0 && globalSettings.filterMode > 0) {
			let hitGlobalFilter = false
				, ban
				, concatContents = `|${req.body.name}|${req.body.message}|${req.body.subject}|${req.body.email}|${res.locals.numFiles > 0 ? req.files.file.map(f => f.name).join('|') : ''}`.toLowerCase()
				, allContents = concatContents;
			if (strictFiltering) {
				allContents += concatContents.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); //removing diacritics
				allContents += concatContents.replace(/[\u200B-\u200D\uFEFF]/g, ''); //removing ZWS
				allContents += concatContents.replace(/[^a-zA-Z0-9.-]+/gm, ''); //removing anything thats not alphamnumeric or . and -
			}
			//global filters
			hitGlobalFilter = globalSettings.filters.some(filter => { return allContents.includes(filter.toLowerCase()) });
			if (hitGlobalFilter) {
				if (globalSettings.filterMode === 1) {
					return dynamicResponse(req, res, 400, 'message', {
						'title': 'Bad request',
						'message': 'Your edit was blocked by a global word filter',
					});
				} else {
					const banDate = new Date();
					const banExpiry = new Date(globalSettings.filterBanDuration + banDate.getTime());
					const ban = {
						'ip': {
							'single': res.locals.ip.single,
							'raw': res.locals.ip.raw,
						},
						'type': 'single',
						'reason': 'global word filter auto ban',
						'board': null,
						'posts': null,
						'issuer': 'system', //what should i call this
						'date': banDate,
						'expireAt': banExpiry,
						'allowAppeal': true, //should i make this configurable if appealable?
						'seen': false
					};
 					const insertedResult = await Bans.insertOne(ban);
					ban._id = insertedResult.insertedId;
					return res.status(403).render('ban', {
						bans: [ban]
					});
				}
			}
		}
	}

	//message hash
	let messageHash = null;
	if (req.body.message && req.body.message.length > 0) {
		const noQuoteMessage = req.body.message.replace(/>>\d+/g, '').replace(/>>>\/\w+(\/\d*)?/gm, '').trim();
		messageHash = createHash('sha256').update(noQuoteMessage).digest('base64');
	}
	//new name, trip and cap
	const { name, tripcode, capcode } = await nameHandler(req.body.name, res.locals.permLevel,
		board.settings, board.owner, res.locals.user ? res.locals.user.username : null);
	//new message and quotes
	const nomarkup = prepareMarkdown(req.body.message, false);
	const { message, quotes, crossquotes } = await messageHandler(nomarkup, req.body.board, post.thread);
	//todo: email and subject (probably dont need any transformation since staff bypass limits on forceanon, and it doesnt have to account for sage/etc

	//intersection/difference of quotes sets for linking and unlinking
	const oldQuoteIds = post.quotes.map(q => q._id.toString());
	const oldQuotesSet = new Set(oldQuoteIds);
	const newQuoteIds = quotes.map(q => q._id.toString());
	const newQuotesSet = new Set(newQuoteIds);

	const addedQuotesSet = new Set(newQuoteIds.filter(qid => !oldQuotesSet.has(qid)).map(Mongo.ObjectId));
	const removedQuotesSet = new Set(oldQuoteIds.filter(qid => !newQuotesSet.has(qid)).map(Mongo.ObjectId));

	//linking new added quotes
	if (addedQuotesSet.size > 0) {
		await Posts.db.updateMany({
			'_id': {
				'$in': [...addedQuotesSet]
			}
		}, {
			'$push': {
				'backlinks': { _id: post._id, postId: post.postId }
			}
		});
	}

	//unlinking removed quotes
	if (removedQuotesSet.size > 0) {
		await Posts.db.updateMany({
			'_id': {
				'$in': [...removedQuotesSet]
			}
		}, {
			'$pull': {
				'backlinks': {
					'postId': post.postId
				}
			}
		});
	}

	//update the post
	const postId = await Posts.db.updateOne({
		board: req.body.board,
		postId: post.postId
	}, {
		'$set': {
			edited: {
				username: req.body.hide_name ? 'Hidden User' : req.session.user,
				date: new Date(),
			},
			nomarkup,
			message,
			'messagehash': messageHash || null,
			quotes,
			crossquotes,
			name,
			tripcode,
			capcode,
			email: req.body.email,
			subject: req.body.subject,
		}
	});

	//add the edit to the modlog
	await Modlogs.insertOne({
		board: board._id,
		showLinks: true,
		postLinks: [{
			postId: post.postId,
			thread: post.thread,
		}],
		actions: 'Edit',
		date: new Date(),
		showUser: req.body.hide_name ? false : true,
		message: req.body.log_message || null,
		user: req.session.user,
		ip: {
			single: res.locals.ip.single,
			raw: res.locals.ip.raw,
		}
	});

	const buildOptions = {
		'threadId': post.thread || post.postId,
		'board': res.locals.board
	};

	//build thread immediately for redirect
	await buildThread(buildOptions);

	dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': 'Post edited successfully',
		'redirect': req.body.referer,
	});
	res.end();

	//rebuild the modlogs
	buildQueue.push({
		'task': 'buildModLog',
		'options': {
			'board': board,
		}
	});
	buildQueue.push({
		'task': 'buildModLogList',
		'options': {
			'board': board,
		}
	});

	//check if post is visible in preview posts
	let postInPreviewPosts = false;
	if (post.thread) {
		const threadPreviewPosts = await Posts.db.find({
			'thread': post.thread,
			'board': board._id
		},{
			'projection': {
				'postId': 1, //only get postId
			}
		}).sort({
			'postId': -1
		}).limit(previewReplies).toArray();
		postInPreviewPosts = threadPreviewPosts.some(p => p.postId <= post.postId)
	}

	if (post.thread === null || postInPreviewPosts) {
		const thread = post.thread === null ? post : (await Posts.getPost(board._id, post.thread));
		const threadPage = await Posts.getThreadPage(board._id, thread);
		//rebuild index page if its a thread or visible in preview posts
		buildQueue.push({
			'task': 'buildBoard',
			'options': {
				'board': res.locals.board,
				'page': threadPage
			}
		});
	}

	if (post.thread === null) {
		//rebuild catalog if its a thread to correct catalog tile
		buildQueue.push({
			'task': 'buildCatalog',
			'options': {
				'board': res.locals.board,
			}
		});
	}

}


code:
'use strict';

const { NumberInt } = require(__dirname+'/../../db/db.js')

module.exports = (posts) => {

	const filteredposts = posts.filter(post => {
		return !post.thread
	})

	if (filteredposts.length === 0) {
		return {
			message: 'No thread(s) to lock',
		};
	}

	return {
		message: `Toggled Lock for ${filteredposts.length} thread(s)`,
		action: '$bit',
		query: {
			'locked': {
				'xor': NumberInt(1)
			},
		}
	};

}


code:
'use strict';

const bcrypt = require('bcrypt')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { Accounts } = require(__dirname+'/../../db/');

module.exports = async (req, res, next) => {

	const username = req.body.username.toLowerCase();
	const password = req.body.password;
	let goto = req.body.goto;
	// we don't want to redirect the user to random sites
	if (goto == null || !/^\/[0-9a-zA-Z][0-9a-zA-Z._/-]*$/.test(goto)) {
		goto = '/account.html';
	}
	const failRedirect = `/login.html${goto ? '?goto='+encodeURIComponent(goto) : ''}`

	//fetch an account
	const account = await Accounts.findOne(username);

	//if the account doesnt exist, reject
	if (!account) {
		return dynamicResponse(req, res, 403, 'message', {
			'title': 'Forbidden',
			'message': 'Incorrect username or password',
			'redirect': failRedirect
		});
	}

	// bcrypt compare input to saved hash
	const passwordMatch = await bcrypt.compare(password, account.passwordHash);

	//if hashes matched
	if (passwordMatch === true) {

		// add the account to the session and authenticate if password was correct
		req.session.user = account._id;

		//successful login
		await Accounts.updateLastActiveDate(username);
		return res.redirect(goto);

	}

	return dynamicResponse(req, res, 403, 'message', {
		'title': 'Forbidden',
		'message': 'Incorrect username or password',
		'redirect': failRedirect
	});

}


code:
'use strict';

module.exports = (req, res, next) => {

	//remove session
	req.session.destroy();
	return res.redirect('/');

}