code:
'use strict';

const addNews = require(__dirname+'/../../models/forms/addnews.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js');

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

	const errors = [];

	if (!req.body.message || res.locals.messageLength === 0) {
		errors.push('Missing message');
	}
	if (res.locals.messageLength > 10000) {
		errors.push('Message must be 10000 characters or less');
	}
	if (!req.body.title || req.body.title.length === 0) {
		errors.push('Missing title');
	}
	if (req.body.title.length > 50) {
		errors.push('Title must be 50 characters or less');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/globalmanage/news.html'
		});
	}

	try {
		await addNews(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const appealBans = require(__dirname+'/../../models/forms/appeal.js')
	, { globalLimits } = require(__dirname+'/../../configs/main.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { Bans } = require(__dirname+'/../../db');

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

	const errors = [];
	if (!req.body.checkedbans || req.body.checkedbans.length === 0 || req.body.checkedbans.length > 10) {
		errors.push('Must select 1-10 bans');
	}
	if (!req.body.message || res.locals.messageLength === 0) {
		errors.push('Appeals must include a message');
	}
	if (res.locals.messageLength > globalLimits.fieldLength.message) {
		errors.push('Appeal message must be 2000 characters or less');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/'
		});
	}

	let amount = 0;
	try {
		amount = await appealBans(req, res, next);
	} catch (err) {
		return next(err);
	}

	if (amount === 0) {
		/*
			this can occur if they selected invalid id, non-ip match, already appealed, or unappealable bans. prevented by databse filter, so we use
			use the updatedCount return value to check if any appeals were made successfully. if not, we end up here.
		*/
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'error': 'Invalid bans selected',
			'redirect': '/'
		});
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': `Appealed ${amount} bans successfully`,
		'redirect': '/'
	});

}

repo_name: dolphin/jschan

license: mit


code:
'use strict';

const changeBoardSettings = require(__dirname+'/../../models/forms/changeboardsettings.js')
	, { themes, codeThemes } = require(__dirname+'/../../helpers/themes.js')
	, { Ratelimits } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { globalLimits, rateLimitCost } = require(__dirname+'/../../configs/main.js');

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

	const errors = [];

//TODO: add helpers for different checks, passing name, min/max and return true with error if hit
	if (req.body.description &&
		(req.body.description.length < 1 ||
		 req.body.description.length > globalLimits.fieldLength.description)) {
		errors.push(`Board description must be 1-${globalLimits.fieldLength.description} characters`);
	}
	if (req.body.announcements && (req.body.announcements.length < 1 || req.body.announcements.length > 2000)) {
		errors.push('Board announcements must be 1-2000 characters');
	}
	if (req.body.tags && req.body.tags.length > 2000) {
		errors.push('Tags length must be 2000 characters or less');
	}
	if (req.body.filters && req.body.filters.length > 2000) {
		errors.push('Filters length must be 2000 characters or less');
	}
	if (req.body.custom_css && globalLimits.customCss.enabled) {
		if (res.locals.permLevel > 1 && globalLimits.customCss.strict && globalLimits.customCss.filters.some(filter => req.body.custom_css.includes(filter))) {
			errors.push(`Custom CSS strict mode is enabled and does not allow the following: "${globalLimits.customCss.filters.join('", "')}"`);
		}
		if (req.body.custom_css.length > globalLimits.customCss.max) {
			errors.push(`Custom CSS must be ${globalLimits.customCss.max} characters or less`);
		}
	}
	if (req.body.moderators && req.body.moderators.length > 500) {
		errors.push('Moderators length must be 500 characters orless');
	}
	if (req.body.name &&
		(req.body.name.length < 1 ||
		 req.body.name.length > globalLimits.fieldLength.boardname)) {
		errors.push(`Board name must be 1-${globalLimits.fieldLength.boardname} characters`);
	}
	if (req.body.default_name && (req.body.default_name.length < 1 || req.body.default_name.length > 50)) {
		errors.push('Anon name must be 1-50 characters');
	}
	if (typeof req.body.reply_limit === 'number'
		&& (req.body.reply_limit < globalLimits.replyLimit.min
			|| req.body.reply_limit > globalLimits.replyLimit.max)) {
		errors.push(`Reply Limit must be ${globalLimits.replyLimit.min}-${globalLimits.replyLimit.max}`);
	}
	if (typeof req.body.bump_limit === 'number'
		&& (req.body.bump_limit < globalLimits.bumpLimit.min
			|| req.body.bump_limit > globalLimits.bumpLimit.max)) {
		errors.push(`Bump Limit must be ${globalLimits.bumpLimit.min}-${globalLimits.bumpLimit.max}`);
	}
	if (typeof req.body.thread_limit === 'number'
		&& (req.body.thread_limit < globalLimits.threadLimit.min
			|| req.body.thread_limit > globalLimits.threadLimit.max)) {
		errors.push(`Threads Limit must be ${globalLimits.threadLimit.min}-${globalLimits.threadLimit.max}`);
	}
	if (typeof req.body.max_files === 'number' && (req.body.max_files < 0 || req.body.max_files > globalLimits.postFiles.max)) {
		errors.push(`Max files must be 0-${globalLimits.postFiles.max}`);
	}

	//make sure new min/max message dont conflict
	if (typeof req.body.min_thread_message_length === 'number'
		&& typeof req.body.max_thread_message_length === 'number'
		&& req.body.min_thread_message_length
		&& req.body.max_thread_message_length
		&& req.body.min_thread_message_length > req.body.max_thread_message_length) {
		errors.push('Min and max thread message lengths must not violate eachother');
	}
	if (typeof req.body.min_reply_message_length === 'number'
		&& typeof req.body.max_reply_message_length === 'number'
		&& req.body.min_reply_message_length > req.body.max_reply_message_length) {
		errors.push('Min and max reply message lengths must not violate eachother');
	}

	//make sure existing min/max message dont conflict
	const minThread = Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxThreadMessageLength) || globalLimits.fieldLength.message;
	if (typeof req.body.min_thread_message_length === 'number'
		&& (req.body.min_thread_message_length < 0
			|| req.body.min_thread_message_length > minThread)) {
		errors.push(`Min thread message length must be 0-${globalLimits.fieldLength.message} and not more than "Max Thread Message Length" (currently ${res.locals.board.settings.maxThreadMessageLength})`);
	}
	const minReply = Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxReplyMessageLength) || globalLimits.fieldLength.message;
	if (typeof req.body.min_reply_message_length === 'number'
		&& (req.body.min_reply_message_length < 0
			|| req.body.min_reply_message_length > minReply)) {
		errors.push(`Min reply message length must be 0-${globalLimits.fieldLength.message} and not more than "Max Reply Message Length" (currently ${res.locals.board.settings.maxReplyMessageLength})`);
	}
	if (typeof req.body.max_thread_message_length === 'number'
		&& (req.body.max_thread_message_length < 0
			|| req.body.max_thread_message_length > globalLimits.fieldLength.message
			|| (req.body.max_thread_message_length
				&& req.body.max_thread_message_length < res.locals.board.settings.minThreadMessageLength))) {
		errors.push(`Max thread message length must be 0-${globalLimits.fieldLength.message} and not less than "Min Thread Message Length" (currently ${res.locals.board.settings.minThreadMessageLength})`);
	}
	if (typeof req.body.max_reply_message_length === 'number'
		&& (req.body.max_reply_message_length < 0
			|| req.body.max_reply_message_length > globalLimits.fieldLength.message
			|| (req.body.max_reply_message_length
				&& req.body.max_reply_message_length < res.locals.board.settings.minReplyMessageLength))) {
		errors.push(`Max reply message length must be 0-${globalLimits.fieldLength.message} and not less than "Min Reply Message Length" (currently ${res.locals.board.settings.minReplyMessageLength})`);
	}

	if (typeof req.body.lock_mode === 'number' && (req.body.lock_mode < 0 || req.body.lock_mode > 2)) {
		errors.push('Invalid lock mode');
	}
	if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) {
		errors.push('Invalid captcha mode');
	}
	if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) {
		errors.push('Invalid filter mode');
	}
	if (typeof req.body.ban_duration === 'number' && req.body.ban_duration < 0) {
		errors.push('Invalid filter auto ban duration');
	}
	if (req.body.theme && !themes.includes(req.body.theme)) {
		errors.push('Invalid theme');
	}
	if (req.body.code_theme && !codeThemes.includes(req.body.code_theme)) {
		errors.push('Invalid code theme');
	}

	if (typeof req.body.tph_trigger === 'number' && (req.body.tph_trigger < 0 || req.body.tph_trigger > 10000)) {
		errors.push('Invalid tph trigger threshold');
	}
	if (typeof req.body.tph_trigger_action === 'number' && (req.body.tph_trigger_action < 0 || req.body.tph_trigger_action > 4)) {
		errors.push('Invalid tph trigger action');
	}
	if (typeof req.body.pph_trigger === 'number' && (req.body.pph_trigger < 0 || req.body.pph_trigger > 10000)) {
		errors.push('Invalid pph trigger threshold');
	}
	if (typeof req.body.pph_trigger_action === 'number' && (req.body.pph_trigger_action < 0 || req.body.pph_trigger_action > 4)) {
		errors.push('Invalid pph trigger action');
	}
	if (typeof req.body.lock_reset === 'number' && (req.body.lock_reset < 0 || req.body.lock_reset > 2)) {
		errors.push('Invalid trigger reset lock');
	}
	if (typeof req.body.captcha_reset === 'number' && (req.body.captcha_reset < 0 || req.body.captcha_reset > 2)) {
		errors.push('Invalid trigger reset captcha');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': `/${req.params.board}/manage/settings.html`
		});
	}

	if (res.locals.permLevel > 1) { //if not global staff or above
		const ratelimitBoard = await Ratelimits.incrmentQuota(req.params.board, 'settings', rateLimitCost.boardSettings); //2 changes a minute
//		const ratelimitIp = await Ratelimits.incrmentQuota(res.locals.ip.single, 'settings', rateLimitCost.boardSettings);
		if (ratelimitBoard > 100 /* || ratelimitIp > 100 */) {
			return dynamicResponse(req, res, 429, 'message', {
				'title': 'Ratelimited',
				'error': 'You are changing settings too quickly, please wait a minute and try again',
				'redirect': `/${req.params.board}/manage/settings.html`
			});
		}
	}

	try {
		await changeBoardSettings(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const changePassword = require(__dirname+'/../../models/forms/changepassword.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js');

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

	const errors = [];

	//check exist
	if (!req.body.username || req.body.username.length <= 0) {
		errors.push('Missing username');
	}
	if (!req.body.password || req.body.password.length <= 0) {
		errors.push('Missing password');
	}
	if (!req.body.newpassword || req.body.newpassword.length <= 0) {
		errors.push('Missing new password');
	}
	if (!req.body.newpasswordconfirm || req.body.newpasswordconfirm.length <= 0) {
		errors.push('Missing new password confirmation');
	}

	//check too long
	if (req.body.username && req.body.username.length > 50) {
		errors.push('Username must be 50 characters or less');
	}
	if (req.body.password && req.body.password.length > 100) {
		errors.push('Password must be 100 characters or less');
	}
	if (req.body.newpassword && req.body.newpassword.length > 100) {
		errors.push('Password must be 100 characters or less');
	}
	if (req.body.newpasswordconfirm && req.body.newpasswordconfirm.length > 100) {
		errors.push('Password confirmation must be 100 characters or less');
	}
	if (req.body.newpassword != req.body.newpasswordconfirm) {
		errors.push('New password and password confirmation must match');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/changepassword.html'
		})
	}

	try {
		await changePassword(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const createBoard = require(__dirname+'/../../models/forms/create.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { enableUserBoardCreation, globalLimits } = require(__dirname+'/../../configs/main.js')
	, alphaNumericRegex = require(__dirname+'/../../helpers/checks/alphanumregex.js')

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

	if (enableUserBoardCreation === false && res.locals.permLevel > 1) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'error': 'User board creation is currently disabled',
			'redirect': '/create.html'
		});
	}

	const errors = [];

	//check exist
	if (!req.body.uri || req.body.uri.length <= 0) {
		errors.push('Missing URI');
	}
	if (!req.body.name || req.body.name.length <= 0) {
		errors.push('Missing name');
	}
	if (!req.body.description || req.body.description.length <= 0) {
		errors.push('Missing description');
	}

	//other validation
	if (req.body.uri) {
		if (req.body.uri.length > globalLimits.fieldLength.uri) {
			errors.push(`URI must be ${globalLimits.fieldLength.uri} characters or less`);
		}
		if (alphaNumericRegex.test(req.body.uri) !== true) {
			errors.push('URI must contain a-z 0-9 only');
		}
	}
	if (req.body.name && req.body.name.length > globalLimits.fieldLength.boardname) {
		errors.push(`Name must be ${globalLimits.fieldLength.boardname} characters or less`);
	}
	if (req.body.description && req.body.description.length > globalLimits.fieldLength.description) {
		errors.push(`Description must be ${globalLimits.fieldLength.description} characters or less`);
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/create.html'
		});
	}

	try {
		await createBoard(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const deleteAccount = require(__dirname+'/../../models/forms/deleteaccount.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')

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

	if (!req.body.confirm) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'error': 'Missing confirmation',
			'redirect': '/account.html',
		});
	}

	const { modBoards, ownedBoards } = res.locals.user;
	if (ownedBoards.length > 0 || modBoards.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'message': 'You cannot delete your account while you hold staff position on any board',
			'redirect': '/account.html',
		});
	}

	try {
		await deleteAccount(res.locals.user.username);
	} catch (err) {
		return next(err);
	}

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

}


code:
'use strict';

const deleteBanners = require(__dirname+'/../../models/forms/deletebanners.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js');

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

	const errors = [];

	if (!req.body.checkedbanners || req.body.checkedbanners.length === 0) {
		errors.push('Must select at least one banner to delete');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': `/${req.params.board}/manage/banners.html`
		})
	}

	for (let i = 0; i < req.body.checkedbanners.length; i++) {
		if (!res.locals.board.banners.includes(req.body.checkedbanners[i])) {
			return dynamicResponse(req, res, 400, 'message', {
				'title': 'Bad request',
				'message': 'Invalid banners selected',
				'redirect': `/${req.params.board}/manage/banners.html`
			})
		}
	}

	try {
		await deleteBanners(req, res, next);
	} catch (err) {
		console.error(err);
		return next(err);
	}

}


code:
'use strict';

const { Boards } = require(__dirname+'/../../db/')
	, deleteBoard = require(__dirname+'/../../models/forms/deleteboard.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, alphaNumericRegex = require(__dirname+'/../../helpers/checks/alphanumregex.js')

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

	const errors = [];

	if (!req.body.confirm) {
		errors.push('Missing confirmation');
	}
	if (!req.body.uri) {
		errors.push('Missing URI');
	}
	let board;
	if (alphaNumericRegex.test(req.body.uri) !== true) {
		errors.push('URI must contain a-z 0-9 only');
	} else {
		//no need to check these if the board name is completely invalid
		if (req.params.board != null && req.params.board !== req.body.uri) {
			//board manage page to not be able to delete other boards;
			//req.params.board will be null on global delete, so this wont happen
			errors.push('URI does not match current board');
		}
		try {
			board = await Boards.findOne(req.body.uri)
		} catch (err) {
			return next(err);
		}
		if (!board) {
			//global must check exists because the route skips Boards.exists middleware
			errors.push(`Board /${req.body.uri}/ does not exist`);
		}
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': req.params.board ? `/${req.params.board}/manage/settings.html` : '/globalmanage/settings.html'
		});
	}

	try {
		await deleteBoard(board._id, board);
	} catch (err) {
		return next(err);
	}

	return dynamicResponse(req, res, 200, 'message', {
		'title': 'Success',
		'message': 'Board deleted',
		'redirect': req.params.board ? '/' : '/globalmanage/settings.html'
	});

}


code:
'use strict';

const deleteNews = require(__dirname+'/../../models/forms/deletenews.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js');

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

	const errors = [];

	if (!req.body.checkednews || req.body.checkednews.length === 0) {
		errors.push('Must select at least one newspost to delete');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/globalmanage/news.html'
		})
	}

	try {
		await deleteNews(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const editAccounts = require(__dirname+'/../../models/forms/editaccounts.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js');

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

	const errors = [];

	if (!req.body.checkedaccounts || req.body.checkedaccounts.length === 0) {
		errors.push('Must select at least one account');
	}
	if (typeof req.body.auth_level !== 'number' && !req.body.delete_account) {
		errors.push('Missing auth level or delete action');
	}
	if (typeof req.body.auth_level === 'number' && req.body.auth_level < 0 || req.body.auth_level > 4) {
		errors.push('Auth level must be 0-4');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/globalmanage/accounts.html'
		})
	}

	try {
		await editAccounts(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const removeBans = require(__dirname+'/../../models/forms/removebans.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, denyAppeals = require(__dirname+'/../../models/forms/denybanappeals.js');

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

	const errors = [];

	if (!req.body.checkedbans || req.body.checkedbans.length === 0) {
		errors.push('Must select at least one ban');
	}
	if (!req.body.option || (req.body.option !== 'unban' && req.body.option !== 'deny_appeal')) {
		errors.push('Invalid ban action')
	}

	const redirect = req.params.board ? `/${req.params.board}/manage/bans.html` : '/globalmanage/bans.html';

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			redirect
		});
	}

	let amount = 0;
	let message;
	try {
		if (req.body.option === 'unban') {
			amount = await removeBans(req, res, next);
			message = `Removed ${amount} bans`;
		} else {
			amount = await denyAppeals(req, res, next);
			message = `Denied ${amount} appeals`;
		}
	} catch (err) {
		return next(err);
	}

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

}


code:
'use strict';

const editPost = require(__dirname+'/../../models/forms/editpost.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { rateLimitCost, globalLimits } = require(__dirname+'/../../configs/main.js')
	, { Ratelimits, Posts, Boards } = require(__dirname+'/../../db/');

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

	const errors = [];

	if ((!req.body.board || req.body.board.length === 0)
		|| (!req.body.postId || typeof req.body.postId !== 'number')) {
		errors.push('Missing board and postId form data');
	}
	// message, subject, email, name, limited length
	if (req.body.message && res.locals.messageLength > globalLimits.fieldLength.message) {
		errors.push(`Message must be ${globalLimits.fieldLength.message} characters or less`);
	}
	if (req.body.name && req.body.name.length > globalLimits.fieldLength.name) {
		errors.push(`Name must be ${globalLimits.fieldLength.name} characters or less`);
	}
	if (req.body.subject && req.body.subject.length > globalLimits.fieldLength.subject) {
		errors.push(`Subject must be ${globalLimits.fieldLength.subject} characters or less`);
	}
	if (req.body.email && req.body.email.length > globalLimits.fieldLength.email) {
		errors.push(`Email must be ${globalLimits.fieldLength.email} characters or less`);
	}
	if (req.body.log_message && req.body.log_message.length > globalLimits.fieldLength.log_message) {
		errors.push(`Modlog message must be ${globalLimits.fieldLength.log_message} characters or less`);
	}

	try {
		res.locals.post = await Posts.getPost(req.body.board, req.body.postId);
	} catch (err) {
		return next(err);
	}

	if (!res.locals.board || !res.locals.post) {
		errors.push(`Post doesn't exist`);
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
		});
	}

	if (res.locals.permLevel > 1) { //if not global staff or above
		const ratelimitUser = await Ratelimits.incrmentQuota(req.session.user, 'edit', rateLimitCost.editPost);
//		const ratelimitIp = await Ratelimits.incrmentQuota(res.locals.ip.single, 'edit', rateLimitCost.editPost);
		if (ratelimitUser > 100 /* || ratelimitIp > 100 */) {
			return dynamicResponse(req, res, 429, 'message', {
				'title': 'Ratelimited',
				'error': 'You are editing posts too quickly, please wait a minute and try again',
			});
		}
	}

	try {
		await editPost(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const { Posts } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { globalLimits } = require(__dirname+'/../../configs/main.js')
	, actionHandler = require(__dirname+'/../../models/forms/actionhandler.js')
	, actionChecker = require(__dirname+'/../../helpers/checks/actionchecker.js');

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

	const errors = [];

	if (!req.body.globalcheckedposts || req.body.globalcheckedposts.length === 0) {
		errors.push(`Must select at least one post`);
	} else if (globalLimits.multiInputs.posts.staff
		&& req.body.globalcheckedposts.length > globalLimits.multiInputs.posts.staff) {
		errors.push(`Must not select >${globalLimits.multiInputs.posts.staff} posts per request`);
	}

	//checked reports
	if (req.body.checkedreports) {
		if (!req.body.global_report_ban) {
			errors.push('Must select a report action if checked reports');
		}
		if (req.body.checkedreports.length > req.body.globalcheckedposts.length*5) {
			//5 reports max per post
			errors.push('Invalid number of reports checked');
		}
	} else if (!req.body.checkedreports && req.body.global_report_ban) {
		errors.push('Must select posts+reports to report ban');
	}

	res.locals.actions = actionChecker(req);

	//make sure they have any global actions, and that they only selected global actions
	if (res.locals.actions.numGlobal === 0 || res.locals.actions.validActions.length > res.locals.actions.numGlobal) {
		errors.push('Invalid actions selected');
	}

	//check that actions are valid
	if (req.body.edit && req.body.globalcheckedposts.length > 1) {
		errors.push('Must select only 1 post for edit action');
	}
	if (req.body.postpassword && req.body.postpassword.length > globalLimits.fieldLength.postpassword) {
		errors.push(`Password must be ${globalLimits.fieldLength.postpassword} characters or less`);
	}
	if (req.body.ban_reason && req.body.ban_reason.length > globalLimits.fieldLength.ban_reason) {
		errors.push(`Ban reason must be ${globalLimits.fieldLength.ban_reason} characters or less`);
	}
	if (req.body.log_message && req.body.log_message.length > globalLimits.fieldLength.log_message) {
		errors.push(`Modlog message must be ${globalLimits.fieldLength.log_message} characters or less`);
	}

	//return the errors
	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/globalmanage/reports.html'
		})
	}

	//get posts with global ids only
	try {
		res.locals.posts = await Posts.globalGetPosts(req.body.globalcheckedposts, true);
	} catch (err) {
		return next(err);
	}
	if (!res.locals.posts || res.locals.posts.length === 0) {
		return dynamicResponse(req, res, 404, 'message', {
			'title': 'Not found',
			'errors': 'Selected posts not found',
			'redirect': '/globalmanage/reports.html'
		})
	}

	if (req.body.edit) {
		//edit post, only allowing one
		return res.render('editpost', {
			'post': res.locals.posts[0],
			'csrf': req.csrfToken(),
			'referer': (req.headers.referer || `/${res.locals.posts[0].board}/manage/thread/${res.locals.posts[0].thread || res.locals.posts[0].postId}.html`) + `#${res.locals.posts[0].postId}`,
		});
	}

	try {
		await actionHandler(req, res, next);
	} catch (err) {
		console.error(err);
		return next(err);
	}

}


code:
'use strict';

const changeGlobalSettings = require(__dirname+'/../../models/forms/changeglobalsettings.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js');

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

	const errors = [];

	if (req.body.filters && req.body.filters.length > 2000) {
		errors.push('Filters length must be 2000 characters or less');
	}
/*
	if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) {
		errors.push('Invalid captcha mode.');
	}
*/
	if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) {
		errors.push('Invalid filter mode.');
	}
	if (typeof req.body.ban_duration === 'number' && req.body.ban_duration < 0) {
		errors.push('Invalid filter auto ban duration.');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/globalmanage/settings.html'
		});
	}

	try {
		await changeGlobalSettings(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const loginAccount = require(__dirname+'/../../models/forms/login.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js');

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

	const errors = [];

	//check exist
	if (!req.body.username || req.body.username.length <= 0) {
		errors.push('Missing username');
	}
	if (!req.body.password || req.body.password.length <= 0) {
		errors.push('Missing password');
	}

	//check too long
	if (req.body.username && req.body.username.length > 50) {
		errors.push('Username must be 50 characters or less');
	}
	if (req.body.password && req.body.password.length > 100) {
		errors.push('Password must be 100 characters or less');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/login.html'
		})
	}

	try {
		await loginAccount(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const makePost = require(__dirname+'/../../models/forms/makepost.js')
	, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { globalLimits, disableOnionFilePosting } = require(__dirname+'/../../configs/main.js')
	, { Files } = require(__dirname+'/../../db/');

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

	const errors = [];

	// even if force file and message are off, the post must contain one of either.
	if ((!req.body.message || res.locals.messageLength === 0) && res.locals.numFiles === 0) {
		errors.push('Posts must include a message or file');
	}
	if (res.locals.tor
		&& (disableOnionFilePosting || res.locals.board.settings.disableOnionFilePosting)
		&& res.locals.numFiles > 0) {
		errors.push(`Posting files through the .onion address has been disabled ${disableOnionFilePosting ? 'globally' : 'on this board'}`);
	}
	if (res.locals.numFiles > res.locals.board.settings.maxFiles) {
		errors.push(`Too many files. Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is ${res.locals.board.settings.maxFiles}`);
	}
	// check file, subject and message enforcement according to board settings
	if (!req.body.subject || req.body.subject.length === 0) {
		if (!req.body.thread && res.locals.board.settings.forceThreadSubject) {
			errors.push('Threads must include a subject');
		} //no option to force op subject, seems useless
	}
	if (globalLimits.postFiles.max !== 0 && res.locals.board.settings.maxFiles !== 0 && res.locals.numFiles === 0) {
		if (!req.body.thread && res.locals.board.settings.forceThreadFile) {
			errors.push('Threads must include a file');
		} else if (req.body.thread && res.locals.board.settings.forceReplyFile) {
			errors.push('Posts must include a file');
		}
	}
	if (!req.body.message || res.locals.messageLength === 0) {
		if (!req.body.thread && res.locals.board.settings.forceThreadMessage) {
			errors.push('Threads must include a message');
		} else if (req.body.therad && res.locals.board.settings.forceReplyMessage) {
			errors.push('Posts must include a message');
		}
	}
	if (req.body.message) {
		if (res.locals.messageLength > globalLimits.fieldLength.message) {
			errors.push(`Message must be ${globalLimits.fieldLength.message} characters or less`);
		} else if (!req.body.thread
			&& res.locals.board.settings.maxThreadMessageLength
			&& res.locals.messageLength > res.locals.board.settings.maxThreadMessageLength) {
			errors.push(`Thread messages must be ${res.locals.board.settings.maxThreadLength} characters or less`);
		} else if (req.body.thread
			&& res.locals.board.settings.maxReplyMessageLength
			&& res.locals.messageLength > res.locals.board.settings.maxReplyMessageLength) {
			errors.push(`Reply messages must be ${res.locals.board.settings.maxReplyMessageLength} characters or less`);
		} else if (!req.body.thread && res.locals.messageLength < res.locals.board.settings.minThreadMessageLength) {
			errors.push(`Thread messages must be at least ${res.locals.board.settings.minThreadMessageLength} characters long`);
		} else if (req.body.thread && res.locals.messageLength < res.locals.board.settings.minReplyMessageLength) {
			errors.push(`Reply messages must be at least ${res.locals.board.settings.minReplyMessageLength} characters long`);
		}
	}

	// subject, email, name, password limited length
	if (req.body.postpassword && req.body.postpassword.length > globalLimits.fieldLength.postpassword) {
		errors.push(`Password must be ${globalLimits.fieldLength.postpassword} characters or less`);
	}
	if (req.body.name && req.body.name.length > globalLimits.fieldLength.name) {
		errors.push(`Name must be ${globalLimits.fieldLength.name} characters or less`);
	}
	if (req.body.subject && req.body.subject.length > globalLimits.fieldLength.subject) {
		errors.push(`Subject must be ${globalLimits.fieldLength.subject} characters or less`);
	}
	if (req.body.email && req.body.email.length > globalLimits.fieldLength.email) {
		errors.push(`Email must be ${globalLimits.fieldLength.email} characters or less`);
	}

	if (errors.length > 0) {
		await deleteTempFiles(req).catch(e => console.error);
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': `/${req.params.board}${req.body.thread ? '/thread/' + req.body.thread + '.html' : ''}`
		});
	}

	try {
		await makePost(req, res, next);
	} catch (err) {
		await deleteTempFiles(req).catch(e => console.error);
		if (res.locals.numFiles > 0) {
			await Files.decrement(req.files.file.filter(x => x.inced === true && x.filename != null).map(x => x.filename)).catch(e => console.error);
		}
		return next(err);
	}

}


code:
'use strict';

const alphaNumericRegex = require(__dirname+'/../../helpers/checks/alphanumregex.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, { enableUserAccountCreation } = require(__dirname+'/../../configs/main.js')
	, registerAccount = require(__dirname+'/../../models/forms/register.js');

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

	if (enableUserAccountCreation === false && res.locals.permLevel > 1) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'error': 'Acount creation is disabled',
			'redirect': '/register.html'
		});
	}

	const errors = [];

	//check exist
	if (!req.body.username || req.body.username.length <= 0) {
		errors.push('Missing username');
	}
	if (!req.body.password || req.body.password.length <= 0) {
		errors.push('Missing password');
	}
	if (!req.body.passwordconfirm || req.body.passwordconfirm.length <= 0) {
		errors.push('Missing password confirmation');
	}

	//check
	if (req.body.username) {
		if (req.body.username.length > 50) {
			errors.push('Username must be 50 characters or less');
		}
		if (alphaNumericRegex.test(req.body.username) !== true) {
			errors.push('Username must contain a-z 0-9 only');
		}
	}
	if (req.body.password && req.body.password.length > 100) {
		errors.push('Password must be 100 characters or less');
	}
	if (req.body.passwordconfirm && req.body.passwordconfirm.length > 100) {
		errors.push('Password confirmation must be 100 characters or less');
	}
	if (req.body.password != req.body.passwordconfirm) {
		errors.push('Password and password confirmation must match');
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': '/register.html'
		})
	}

	try {
		await registerAccount(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const { Boards } = require(__dirname+'/../../db/')
	, resignFromBoard = require(__dirname+'/../../models/forms/resign.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, alphaNumericRegex = require(__dirname+'/../../helpers/checks/alphanumregex.js');

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

	const errors = [];

	if (!req.body.confirm) {
		errors.push('Missing confirmation');
	}
	if (!req.body.board || req.body.board.length === 0) {
		errors.push('You did not select a board');
	} else if (alphaNumericRegex.test(req.body.board) !== true) {
		errors.push('URI must contain a-z 0-9 only');
	} else {
		try {
			res.locals.board = await Boards.findOne(req.body.board);
		} catch (err) {
			return next(err);
		}
		if (!res.locals.board) {
			errors.push(`Board /${req.body.board}/ does not exist`);
		}
	}

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': `/account.html`
		})
	}

	try {
		await resignFromBoard(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const transferBoard = require(__dirname+'/../../models/forms/transferboard.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, alphaNumericRegex = require(__dirname+'/../../helpers/checks/alphanumregex.js');

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

	const errors = [];

	if (!req.body.username || req.body.username.length === 0) {
		errors.push('Missing transfer username');
	}
	if (req.body.username && req.body.username.length > 50) {
		errors.push('Transfer username must be 50 characters or less');
	}
	if (req.body.username === res.locals.board.owner) {
		errors.push('New owner username must not be same as old owner');
	}
	if (alphaNumericRegex.test(req.body.username) !== true) {
        errors.push('Username must contain a-z 0-9 only');
    }

	if (errors.length > 0) {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': `/${req.params.board}/manage/settings.html`
		});
	}

	try {
		await transferBoard(req, res, next);
	} catch (err) {
		return next(err);
	}

}


code:
'use strict';

const uploadBanners = require(__dirname+'/../../models/forms/uploadbanners.js')
	, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
	, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
	, { globalLimits } = require(__dirname+'/../../configs/main.js');

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

	const errors = [];

	if (res.locals.numFiles === 0) {
		errors.push('Must provide a file');
	} else if (res.locals.numFiles > globalLimits.bannerFiles.max) {
		errors.push(`Exceeded max banner uploads in one request of ${globalLimits.bannerFiles.max}`);
	} else if (res.locals.board.banners.length+res.locals.numFiles > globalLimits.bannerFiles.total) {
		errors.push(`Total number of banners would exceed global limit of ${globalLimits.bannerFiles.total}`);
	}

	if (errors.length > 0) {
		await deleteTempFiles(req).catch(e => console.error);
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Bad request',
			'errors': errors,
			'redirect': `/${req.params.board}/manage/banners.html`
		})
	}

	try {
		await uploadBanners(req, res, next);
	} catch (err) {
		await deleteTempFiles(req).catch(e => console.error);
		return next(err);
	}

}


code:
'use strict';

const express  = require('express')
	, router = express.Router()
	, Boards = require(__dirname+'/../db/boards.js')
	, Posts = require(__dirname+'/../db/posts.js')
	, { captchaOptions } = require(__dirname+'/../configs/main.js')
	//middlewares
	, processIp = require(__dirname+'/../helpers/processip.js')
	, geoAndTor = require(__dirname+'/../helpers/geoip.js')
	, calcPerms = require(__dirname+'/../helpers/checks/calcpermsmiddleware.js')
	, hasPerms = require(__dirname+'/../helpers/checks/haspermsmiddleware.js')
	, isLoggedIn = require(__dirname+'/../helpers/checks/isloggedin.js')
	, paramConverter = require(__dirname+'/../helpers/paramconverter.js')
	, useSession = require(__dirname+'/../helpers/usesession.js')
	, sessionRefresh = require(__dirname+'/../helpers/sessionrefresh.js')
	, csrf = require(__dirname+'/../helpers/checks/csrfmiddleware.js')
	, setMinimal = require(__dirname+'/../helpers/setminimal.js')
	//page models
	, { manageRecent, manageReports, manageBanners, manageSettings, manageBans,
		manageBoard, manageThread, manageLogs, manageCatalog } = require(__dirname+'/../models/pages/manage/')
	, { globalManageSettings, globalManageReports, globalManageBans, globalManageBoards,
		globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/')
	, { changePassword, blockBypass, home, register, login, create,
		board, catalog, banners, randombanner, news, captchaPage, overboard,
		captcha, thread, modlog, modloglist, account, boardlist } = require(__dirname+'/../models/pages/');

//homepage
router.get('/index.html', home);

//news page
router.get('/news.html', news);

//board list
router.get('/boards.(html|json)', useSession, sessionRefresh, calcPerms, boardlist);

//board pages
router.get('/:board/:page(1[0-9]{1,}|[2-9][0-9]{0,}|index).(html|json)', Boards.exists, paramConverter, board); //index
router.get('/:board/thread/:id([1-9][0-9]{0,}).(html|json)', Boards.exists, paramConverter, Posts.exists, thread); //thread view
router.get('/:board/catalog.(html|json)', Boards.exists, catalog); //catalog
router.get('/:board/logs.html', Boards.exists, modloglist);//modlog list
router.get('/:board/logs/:date(\\d{2}-\\d{2}-\\d{4}).html', Boards.exists, paramConverter, modlog); //daily log
router.get('/:board/banners.html', Boards.exists, banners); //banners
router.get('/all.html', overboard); //overboard
router.get('/create.html', useSession, sessionRefresh, isLoggedIn, create); //create new board
router.get('/randombanner', randombanner); //random banner

//board manage pages
router.get('/:board/manage/reports.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageReports);
router.get('/:board/manage/recent.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageRecent);
router.get('/:board/manage/bans.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageBans);
router.get('/:board/manage/logs.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageLogs);
router.get('/:board/manage/settings.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, manageSettings);
router.get('/:board/manage/banners.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(2), csrf, manageBanners);
// if (mod view enabled) {
router.get('/:board/manage/catalog.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms, hasPerms(3), csrf, manageCatalog);
router.get('/:board/manage/:page(1[0-9]{1,}|[2-9][0-9]{0,}|index).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(3), csrf, manageBoard);
router.get('/:board/manage/thread/:id([1-9][0-9]{0,}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, paramConverter, calcPerms, hasPerms(3), csrf, Posts.exists, manageThread);

//global manage pages
router.get('/globalmanage/reports.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), csrf, globalManageReports);
router.get('/globalmanage/bans.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), csrf, globalManageBans);
router.get('/globalmanage/recent.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), csrf, globalManageRecent);
router.get('/globalmanage/boards.(html|json)', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), globalManageBoards);
router.get('/globalmanage/globallogs.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(1), csrf, globalManageLogs);
router.get('/globalmanage/news.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageNews);
router.get('/globalmanage/accounts.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageAccounts);
router.get('/globalmanage/settings.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageSettings);

//captcha
if (captchaOptions.type !== 'google') {
	router.get('/captcha', geoAndTor, processIp, captcha); //get captcha image and cookie
}
router.get('/captcha.html', captchaPage); //iframed for noscript users
router.get('/bypass.html', blockBypass); //block bypass page
router.get('/bypass_minimal.html', setMinimal, blockBypass); //block bypass page

//accounts
router.get('/account.html', useSession, sessionRefresh, isLoggedIn, csrf, account); //page showing boards you are mod/owner of, links to password rese, logout, etc
router.get('/login.html', login);
router.get('/register.html', register);
router.get('/changepassword.html', changePassword);

module.exports = router;


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, db = Mongo.db.collection('accounts')
	, bcrypt = require('bcrypt')
	, cache = require(__dirname+'/../redis.js');

module.exports = {

	db,

	countUsers: (usernames) => {
		return db.countDocuments({
			'_id': {
				'$in': usernames
			}
		});
	},

	count: (filter) => {
		if (filter) {
			return db.countDocuments(filter);
		} else {
			return db.estimatedDocumentCount();
		}
	},

	findOne: (username) => {
		return db.findOne({ '_id': username });
	},

	insertOne: async (original, username, password, authLevel) => {
		// hash the password
		const passwordHash = await bcrypt.hash(password, 12);
		//add to db
		const res = await db.insertOne({
			'_id': username,
			original,
			authLevel,
			passwordHash,
			'ownedBoards': [],
			'modBoards': []
		});
		cache.del(`users:${username}`);
		return res;
	},

	changePassword: async (username, newPassword) => {
		const passwordHash = await bcrypt.hash(newPassword, 12);
		const res = await db.updateOne({
			'_id': username
		}, {
			'$set': {
				'passwordHash': passwordHash
			}
		});
		cache.del(`users:${username}`);
		return res;
	},

	updateLastActiveDate: (username) => {
		return db.updateOne({
			'_id': username
		}, {
			'$set': {
				lastActiveDate: new Date()
			}
		})
	},

	find: (filter, skip=0, limit=0) => {
		return db.find(filter, {
			'projection': {
				'passwordHash': 0
			}
		}).sort({
			'authLevel': 1
		}).skip(skip).limit(limit).toArray();
	},

	deleteOne: async (username) => {
		const res = await db.deleteOne({
			'_id': username
		});
		cache.del(`users:${username}`);
		return res;
	},

	deleteMany: async (usernames) => {
		const res = await db.deleteMany({
			'_id': {
				'$in': usernames
			}
		});
		cache.del(usernames.map(n => `users:${n}`));
		return res;
	},

	addOwnedBoard: async (username, board) => {
		const res = await db.updateOne({
			'_id': username
		}, {
			'$addToSet': {
				'ownedBoards': board
			}
		});
		cache.del(`users:${username}`);
		return res;
	},

    removeOwnedBoard: async (username, board) => {
		const res = await db.updateOne({
			'_id': username
		}, {
			'$pull': {
				'ownedBoards': board
			}
		});
		cache.del(`users:${username}`);
		return res;
    },

	addModBoard: async (usernames, board) => {
		const res = await db.updateMany({
			'_id': {
				'$in': usernames
			}
		}, {
			'$addToSet': {
				'modBoards': board
			}
		});
		cache.del(usernames.map(n => `users:${n}`));
		return res;
	},

	removeModBoard: async (usernames, board) => {
		const res = await db.updateMany({
			'_id': {
				'$in': usernames
			}
		}, {
			'$pull': {
				'modBoards': board
			}
		});
		cache.del(usernames.map(n => `users:${n}`));
		return res;
	},

	getOwnedOrModBoards: (usernames) => {
		return db.find({
			'_id': {
				'$in': usernames
			},
			'$or': [
				{
					'ownedBoards.0': {
						'$exists': true
					},
				},
				{
					'modBoards.0': {
						'$exists': true
					}
				}
			]
		}, {
			'projection': {
				'ownedBoards': 1,
				'modBoards': 1,
			}
		}).toArray();
	},

	setLevel: async (usernames, level) => {
		//increase users auth level
		const res = await db.updateMany({
			'_id': {
				'$in': usernames
			}
		}, {
			'$set': {
				'authLevel': level
			}
		});
		cache.del(usernames.map(n => `users:${n}`));
		return res;
	},

	deleteAll: () => {
		return db.deleteMany({});
	},

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, db = Mongo.db.collection('bans');

module.exports = {

	db,

	find: (ip, board) => {
		let ipQuery;
		if (typeof ip === 'object') { //object with hash and ranges in bancheck
			ipQuery = {
				'$in': [ip.single, ip.qrange, ip.hrange] //gets single and range ban in 1 query
			}
		} else {
			ipQuery = ip;
		}
		return db.find({
			'ip.single': ipQuery,
			'board': {
				'$in': [board, null]
			}
		}).toArray();
	},

	markSeen: (ids) => {
		return db.updateMany({
			'_id': {
				'$in': ids
			}
		}, {
			'$set': {
				'seen': true,
			}
		});
	},

	appeal: (ip, ids, appeal) => {
		return db.updateMany({
			'_id': {
				'$in': ids
			},
			'ip.single': ip,
			'allowAppeal': true,
			'appeal': null
		}, {
			'$set': {
				'appeal': appeal,
			}
		});
	},

	getGlobalBans: () => {
		return db.find({
			'board': null
		}).sort({
			'allowAppeal': -1,
			'appeal': -1,
			'date': -1,
		}).toArray();
	},

	getBoardBans: (board) => {
		return db.find({
			'board': board,
		}).sort({
			'allowAppeal': -1,
			'appeal': -1,
			'date': -1,
		}).toArray();
	},

	denyAppeal: (board, ids) => {
		return db.updateMany({
			'board': board,
			'_id': {
				'$in': ids
			},
/*
			//allow denying appeal even if missing, to allow moving ban to bottom of list
			'allowAppeal': true,
			'appeal': {
				'$ne': null
			},
*/
		}, {
			'$set': {
				'allowAppeal': false,
			}
		});
	},

	removeMany: (board, ids) => {
		return db.deleteMany({
			'board': board,
			'_id': {
				'$in': ids
			}
		});
	},

	deleteBoard: (board) => {
		return db.deleteMany({
			'board': board
		});
	},

	insertOne: (ban) => {
		return db.insertOne(ban);
	},

	insertMany: (bans) => {
		return db.insertMany(bans);
	},

	deleteAll: () => {
		return db.deleteMany({});
	},

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, cache = require(__dirname+'/../redis.js')
	, dynamicResponse = require(__dirname+'/../helpers/dynamic.js')
	, db = Mongo.db.collection('boards');

module.exports = {

	db,

	findOne: async (name) => {
		let board = await cache.get(`board:${name}`);
		if (board) {
			return board === 'no_exist' ? null : board;
		} else {
			board = await db.findOne({ '_id': name });
			if (board) {
				cache.set(`board:${name}`, board, 3600);
				if (board.banners.length > 0) {
					cache.sadd(`banners:${name}`, board.banners);
				}
			} else {
				cache.set(`board:${name}`, 'no_exist', 600);
			}
		}
		return board;
	},

	randomBanner: async (name) => {
		let banner = await cache.srand(`banners:${name}`);
		if (!banner) {
			const board = await module.exports.findOne(name);
			if (board) {
				banner = board.banners[Math.floor(Math.random()*board.banners.length)];
			}
		}
		return banner;
	},

	setOwner: (board, username) => {
		cache.del(`board:${board}`);
		return db.updateOne({
			'_id': board
		}, {
			'$set': {
				'owner': username
			}
		});
	},

	insertOne: (data) => {
		cache.del(`board:${data._id}`); //removing cached no_exist
		if (!data.settings.unlistedLocal) {
			cache.sadd('boards:listed', data._id);
		}
		return db.insertOne(data);
	},

	deleteOne: (board) => {
		cache.del(`board:${board}`);
		cache.del(`banners:${board}`);
		cache.srem('boards:listed', board);
		cache.srem('triggered', board);
		return db.deleteOne({ '_id': board });
	},

	updateOne: (board, update) => {
		if (update['$set']
			&& update['$set'].settings
			&& update['$set'].settings.unlistedLocal !== null) {
			if (update['$set'].settings.unlistedLocal) {
				cache.srem('boards:listed', board);
			} else {
				cache.sadd('boards:listed', board);
			}
		}
		cache.del(`board:${board}`);
		return db.updateOne({
			'_id': board
		}, update);
	},

	deleteAll: (board) => {
		return db.deleteMany({});
	},

	removeModerator: (board, username) => {
		cache.del(`board:${board}`);
		return db.updateOne(
			{
				'_id': board,
			}, {
				'$pull': {
					'settings.moderators': username
				}
			}
		);
	},

	removeBanners: (board, filenames) => {
		cache.del(`board:${board}`);
		cache.del(`banners:${board}`);
		return db.updateOne(
			{
				'_id': board,
			}, {
				'$pullAll': {
					'banners': filenames
				}
			}
		);
	},

	addBanners: (board, filenames) => {
		cache.del(`board:${board}`);
		cache.del(`banners:${board}`);
		return db.updateOne(
			{
				'_id': board,
			}, {
				'$push': {
					'banners': {
						'$each': filenames
					}
				}
			}
		);
	},

	getLocalListed: async () => {
		let cachedListed = await cache.sgetall('boards:listed');
		if (cachedListed && cachedListed.length > 0) {
			return cachedListed;
		}
		let listedBoards = await db.find({
			'settings.unlistedLocal': false
		}, {
			'projection': {
				'_id': 1,
			}
		}).toArray();
		if (listedBoards.length == 0) {
			return [];
		}
		listedBoards = listedBoards.map(b => b._id);
		await cache.sadd('boards:listed', listedBoards);
		return listedBoards;
	},

	boardSort: (skip=0, limit=50, sort={ ips:-1, pph:-1, sequence_value:-1 }, filter={}, showSensitive=false) => {
		const addedFilter = {};
		const projection = {
			'_id': 1,
			'lastPostTimestamp': 1,
			'sequence_value': 1,
			'pph': 1,
			'ips': 1,
			'settings.sfw': 1,
			'settings.description': 1,
			'settings.name': 1,
			'settings.tags': 1,
			'settings.unlistedLocal': 1,
		};
		if (!showSensitive) {
			addedFilter['settings.unlistedLocal'] = false;
		} else {
			if (filter.filter_sfw) {
				addedFilter['settings.sfw'] = true;
			}
			if (filter.filter_unlisted) {
				addedFilter['settings.unlistedLocal'] = true;
			}
			if (filter.filter_abandoned) {
				addedFilter['owner'] = null;
				addedFilter['settings.moderators'] = [];
			}
			projection['settings.moderators'] = 1;
			projection['owner'] = 1;
		}
		if (filter.search) {
			addedFilter['$or'] = [
				{ 'settings.tags': filter.search },
				{ '_id':  filter.search },
			];
		}
		return db.find(addedFilter, { projection })
		.sort(sort)
		.skip(skip)
		.limit(limit)
		.toArray();
	},

	webringBoards: () => {
		return db.find({
			'settings.unlistedWebring': false
		}, {
			'projection': {
				'_id': 1,
				'lastPostTimestamp': 1,
				'sequence_value': 1,
				'pph': 1,
				'ips': 1,
				'settings.sfw': 1,
				'settings.description': 1,
				'settings.name': 1,
				'settings.tags': 1,
			}
		}).toArray();
	},

	count: (filter, showSensitive=false) => {
		const addedFilter = {};
		if (!showSensitive) {
			addedFilter['settings.unlistedLocal'] = false;
		} else {
			if (filter.filter_sfw) {
				addedFilter['settings.sfw'] = true;
			}
			if (filter.filter_unlisted) {
				addedFilter['settings.unlistedLocal'] = true;
			}
			if (filter.filter_abandoned) {
				addedFilter['owner'] = null;
				addedFilter['settings.moderators'] = [];
			}
		}
		if (filter.search) {
			addedFilter['$or'] = [
				{ 'settings.tags': filter.search },
				{ '_id':  filter.search },
			];
		}
		return db.countDocuments(addedFilter);
	},

	totalStats: () => {
		return db.aggregate([
			{
				'$group': {
					'_id': null,
					'posts': {
						'$sum': '$sequence_value'
					},
					'pph': {
						'$sum': '$pph'
					},
					'total': {
						'$sum': 1
					},
					'unlisted': {
						'$sum': {
							'$cond': ['$settings.unlistedLocal', 1, 0]
						}
					},
					//removed ips because sum is inaccurate
				}
			}
		]).toArray().then(res => res[0]);
	},

	exists: async (req, res, next) => {
		const board = await module.exports.findOne(req.params.board);
		if (!board) {
			return res.status(404).render('404');
		}
		res.locals.board = board;
		next();
	},

	bodyExists: async (req, res, next) => {
		const board = await module.exports.findOne(req.body.board);
		if (!board) {
			return dynamicResponse(req, res, 404, '404', {
				'title': 'Bad request',
				'message': 'Board does not exist',
			});
		}
		res.locals.board = board;
		next();
	},

	triggerModes: (boards) => {
		return db.aggregate([
			{
				'$match': {
					'_id': {
						'$in': boards
					}
				}
			}, {
				'$project': {
					'_id': 1,
					'lockMode': '$settings.lockMode',
					'lockReset': '$settings.lockReset',
					'captchaMode': '$settings.captchaMode',
					'captchaReset': '$settings.captchaReset',
					'threadLimit': '$settings.threadLimit',
				}
			}
		]).toArray();
	},

	getNextId: async (board, saged) => {
		const update = {
			'$inc': {
				'sequence_value': 1
			},
		};
		if (!saged) {
			update['$set'] = {
				'lastPostTimestamp': new Date()
			};
		}
		const increment = await db.findOneAndUpdate(
			{
				'_id': board
			}, update, {
				'projection': {
					'sequence_value': 1
				}
			}
		);
		return increment.value.sequence_value;
	},

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, { blockBypass } = require(__dirname+'/../configs/main.js')
	, db = Mongo.db.collection('bypass');

module.exports = {

	db,

	checkBypass: (id) => {
		return db.findOneAndUpdate({
			'_id': id,
			'uses': {
				'$lte': blockBypass.expireAfterUses
			}
		}, {
			'$inc': {
				'uses': 1,
			}
		}).then(r => r.value);
	},

	getBypass: () => {
		return db.insertOne({
			'uses': 0,
			'expireAt': new Date(Date.now() + blockBypass.expireAfterTime)
		});
	},

	deleteAll: () => {
		return db.deleteMany({});
	},

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, db = Mongo.db.collection('captcha');

module.exports = {

	db,

	findOne: (id) => {
		return db.findOne({ '_id': id });
	},

	insertOne: (answer) => {
		return db.insertOne({
			'answer': answer,
			'expireAt': new Date()
		});
	},

	findOneAndDelete: (id, answer) => {
		return db.findOneAndDelete({
			'_id': id,
			'answer': answer
		});
	},

	randomSample: () => {
		return db.aggregate([
			{
				$sample: { size: 1 }
			}
		]).toArray();
	},

	deleteAll: () => {
		return db.deleteMany({});
	},

}


code:
'use strict';

const { MongoClient, ObjectId, Int32 } = require('mongodb')
	, { migrateVersion } = require(__dirname+'/../package.json')
	, configs = require(__dirname+'/../configs/main.js');

module.exports = {

	connect: async () => {
		if (module.exports.client) {
			throw new Error('Mongo already connected');
		}
		module.exports.client = await MongoClient.connect(configs.dbURL, {
			useNewUrlParser: true,
			useUnifiedTopology: true
		});
		module.exports.db = module.exports.client.db(configs.dbName);
	},

	checkVersion: async() => {
		const currentVersion = await module.exports.db
			.collection('version')
			.findOne({ '_id': 'version' })
			.then(res => res.version);
		if (currentVersion < migrateVersion) {
			console.error('Your migration version is out-of-date. Run `gulp migrate` to update.');
			process.exit(1);
		}
	},

	ObjectId,

	NumberInt: Int32,

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, Boards = require(__dirname+'/boards.js')
	, formatSize = require(__dirname+'/../helpers/files/formatsize.js')
	, db = Mongo.db.collection('files')

module.exports = {

	db,

	increment: (file) => {
		return db.updateOne({
			'_id': file.filename
		}, {
			'$inc': {
				'count': 1
			},
			'$addToSet': {//save list of thumb exts incase config is changed to track old exts
				'exts': file.thumbextension,
			},
			'$setOnInsert': {
				'size': file.size
			}
		}, {
			'upsert': true
		});
	},

	decrement: (fileNames) => {
		return db.updateMany({
			'_id': {
				'$in': fileNames
			}
		}, {
			'$inc': {
				'count': -1
			}
		}, {
			'upsert': true //probably not necessary
		});
	},

	activeContent: () => {
		return db.aggregate([
			{
				'$group': {
					'_id': null,
					'count': {
						'$sum': 1
					},
					'size': {
						'$sum': '$size'
					}
				}
			}
		]).toArray().then(res => {
			const stats = res[0];
			if (stats) {
				return {
					count: stats.count,
					totalSize: stats.size,
					totalSizeString: formatSize(stats.size)
				}
			} else {
				return {
					count: 0,
					totalSize: 0,
					totalSizeString: '0B'
				}
			}
		});
	},

	deleteAll: () => {
		return db.deleteMany({});
	}

}


code:
'use strict';

module.exports = {

	Posts: require(__dirname+'/posts.js'),
	Boards: require(__dirname+'/boards.js'),
	Webring: require(__dirname+'/webring.js'),
	Stats: require(__dirname+'/stats.js'),
	Accounts: require(__dirname+'/accounts.js'),
	Bans: require(__dirname+'/bans.js'),
	Captchas: require(__dirname+'/captchas.js'),
	Files: require(__dirname+'/files.js'),
	News: require(__dirname+'/news.js'),
	Ratelimits: require(__dirname+'/ratelimits.js'),
	Modlogs: require(__dirname+'/modlogs.js'),
	Bypass: require(__dirname+'/bypass.js'),

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, db = Mongo.db.collection('modlog');

module.exports = {

	db,

	getDates: (board) => {
		return db.aggregate([
			{
				'$match': {
					'board': board._id
				}
			},
			{
				'$project': {
					'year': {
						'$year': '$date'
					},
					'month': {
						'$month': '$date'
					},
					'day': {
						'$dayOfMonth': '$date'
					}
				}
			},
			{
				'$group': {
					'_id': {
						'year': '$year',
						'month': '$month',
						'day': '$day',
					},
					'count': {
						'$sum': 1
					}
				}
			},
			{
				'$project': {
					'_id': 0,
					'date': '$_id',
					'count': '$count'
				}
			},
			{
				'$sort': {
					'date.year': -1,
					'date.month': -1,
					'date.day': -1
				}
			},
		]).toArray();
	},

	find: (filter, offset, limit) => {
		return db.find(filter)
			.skip(offset)
			.limit(limit)
			.sort({
				'_id': -1
			})
			.toArray();
	},

	count: (filter) => {
		return db.countDocuments(filter);
	},

	findBetweenDate: (board, start, end) => {
		const startDate = Mongo.ObjectId.createFromTime(Math.floor(start.getTime()/1000));
		const endDate = Mongo.ObjectId.createFromTime(Math.floor(end.getTime()/1000));
		return db.find({
			'_id': {
				'$gte': startDate,
				'$lte': endDate
			},
			'board': board._id
		}).sort({
			'_id': -1
		}).toArray();
	},

	deleteOld: (board, date) => {
		const monthOld = Mongo.ObjectId.createFromTime(Math.floor(date.getTime()/1000));
		return db.deleteMany({
			'_id': {
				'$lt': monthOld
			}
		});
	},

	insertOne: (event) => {
		return db.insertOne(event);
	},

	insertMany: (events) => {
		return db.insertMany(events);
	},

	deleteBoard: (board) => {
		return db.deleteMany({
			'board': board
		});
	},

	deleteAll: () => {
		return db.deleteMany({});
	},

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, db = Mongo.db.collection('news');

module.exports = {

	db,

	find: (limit=0) => {
		return db.find({}).sort({
			'_id': -1
		})
		.limit(limit)
		.toArray();
	},

	insertOne: (news) => {
		return db.insertOne(news);
	},

	deleteMany: (ids) => {
		return db.deleteMany({
			'_id': {
				'$in': ids
			}
		})
	},

	deleteAll: () => {
		return db.deleteMany({});
	},

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, Boards = require(__dirname+'/boards.js')
	, Stats = require(__dirname+'/stats.js')
	, db = Mongo.db.collection('posts')
	, { quoteLimit, previewReplies, stickyPreviewReplies
		, early404Replies, early404Fraction } = require(__dirname+'/../configs/main.js');

module.exports = {

	db,

	getThreadPage: async (board, thread) => {
		const threadsBefore = await db.countDocuments({
			'board': board,
			'thread': null,
			'bumped': {
				'$gte': thread.bumped
			}
		});
		return Math.ceil(threadsBefore/10) || 1; //1 because 0 threads before is page 1
	},

	getGlobalRecent: (offset=0, limit=20, ip) => {
		//global recent posts for recent section of global manage page
		const query = {};
		if (ip instanceof RegExp) {
			query['ip.single'] = ip;
		} else if (typeof ip === 'string') {
			query['ip.raw'] = ip;
		}
		return db.find(query).sort({
			'_id': -1
		}).skip(offset).limit(limit).toArray();
	},

	getBoardRecent: (offset=0, limit=20, ip, board) => {
		const query = {
			board
		};
		if (ip instanceof RegExp) {
			query['ip.single'] = ip;
		} else if (typeof ip === 'string') {
			query['ip.raw'] = ip;
		}
		return db.find(query).sort({
			'_id': -1
		}).skip(offset).limit(limit).toArray();
	},

	getRecent: async (board, page, limit=10, getSensitive=false, sortSticky=true) => {
		// get all thread posts (posts with null thread id)
		const projection = {
			'salt': 0,
			'password': 0,
			'reports': 0,
			'globalreports': 0,
		};
		if (!getSensitive) {
			projection['ip'] = 0;
		}
		const threadsQuery = {
			'thread': null,
		};
		if (board) {
			if (Array.isArray(board)) {
				//array for overboard
				threadsQuery['board'] = {
					'$in': board
				}
			} else {
				threadsQuery['board'] = board;
			}
		}
		let threadsSort = {
			'bumped': -1,
		};
		if (sortSticky === true) {
			threadsSort = {
				'sticky': -1,
				'bumped': -1
			}
		}
		const threads = await db.find(threadsQuery, {
			projection
		})
		.sort(threadsSort)
		.skip(10*(page-1))
		.limit(limit)
		.toArray();

		// add last n posts in reverse order to preview
		await Promise.all(threads.map(async thread => {
			const previewRepliesLimit = thread.sticky ? stickyPreviewReplies : previewReplies;
			const replies = previewRepliesLimit === 0 ? [] : await db.find({
				'thread': thread.postId,
				'board': thread.board
			},{
				projection
			}).sort({
				'postId': -1
			}).limit(previewRepliesLimit).toArray();

			//reverse order for board page
			thread.replies = replies.reverse();

			//if enough replies, show omitted count
			if (thread.replyposts > previewRepliesLimit) {
				//dont show all backlinks on OP for previews on index page
				thread.previewbacklinks = [];
				if (previewRepliesLimit > 0) {
					const firstPreviewId = thread.replies[0].postId;
					const latestPreviewBacklink = thread.backlinks.find(bl => { return bl.postId >= firstPreviewId });
					if (latestPreviewBacklink != null) {
						const latestPreviewIndex = thread.backlinks.map(bl => bl.postId).indexOf(latestPreviewBacklink.postId);
						thread.previewbacklinks = thread.backlinks.slice(latestPreviewIndex);
					}
				}
				//count omitted image and posts
				const numPreviewFiles = replies.reduce((acc, post) => { return acc + post.files.length }, 0);
				thread.omittedfiles = thread.replyfiles - numPreviewFiles;
				thread.omittedposts = thread.replyposts - replies.length;
			}
		}));
		return threads;

	},

	resetThreadAggregates: (ors) => {
		return db.aggregate([
			{
				'$match': {
					'$or': ors
				}
			}, {
				'$set': {
					'replyposts': 0,
					'replyfiles': 0,
					'bumped': '$date'
				}
			}, {
				'$project': {
					'_id': 1,
					'board': 1,
					'replyposts': 1,
					'replyfiles': 1,
					'bumped': 1
				}
			}
		]).toArray();
	},

	getThreadAggregates: (ors) => {
		return db.aggregate([
			{
				'$match': {
					'$or': ors
				}
			}, {
				'$group': {
					'_id': {
						'thread': '$thread',
						'board': '$board'
					},
					'replyposts': {
						'$sum': 1
					},
					'replyfiles': {
						'$sum': {
							'$size': '$files'
						}
					},
					'bumped': {
						'$max': {
							'$cond': [
								{ '$ne': [ '$email', 'sage' ] },
								'$date',
								0 //still need to improve this to ignore bump limitthreads
							]
						}
					}
				}
			}
		]).toArray();
	},

	getPages: (board) => {
		return db.countDocuments({
			'board': board,
			'thread': null
		});
	},

	getThread: async (board, id, getSensitive=false) => {
		// get thread post and potential replies concurrently
		const projection = {
			'salt': 0,
			'password': 0,
			'reports': 0,
			'globalreports': 0,
		};
		if (!getSensitive) {
			projection['ip'] = 0;
		}
		const [thread, replies] = await Promise.all([
			db.findOne({
				'postId': id,
				'board': board,
				'thread': null,
			}, {
				projection,
			}),
			module.exports.getThreadPosts(board, id, projection)
		])
		// attach the replies to the thread post
		if (thread && replies) {
			thread.replies = replies;
		}
		return thread;
	},

	getThreadPosts: (board, id, projection) => {
		// all posts within a thread
		return db.find({
			'thread': id,
			'board': board
		}, {
			projection
		}).sort({
			'postId': 1
		}).toArray();
	},

	getMultipleThreadPosts: (board, ids) => {
		//all posts from multiple threads in a single board
		return db.find({
			'board': board,
			'thread': {
				'$in': ids
			}
		}, {
			'projection': {
				'salt': 0 ,
				'password': 0,
				'ip': 0,
				'reports': 0,
				'globalreports': 0,
			}
		}).toArray();
	},

	getCatalog: (board) => {

		// get all threads for catalog
		return db.find({
			'thread': null,
			'board': board
		}, {
			'projection': {
				'salt': 0,
				'password': 0,
				'ip': 0,
				'reports': 0,
				'globalreports': 0,
			}
		}).sort({
			'sticky': -1,
			'bumped': -1,
		}).toArray();

	},

	getPost: (board, id, getSensitive=false) => {

		// get a post
		if (getSensitive) {
			return db.findOne({
				'postId': id,
				'board': board
			});
		}

		return db.findOne({
			'postId': id,
			'board': board
		}, {
			'projection': {
				'salt': 0,
				'password': 0,
				'ip': 0,
				'reports': 0,
				'globalreports': 0,
			}
		});

	},

	checkExistingMessage: async (board, thread = null, hash) => {
		const query = {
			'board': board,
			'messagehash': hash,
		}
		if (thread !== null) {
			query['$or'] = [
				{ 'thread': thread },
				{ 'postId': thread },
			]
		}
		const postWithExistingMessage = await db.findOne(query, {
			'projection': {
				'messagehash': 1,
			}
		});
		return postWithExistingMessage;
	},

	checkExistingFiles: async (board, thread = null, hashes) => {
		const query = {
			'board': board,
			'files.hash': {
				'$in': hashes
			}
		}
		if (thread !== null) {
			query['$or'] = [
				{ 'thread': thread },
				{ 'postId': thread },
			]
		}
		const postWithExistingFiles = await db.findOne(query, {
			'projection': {
				'files.hash': 1,
			}
		});
		return postWithExistingFiles;
	},

	allBoardPosts: (board) => {
		return db.find({
			'board': board
		}).toArray();
	},

	//takes array "ids" of post ids
	getPosts: (board, ids, getSensitive=false) => {

		if (getSensitive) {
			return db.find({
				'postId': {
					'$in': ids
				},
				'board': board
			}).toArray();
		}

		return db.find({
			'postId': {
				'$in': ids
			},
			'board': board
		}, {
			'projection': {
				'salt': 0,
				'password': 0,
				'ip': 0,
				'reports': 0,
				'globalreports': 0,
			}
		}).toArray();

	},

	// get only thread and post id for use in quotes
	getPostsForQuotes: (queryOrs) => {
		return db.find({
			'$or': queryOrs
		}, {
			'projection': {
				'postId': 1,
				'board': 1,
				'thread': 1,
			}
		}).limit(quoteLimit).toArray();
	},

	//takes array "ids" of mongo ids to get posts from any board
	globalGetPosts: (ids) => {
		return db.find({
			'_id': {
				'$in': ids
			},
		}).toArray();
	},

	insertOne: async (board, data, thread) => {
		const sageEmail = data.email === 'sage';
		const bumpLocked = thread && thread.bumplocked === 1;
		const bumpLimited = thread && thread.replyposts >= board.settings.bumpLimit;
		const cyclic = thread && thread.cyclic === 1;
		const saged = sageEmail || bumpLocked || (bumpLimited && !cyclic);
		if (data.thread !== null) {
			const filter = {
				'postId': data.thread,
				'board': board._id
			}
			//update thread reply and reply file count
			const query = {
				'$inc': {
					'replyposts': 1,
					'replyfiles': data.files.length
				}
			}
			//if post email is not sage, and thread not bumplocked, set bump date
			if (!saged) {
				query['$set'] = {
					'bumped': new Date()
				}
			} else if (bumpLimited && !cyclic) {
				query['$set'] = {
					'bumplocked': 1
				}
			}
			//update the thread
			await db.updateOne(filter, query);
		} else {
			//this is a new thread so just set the bump date
			data.bumped = new Date();
		}

		//get the postId and add it to the post
		const postId = await Boards.getNextId(board._id, saged);
		data.postId = postId;

		//insert the post itself
		const postMongoId = await db.insertOne(data).then(result => result.insertedId); //_id of post

		await Stats.updateOne(board._id, data.ip.single, data.thread == null);

		//add backlinks to the posts this post quotes
		if (data.thread && data.quotes.length > 0) {
			await db.updateMany({
				'_id': {
					'$in': data.quotes.map(q => q._id)
				}
			}, {
				'$push': {
					'backlinks': { _id: postMongoId, postId: postId }
				}
			});
		}

		return postId;

	},

	getReports: (board) => {
		return db.find({
			'reports.0': {
				'$exists': true
			},
			'board': board
		}, {
			'projection': {
				'salt': 0,
				'password': 0,
				//'ip': 0,
				'globalreports': 0,
			}
		}).toArray();
	},

	getGlobalReports: (offset=0, limit, ip) => {
		const query = {
			'globalreports.0': {
				'$exists': true
			}
		}
		if (ip instanceof RegExp) {
			query['$or'] = [
				{ 'ip.single': ip },
				{ 'globalreports.ip.single': ip }
			];
		} else if (typeof ip === 'string') {
			query['$or'] = [
				{ 'ip.raw': ip },
				{ 'globalreports.ip.raw': ip }
			];
		}
		return db.find(query, {
			'projection': {
				'salt': 0,
				'password': 0,
				'reports': 0,
			}
		}).skip(offset).limit(limit).toArray();
	},

	deleteOne: (board, options) => {
		return db.deleteOne(options);
	},

	pruneThreads: async (board) => {

		//get threads that have been bumped off last page
		const oldThreads = await db.find({
			'thread': null,
			'board': board._id
		}).sort({
			'sticky': -1,
			'bumped': -1
		}).skip(board.settings.threadLimit).toArray();

		let early404Threads = [];
		if (board.settings.early404 === true) {
			early404Threads = await db.aggregate([
				{
					//get all the threads for a board
					'$match': {
						'thread': null,
						'board': board._id
					}
				}, {
					//in bump date order
					'$sort': {
						'sticky': -1,
						'bumped': -1
					}
				}, {
					//skip the first (board.settings.threadLimit/early404Fraction)
					'$skip': Math.ceil(board.settings.threadLimit/early404Fraction)
				}, {
					//then any that have less than early404Replies replies get matched again
					'$match': {
						'sticky':0,
						'replyposts': {
							'$lt': early404Replies
						}
					}
				}
			]).toArray();
		}

		return oldThreads.concat(early404Threads);
	},

	fixLatest: (boards) => {
		return db.aggregate([
			{
				'$match': {
					//going to match against thread bump date instead
					'thread': null,
					'board': {
						'$in': boards
					},
				}
			}, {
				'$group': {
					'_id': '$board',
					'lastPostTimestamp': {
						'$max':'$bumped'
					}
				}
			}, {
				'$merge': {
					'into': 'boards'
				}
			}
		]).toArray();
	},

	deleteMany: (ids) => {
		return db.deleteMany({
			'_id': {
				'$in': ids
			}
		});
	},

	deleteAll: (board) => {
		return db.deleteMany();
	},

	move: (ids, dest) => {
		return db.updateMany({
			'_id': {
				'$in': ids
			}
		}, {
			'$set': {
				'thread': dest
			},
			'$unset': {
				'replyposts': '',
				'replyfiles': '',
				'sticky': '',
				'locked': '',
				'bumplocked': '',
				'cyclic': '',
				'salt': ''
			}
		});
	},

	threadExists: (board, thread) => {
		return db.findOne({
			'postId': thread,
			'board': board,
			'thread': null,
		}, {
			'projection': {
				'_id': 1,
				'postId': 1
			}
		});
	},

	exists: async (req, res, next) => {
		const thread = await module.exports.threadExists(req.params.board, req.params.id);
		if (!thread) {
			return res.status(404).render('404');
		}
		res.locals.thread = thread; // can acces this in views or next route handlers
		next();
	}

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, db = Mongo.db.collection('ratelimit');

module.exports = {

	db,

	resetQuota: (identifier, action) => {
		return db.deleteOne({ '_id': `${identifier}-${action}` });
	},

	incrmentQuota: (identifier, action, amount) => {
		return db.findOneAndUpdate(
			{
				'_id': `${identifier}-${action}`
			},
			{
				'$inc': {
					'sequence_value': amount
				},
				'$setOnInsert': {
					'expireAt': new Date()
				}
			},
			{
				'upsert': true
			}
		).then(r => { return r.value ? r.value.sequence_value : 0 });
	},

	deleteAll: () => {
		return db.deleteMany({});
	},

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, db = Mongo.db.collection('poststats');

module.exports = {

	db,

	updateOne: (board, ip, thread) => {
		return db.updateOne({
			'board': board,
			'hour': new Date().getHours()
		}, {
			'$inc': {
				'pph': 1,
				'tph': thread ? 1 : 0
			},
			'$addToSet': {
				'ips': ip
			}
		}, {
			'upsert': true
		});
	},

	getHourPosts: (board) => {
		return db.findOne({
			'board': board,
			'hour': new Date().getHours()
		}, {
			'projection': {
				'_id': 0,
				'pph': 1,
				'tph': 1
			}
		})
	},

	updateBoards: () => {
//todo: improve this query
		return db.aggregate([{
		    '$unwind': {
		        'path': '$ips',
		        'preserveNullAndEmptyArrays': true
		    }
		}, {
		    '$group': {
		        '_id': '$board',
		        'pph': {
		            '$max': '$pph' //use max since only one will have a value until we do a multi facet system that can $merge (dunno if even possible)
		        },
		        'ips': {
		            '$addToSet': '$ips'
		        }
		    }
		}, {
		    '$project': {
		        'ips': {
		            '$size': '$ips'
		        },
		        'pph': 1
		    }
		}, {
		    '$merge': {
		        'into': 'boards'
		    }
		}]).toArray();
	},

	//reset stats, used at start of each hour
	resetStats: () => {
		return Promise.all([
			db.updateMany({
				'hour': new Date().getHours()
			}, {
				'$set': {
					'ips': [],
				}
			}),
			db.updateMany({}, {
				'$set': {
					'pph': 0,
					'tph': 0
				}
			}),
		]);
	},

	deleteBoard: (board) => {
		return db.deleteMany({
			'board': board
		});
	},

	deleteAll: () => {
		return db.deleteMany({});
	},

}


code:
'use strict';

const Mongo = require(__dirname+'/db.js')
	, db = Mongo.db.collection('webring');

module.exports = {

	db,

	boardSort: (skip=0, limit=50, sort={ ips:-1, pph:-1, sequence_value:-1 }, filter={}) => {
		const addedFilter = {};
		if (filter.search) {
			addedFilter['$or'] = [
				{ uri: filter.search },
				{ tags: filter.search }
			]
		}
		const addedSort = {};
		if (sort.ips) {
				addedSort['uniqueUsers'] = sort.ips
		}
		if (sort.pph) {
				addedSort['postsPerHour'] = sort.pph
		}
		if (sort.sequence_value) {
				addedSort['totalPosts'] = sort.sequence_value
		}
		if (sort.lastPostTimestamp) {
				addedSort['lastPostTimestamp'] = sort.lastPostTimestamp
		}
		return db.find(addedFilter)
		.sort(addedSort)
		.skip(skip)
		.limit(limit)
		.toArray();
	},

	count: (filter) => {
		const addedFilter = {};
		if (filter.search) {
			addedFilter['$or'] = [
				{ uri: filter.search },
				{ tags: filter.search }
			]
		}
		return db.countDocuments(addedFilter);
	},

	deleteAll: () => {
		return db.deleteMany({});
	},

}


code:
const numCpus = require('os').cpus().length;
module.exports = {
	// Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/
	apps : [{
		name: 'build-worker',
		script: 'worker.js',
		instances: Math.floor(numCpus/2), //if you only have 1 core and floor to 0, 0 just means "all cores" which is correct in that case.
		autorestart: true,
		watch: false,
		max_memory_restart: '1G',
		log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS',
		env: {
			NODE_ENV: 'development'
		},
		env_development: {
			NODE_ENV: 'development'
		},
		env_production: {
			NODE_ENV: 'production'
		}
	}, {
		name: 'chan',
		script: 'server.js',
		instances: Math.floor(numCpus/2),
		autorestart: true,
		watch: false,
		max_memory_restart: '1G',
		log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS',
		wait_ready: true,
		kill_timeout: 5000,
		env: {
			NODE_ENV: 'development'
		},
		env_production: {
			NODE_ENV: 'production'
		}
	}, {
		name: 'schedules',
		script: 'schedules/index.js',
		instances: 1,
		autorestart: true,
		watch: false,
		max_memory_restart: '1G',
		log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS',
		env: {
			NODE_ENV: 'development'
		},
		env_development: {
			NODE_ENV: 'development'
		},
		env_production: {
			NODE_ENV: 'production'
		}
	}]
};


code:
img {
	width: var(--captcha-w);
	height: var(--captcha-h);
	image-rendering: crisp-edges;
	margin: 0 auto;
}
input {
	position: fixed;
	left: -3px;
	bottom: 0;
	opacity: 0.9;
	border: none;
	background: none;
	font-size: 18px;
	cursor: pointer;
	text-shadow: rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px;
	color: white;
}
body {
	font-family: arial, helvetica, sans-serif;
	font-size: 10pt;
	margin: 0;
	padding: 0;
}
.flexcenter {
	display: flex;
	flex-direction: column;
	align-items: center;
}


code:
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color:#040404;
	--alt-font-color:#c5c8c6;
	--background-top:#000;
	--background-rest:#000;
	--navbar-color:#040404;
	--post-color:#000;
	--post-outline-color:#111;
	--label-color:#040404;
	--box-border-color:#111;
	--darken:#040404;
	--highlighted-post-color:#000;
	--highlighted-post-outline-color:#444;
	--board-title:#c5c8c6;
	--hr:#040404;
	--font-color:#c5c8c6;
	--name-color:#c5c8c6;
	--capcode-color:#f00;
	--subject-color:#b294bb;
	--link-color:#c5c8c6;
	--post-link-color:#5f89ac;
	--link-hover:#81a2be;
	--input-borders:#111;
	--input-color:#c5c8c6;
	--input-background:#040404;
	--dice-color:darkorange;
	--title-color:#d70000;
	--greentext-color:green;
	--pinktext-color:#E0727F;
}
.spoiler	{
	background:gray;
	color:gray;
}
.spoiler:not(:hover) * {
	color: gray;
	background: gray!important;
}
.anchor:target + .post-container,
.post-container.highlighted,
.anchor:target + table tbody tr th,
.anchor:target + table {
/*    border-style: dashed var(--highlighted-post-outline-color);*/
}
.post-info, .post-container:target .post-info, .post-container.highlighted .post-info {
    background:none!important;border:none!important
}


code:
:root {
--label-color: #1DA1F2;
--alt-label-color: #1DA1F2;
--background-top: #E6ECF0;
--background-rest: #E6ECF0;
--navbar-color: white;
--post-color: white;
--highlighted-post-color: white;
--highlighted-post-outline-color: #1DA1F2;
--link-color: #1da1f2;
--board-title: #1DA1F2;
--font-color: black;
}
body, .navbar {
font-size: unset;
margin: 0 auto;
}
@media screen and (min-width: 800px) {
body, .navbar {
width: 50vw;
}
}
.post-container.op {
background:var(--post-color);
}
.stickynav{display:none}
.post-container, .pages {
padding: 10px;
width: 100%;
}
.post-name::before {
content:"👤";
font-size: 30px;
justify-content: center;
align-items: center;
display: inline-flex;
width: 70px;
height: 70px;
background: lightgray;
border-radius: 50%;
margin: 10px
}
.post-icons, .post-subject, .post-check, .board-banner, .postmenu, .user-id, .flag {
display: none;
}
.post-container, .catalog-tile, .navbar, .pages, .toggle-summary, #threadstats, #livetext  {
border: none;
border-radius: 10px;
}
.highlighted, .hoverhighlighted {
border: 1px solid var(--highlighted-post-outline-color)!important;
}
.anchor{
background: var(--post-color);
height: 30px;
width: 10px;
margin-left: 50px;
margin-top: 0
}
#float  {
pointer-events: none;
border: none;
box-shadow: none;
}
.label, .rlabel, table {
border:none;
}


code:
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color:#001010;
	--alt-font-color:#00CCCC;
	--background-top:#000505;
	--background-rest:#000505;
	--navbar-color:#001010;
	--post-color:#001010;
	--post-outline-color:#B7C5D9;
	--label-color:#001010;
	--box-border-color:#00AAAA;
	--darken:#00000010;
	--highlighted-post-color:#003030;
	--highlighted-post-outline-color:#FF0000;
	--board-title:#af0a0f;
	--hr:lightgray;
	--font-color:#00CCCC;
	--name-color:#FF0000;
	--capcode-color:#f00;
	--subject-color:#FF0000;
	--link-color:#FF0000;
	--post-link-color:#FF0000;
	--link-hover:#d00;
	--input-borders:#00AAAA;
	--input-color:#00CCCC;
	--input-background:#000505;
	--dice-color:darkorange;
	--title-color:#d70000;
	--greentext-color:#FF0000;
	--pinktext-color:#E0727F;
}
#livetext,
#threadstats,
#float	.post-container,
.catalog-tile,
.live,
.modal,
.pages,
.post-container,
.stickynav,
.toggle-summary,
input[type=file],
input[type=number],
input[type=password],
input[type=submit],
input[type=text],
select,
textarea,
.label,
.postform-style,
.close,
table,
.board-banner,
#postform {
	border:	1px	dotted;
}
hr {
	border-top:	1px	dotted;
}
.navbar {
	border-bottom:	1px	dotted;
}
a .post-name:hover,
a:hover {
	background:var(--link-hover)!important;
	color:black!important;
}
.anchor:target + .post-container .post-info, .post-container.highlighted .post-info {
	border-bottom: none;
}
@media only screen and (max-width: 600px) {

	.post-info{
		background: none;
		border-bottom: none;
	}

}


code:
/*tomorrow*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #42200f;
	--alt-font-color: #ff7e83;
	--background-top: #2f1903;
	--background-rest: #261602;
	--navbar-color: #302016;
	--post-color: #302016;
	--post-outline-color: #523830;
	--label-color: #42200f;
	--box-border-color: #4c332c;
	--darken: #ffffff10;
	--highlighted-post-color: #572a1b;
	--highlighted-post-outline-color: #784134;
	--board-title: #ff7e83;
	--hr: #4c332c;
	--font-color: #ff7e83;
	--name-color: #ff7e83;
	--capcode-color: #ff5252;
	--subject-color: #ff5252;
	--link-color: #a2a2ff;
	--post-link-color: #a2a2ff;
	--link-hover: #ff4d4b;
	--input-borders: #535353;
	--input-color: #fff;
	--input-background: #000;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: green;
	--pinktext-color:#E0727F;
}


code:
/*clear*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #f5f5f5;
	--alt-font-color: #333;
	--background-top: #EEE;
	--background-rest: #EEE;
	--navbar-color: #DDD;
	--post-color: #DDD;
	--post-outline-color: #d2d2d2;
	--label-color: #f5f5f5;
	--box-border-color: #e0e0e0;
	--darken: #0000000f;
	--highlighted-post-color: #EEDACB;
	--highlighted-post-outline-color: #d2d2d2;
	--board-title: maroon;
	--hr: #DDD;
	--font-color: #333;
	--name-color: #333;
	--capcode-color: #f00;
	--subject-color: #004A99;
	--link-color: black;
	--post-link-color: darkblue;
	--link-hover: #a74300;
	--input-borders: #d2d2d2;
	--input-color: #333;
	--input-background: #f5f5f5;
	--dice-color: maroon;
	--title-color: #d70000;
	--greentext-color: #789922;
	--pinktext-color:#E0727F;
}
.post-info, .post-container:target .post-info, .post-container.highlighted .post-info {
    background:none!important;border:none!important
}


code:
:root {
	--background-top: rgb(40, 40, 46);
	--background-rest: rgb(40, 40, 46);
	--label-color: #1f1f1f;
	--box-border-color: #0099ff;
	--font-color: #eee;
	--alt-font-color: #fff;
	--alt-label-color: #800;
	--navbar-color: #3f3f3f;
	--darken: #00000010;
	--link-color: #0099ff;
	--post-link-color: #0099ff;
	--link-hover: #3333ff;
	--input-borders: #a9a9a9;
	--input-color: #fff;
	--input-background: black;
	--post-color: #1f1f1f;
	--post-outline-color: #0099ff;
	--highlighted-post-color: #2c2c3c;
	--highlighted-post-outline-color: #0555ff;
	--dice-color: darkorange;
	--title-color: #ff0000;
	--greentext-color: #55ff33;
	--pinktext-color:#E0727F;
	--board-title: #33ffff;
	--name-color: #0044ff;
	--capcode-color: #f00;
	--subject-color: #33ddff;
	--hr: #0099ff;
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
}
body {
	font-family: 'Hack', monospace, sans-serif;
	font-size: 9pt;
	color: #eee;
}
.bold {
	font-weight: bold;
	color: #fff;
}
.em {
	color: #FF6700;
}
.strikethrough {
	color: #e6e600;
}
a, a:visited, a .post-name {
	text-decoration: none;
	font-weight: bolder;
	font-size: 12px;
	color: var(--link-color);
}
/* prevent above rule making link in board titles small */
.board-title a {
    font-size: inherit;
}
.catalog-tile {
	padding: 10px;
	margin: 4px;
	height: 240px;
	border: 1px solid var(--post-outline-color);
	max-width: 250px;
}
.catalog-thumb {
	width: 80px;
	height: 80px;
}
.spoiler {
	cursor: help;
}


code:
/*gurochan*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #e6cbc0;
	--alt-font-color: #000;
	--background-top: #EDDAD2;
	--background-rest: #EDDAD2;
	--navbar-color: #D9AF9E;
	--post-color: #D9AF9E;
	--post-outline-color: #CA927B;
	--label-color: #E6CBC0;
	--box-border-color: #CA927B;
	--darken: #0000000f;
	--highlighted-post-color: #EEDACB;
	--highlighted-post-outline-color: #CA927B;
	--board-title: #FF6600;
	--hr: #CA927B;
	--font-color: #000;
	--name-color: #000;
	--capcode-color: #f00;
	--subject-color: #004A99;
	--link-color: #34345C;
	--post-link-color: #34345C;
	--link-hover: #34345C;
	--input-borders: #CA927B;
	--input-color: #000;
	--input-background: #E6CBC0;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: #789922;
	--pinktext-color:#E0727F;
}


code:
/*lainchan*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #333;
	--alt-font-color: #bbb;
	--background-top: #1E1E1E;
	--background-rest: #1E1E1E;
	--navbar-color: #333;
	--post-color: #333;
	--post-outline-color: #555;
	--label-color: #333;
	--box-border-color: #666;
	--darken: #ffffff0f;
	--highlighted-post-color: #32DD7220;
	--highlighted-post-outline-color: #32DD7250;
	--board-title: #32DD72;
	--hr: #333;
	--font-color: #bbb;
	--name-color: #32DD72;
	--capcode-color: #f00;
	--subject-color: #b294bb;
	--link-color: #fff;
	--post-link-color: #fff;
	--link-hover: #32DD72;
	--input-borders: #666;
	--input-color: #CCCCCC;
	--input-background: #333;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: #B8D962;
	--pinktext-color:#E0727F;
}
.post-container, #float .post-container, .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats {
    border-width: 1px;
}
.captcha {
	filter: invert(90%);
}


code:
/*miku*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #B6DDDE;
	--alt-font-color: black;
	--background-top: #2e99cc70;
	--background-rest: #D2FFEE;
	--navbar-color: #B6DDDE;
	--post-color: #B6DDDE;
	--post-outline-color: #8FCCCD;
	--label-color: #B6DDDE;
	--box-border-color: #8FCCCD;
	--darken: #00000010;
	--highlighted-post-color: #a9d8ff;
	--highlighted-post-outline-color: #8FCCCD;
	--board-title: #800000;
	--hr: #B7D9C5;
	--font-color: black;
	--name-color: #117743;
	--capcode-color: #f00;
	--subject-color: #117743;
	--link-color: #00637B;
	--post-link-color: #d00;
	--link-hover: #d00;
	--input-borders: #a9a9a9;
	--input-color: #000;
	--input-background: white;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: #789922;
	--pinktext-color:#E0727F;
}
.post-container, #float .post-container, .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats {
	border-width: 0 1px 1px 0;
}


code:
:root {
	--background-top: #0d1a1d;
	--background-rest: #0d1a1d;
	--alt-label-color: #98E;
	--alt-font-color: white;
	--navbar-color: #D6DAF0;
	--post-color: #182F3F;
	--post-outline-color: #B7C5D9;
	--label-color: #98E;
	--box-border-color: #000;
	--darken: #00000010;
	--highlighted-post-color: #244860;
	--highlighted-post-outline-color: #82CECE;
	--board-title: #af0a0f;
	--hr: lightgray;
	--font-color: #ACACAC;
	--name-color: #117743;
	--capcode-color: #f00;
	--subject-color: #0F0C5D;
	--link-color: #34345C;
	--post-link-color: #d00;
	--link-hover: #d00;
	--input-borders: #a9a9a9;
	--input-color: #000;
	--input-background: white;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: #789922;
	--pinktext-color: #E0727F;
}

body {
	background: var(--background-rest) url(/file/mushroom.png);
	color: #acacac;
}

.spoiler {
	background: #4b4b4b;
	color: #4b4b4b;
	cursor: none;
}

.spoiler:hover,.spoiler:hover a {
	color: #fff;
}

.spoiler:not(:hover) * {
	color: #4b4b4b;
	background: #4b4b4b!important;
}

a,a.post-name,a:visited {
	color: #82cece;
}

.navbar {
	background-color: #182f3f9e;
	border-bottom: 1px solid #82cece;
}

table {
	border: 1px solid #82cece;
}

#float .post-container,#livetext,#threadstats,.catalog-tile,.pages,.post-container,.stickynav,.toggle-summary {
	border: 1px solid #82cece;
}

a.quote {
	color: #82cece;
}

.label,.rlabel {
	background: #182f3f;
	border: 1px solid #82cece;
}

input[type=button],input[type=file],input[type=number],input[type=password],input[type=range],input[type=submit],input[type=text],select,textarea {
	border: 1px solid #82cece;
	background: #182f3f;
	color: #acacac;
}

.postform-style {
	border: 1px solid #82cece;
	background: #182f3f;
	color: #acacac;
}

.post-subject {
	color: #46b5b5!important;
}

.post-name,.post-tripcode {
	color: #a0ffff;
}

th {
	background: #182f3f;
}

.alt {
	background: #182f3f;
}

tr {
	background: url(/file/mushroom.png);
}

.post-icons img {
	filter: invert(15%) sepia(66%) saturate(7479%) hue-rotate(6deg) brightness(98%) contrast(124%);
}

.post-links a {
	color: #acacac;
}


code:
/*tomorrow*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #1a1e34;
	--alt-font-color: #bfbfbf;
	--background-top: #080c19;
	--background-rest: #080c19;
	--navbar-color: #1a1e34;
	--post-color: #1a1e34;
	--post-outline-color: #2c3a4e;
	--label-color: #1a1e34;
	--box-border-color: #2c3a4e;
	--darken: #ffffff08;
	--highlighted-post-color: #44283e;
	--highlighted-post-outline-color: #664e5c;
	--board-title: #c10000;
	--hr: #303030;
	--font-color: #bfbfbf;
	--name-color: #67b38d;
	--capcode-color: #ff4141;
	--subject-color: #8f8dc9;
	--link-color: #8585a3;
	--post-link-color: #f65252;
	--link-hover: #f65252;
	--input-borders: #434343;
	--input-color: #bfbfbf;
	--input-background: #080c19;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: #0a0;
	--pinktext-color:#E0727F;
}


code:
/*pink*/
:root {
	--icon-color: invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%);
	--alt-label-color: #ffcccb;
	--alt-font-color:#80002c;
	--background-top:#ffe4e6;
	--background-rest:#ffe4e6;
	--navbar-color:#ffcccb;
	--post-color:#ffcccb;
	--post-outline-color:#80002c;
	--label-color:#ffcccb;
	--box-border-color:#80002c;
	--darken:#80002c54;
	--highlighted-post-color:#eab5b4;
	--highlighted-post-outline-color:#80002c;
	--board-title:#80002c;
	--hr:#80002c;
	--font-color:#80002c;
	--name-color:#b3003e;
	--capcode-color:#f00;
	--subject-color:#e60050;
	--link-color:#80002c;
	--post-link-color:#80002c;
	--link-hover:#c48899;
	--input-borders:#80002c;
	--input-color:#80002c;
	--input-background:#ffe6ea;
	--dice-color:darkorange;
	--title-color:#d70000;
	--greentext-color:#789922;
	--pinktext-color:#FF00EF;
}
.post-container:not(.op), .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats,#float, table {
	box-shadow: 3px 3px 3px var(--darken);
}
hr {
	height: 0px;
	border-width: 2px medium medium;
	border-style: dashed none none;
	border-color: var(--post-outline-color);
}
.anchor:target + .post-container,
.post-container.highlighted,
.anchor:target + table tbody tr th,
.anchor:target + table {
	border: 1px dashed;
}

@media only screen and (max-width: 600px) {

	.post-info{
		background: none;
		border-bottom: none;
	}

	.anchor:target + .post-container .post-info, .post-container.highlighted .post-info {
		border-bottom: none;
	}

}


code:
/*rei-zero*/
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color: #29373e;
	--alt-font-color: #d6d6d6;
	--background-top: #000E1C;
	--background-rest: #000E1C;
	--navbar-color: #29373E;
	--post-color: #29373E;
	--post-outline-color: #666666;
	--label-color: #29373E;
	--box-border-color: #000;
	--darken: #00000010;
	--highlighted-post-color: #193A3A;
	--highlighted-post-outline-color: #999999;
	--board-title: #193A3A;
	--hr: #474747;
	--font-color: #D6D6D6;
	--name-color: #117743;
	--capcode-color: #f00;
	--subject-color: #CCCCCC;
	--link-color: #5C99AD;
	--post-link-color: #00BfFF;
	--link-hover: #eeeeee;
	--input-borders: #a9a9a9;
	--input-color: lightgray;
	--input-background: #001229;
	--dice-color: darkorange;
	--title-color: #d70000;
	--greentext-color: #789922;
	--pinktext-color:#E0727F;
}


code:
:root {
	--icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%);
	--alt-label-color:#2d2d86;
	--alt-font-color:#f2f2f2;
	--background-top:#19194d;
	--background-rest:#19194d;
	--navbar-color:#2d2d86;
	--post-color:#262673;
	--post-outline-color:#262673;
	--label-color:#2d2d86;
	--box-border-color:#000000;
	--darken:#ffffff10;
	--highlighted-post-color:#3636a6;
	--highlighted-post-outline-color:#111;
	--board-title:#c5c8c6;
	--hr:#282a2e;
	--font-color:#f2f2f2;
	--name-color:#117743;
	--capcode-color:red;
	--subject-color:#6666CC;
	--link-color:white;
	--post-link-color:red;
	--link-hover:#f2f2f2;
	--input-borders:black;
	--input-color:black;
	--input-background:white;
	--dice-color:darkorange;
	--title-color:#d70000;
	--greentext-color:#789922;
	--pinktext-color:#E0727F;
}
.post-message a:hover {
	color:var(--post-link-color)!important;
}
.postform-style {
	color:black
}
.post-info, .post-container:target .post-info, .post-container.highlighted .post-info {
    background:none!important;border:none!important
}


code:
:root {
	--icon-color: invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%);
	--alt-label-color: #800;
	--alt-font-color: #fff;
	--background-top: #002b36;
	--background-rest: #002b36;
	--navbar-color: #002b36;
	--post-color: #073642;
	--post-outline-color: #586e75;
	--label-color: #073642;
	--box-border-color: #586e75;
	--darken: #00000010;
	--highlighted-post-color: #002b36;
	--highlighted-post-outline-color: #cb4b16;
	--board-title: #dc322f;
	--hr: #93a1a1;
	--font-color: #839496;
	--name-color: #b58900;
	--capcode-color: #f00;
	--subject-color: #b58900;
	--link-color: #839496;
	--post-link-color: #268bd2;
	--link-hover: #2aa198;
	--input-borders: #586e75;
	--input-color: #839496;
	--input-background: #002b36;
	--dice-color: #D33682;
	--title-color: #dc322f;
	--greentext-color: #789922;
	--pinktext-color: #d33682;
}
.bold {
	color: #93a1a1;
}
.em {
	color: #586e75;
}
img[src='/file/dice.png'] {
	filter: hue-rotate(290deg)
}


code:
:root {
	--icon-color: invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%);
	--alt-label-color: #EEE8D5;
	--alt-font-color: #657B83;
	--background-top: #FDF6E3;
	--background-rest: #FDF6E3;
	--navbar-color: #EEE8D5;
	--post-color: #EEE8D5;
	--post-outline-color: #93A1A1;
	--label-color: #EEE8D5;
	--box-border-color: #93A1A1;
	--darken: #00000010;
	--highlighted-post-color: #FDF6E3;
	--highlighted-post-outline-color: #cb4b16;
	--board-title: #dc322f;
	--hr: #93a1a1;
	--font-color: #657B83;
	--name-color: #b58900;
	--capcode-color: #f00;
	--subject-color: #b58900;
	--link-color: #657B83;
	--post-link-color: #268bd2;
	--link-hover: #2aa198;
	--input-borders: #93A1A1;
	--input-color: #657B83;
	--input-background: #FDF6E3;
	--dice-color: #D33682;
	--title-color: #dc322f;
	--greentext-color: #789922;
	--pinktext-color: #d33682;
}
.bold {
	color: #93a1a1;
}
.em {
	color: #586e75;
}
img[src='/file/dice.png'] {
	filter: hue-rotate(290deg)
}