code:
function pug_attr(t,e,n,r){if(!1===e||null==e||!e&&("class"===t||"style"===t))return"";if(!0===e)return" "+(r?t:t+'="'+t+'"');var f=typeof e;return"object"!==f&&"function"!==f||"function"!=typeof e.toJSON||(e=e.toJSON()),"string"==typeof e||(e=JSON.stringify(e),n||-1===e.indexOf('"'))?(n&&(e=pug_escape(e))," "+t+'="'+e+'"'):" "+t+"='"+e.replace(/'/g,"'")+"'"}
function pug_escape(e){var a=""+e,t=pug_match_html.exec(a);if(!t)return e;var r,c,n,s="";for(r=t.index,c=0;r<a.length;r++){switch(a.charCodeAt(r)){case 34:n=""";break;case 38:n="&";break;case 60:n="<";break;case 62:n=">";break;default:continue}c!==r&&(s+=a.substring(c,r)),c=r+1,s+=n}return c!==r?s+a.substring(c,r):s}
var pug_match_html=/["&<>]/;function uploaditem(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;
    var locals_for_with = (locals || {});

    (function (uploaditem) {
      pug_mixins["uploaditem"] = pug_interp = function(item){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Cdiv\u003E\u003Cdiv class=\"upload-item\"\u003E\u003Cimg" + (" class=\"upload-thumb\""+pug_attr("src", item.url, true, false)) + "\u002F\u003E\u003Cp\u003E" + (pug_escape(null == (pug_interp = item.name) ? "" : pug_interp)) + "\u003C\u002Fp\u003E\u003Ca class=\"close\"\u003EX\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row sb\"\u003E";
if (item.spoilers) {
pug_html = pug_html + "\u003Clabel\u003E\u003Cinput" + (" type=\"checkbox\" name=\"spoiler\""+pug_attr("value", item.name, true, false)) + "\u002F\u003ESpoiler\u003C\u002Flabel\u003E";
}
pug_html = pug_html + "\u003Clabel\u003E\u003Cinput" + (" type=\"checkbox\" name=\"strip_filename\""+pug_attr("value", item.name, true, false)) + "\u002F\u003EStrip Filename\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdiv\u003E";
};
pug_mixins["uploaditem"](uploaditem);
    }.call(this, "uploaditem" in locals_for_with ?
        locals_for_with.uploaditem :
        typeof uploaditem !== 'undefined' ? uploaditem : undefined));
    ;;return pug_html;}


code:
window.addEventListener('DOMContentLoaded', (event) => {

	let loading = {};

	const viewFullText = async function (e) {
		e.preventDefault();
		const parentPost = this.closest('.post-container');
		if (!parentPost) {
			return;
		}
		const postId = this.hash.substring(1);
		let jsonParts = this.pathname.replace(/\.html$/, '.json').split('/');
		let jsonPath;
		if ((isManage || isModView) && jsonParts.length === 5) {
			jsonParts.splice(2,1); //remove manage from json url
		}
		jsonPath = jsonParts.join('/');
		let hovercache = localStorage.getItem(`hovercache-${jsonPath}`);
		let postJson;
		if (hovercache) {
			hovercache = JSON.parse(hovercache);
			if (hovercache.postId == postId) {
				postJson = hovercache;
			} else if (hovercache.replies.length > 0) {
				postJson = hovercache.replies.find(r => r.postId == postId);
			}
		}
		if (!postJson) {//wasnt cached or cache outdates
			this.style.cursor = 'wait';
			let json;
			try {
				if (!loading[jsonPath]) {
					loading[jsonPath] = fetch(jsonPath).then(res => res.json());
				}
				json = await loading[jsonPath];
			} catch (e) {
				return console.error(e);
			} finally {
				this.style.cursor = '';
			}
			if (json) {
				setLocalStorage(`hovercache-${jsonPath}`, JSON.stringify(json));
				hoverCacheList.value = Object.keys(localStorage).filter(k => k.startsWith('hovercache'));
				if (json.postId == postId) {
					postJson = json;
				} else {
					postJson = json.replies.find(r => r.postId == postId);
				}
			} else {
				return localStorage.removeItem(`hovercache-${jsonPath}`); //thread deleted
			}
		}
		if (!postJson) {
			return; //post was deleted or missing, handle somehow
		}
		this.parentNode.remove();
		const messageParent = parentPost.querySelector('.post-message')
		if (messageParent) {
			messageParent.innerHTML = postJson.message;
		}
		const updatePostMessageEvent = new CustomEvent('updatePostMessage', {
			detail: {
				post: parentPost,
				postId: postJson.postId,
				json: postJson
			}
		});
		window.dispatchEvent(updatePostMessageEvent);
	}

	const viewFullTextLinks = document.getElementsByClassName('viewfulltext');

	for (let i = 0; i < viewFullTextLinks.length; i++) {
		viewFullTextLinks[i].addEventListener('click', viewFullText, false);
	}

});

repo_name: dolphin/jschan

license: mit


code:
let notificationsEnabled = localStorage.getItem('notifications') == 'true';
let notificationYousOnly = localStorage.getItem('notification-yous-only') == 'true';
let yousEnabled = localStorage.getItem('yous-setting') == 'true';
let savedYous = new Set(JSON.parse(localStorage.getItem('yous')));
let yousList;

const toggleAll = (state) => savedYous.forEach(y => toggleOne(y, state));

const toggleQuotes = (quotes, state) => {
	quotes.forEach(q => {
		q.classList[state?'add':'remove']('you');
	});
}

const toggleOne = (you, state) => {
	const [board, postId] = you.split('-');
	const post = document.querySelector(`[data-board="${board}"][data-post-id="${postId}"]`);
	if (post) {
		const postName = post.querySelector('.post-name');
		if (postName) {
			postName.classList[state?'add':'remove']('you');
		}
	}
	const quotes = document.querySelectorAll(`.quote[href^="/${board}/"][href$="#${postId}"]`);
	if (quotes) {
		toggleQuotes(quotes, state);
	}
}

if (yousEnabled) {
	toggleAll(yousEnabled);
}

const handleNewYous = (e) => {
	const postYou = `${e.detail.json.board}-${e.detail.postId}`;
	const isYou = window.myPostId == e.detail.postId
	if (isYou) {
		//save you
		savedYous.add(postYou);
		const arrayYous = [...savedYous];
		yousList.value = arrayYous.toString();
		setLocalStorage('yous', JSON.stringify(arrayYous));
	}
	if (savedYous.has(postYou)) {
		//toggle forn own post for name field
		toggleOne(postYou, yousEnabled);
	}
	const quotesYou = e.detail.json.quotes
		.map(q => `${e.detail.json.board}-${q.postId}`)
		.filter(y => savedYous.has(y))
		.length > 0;
	const youHoverQuotes = e.detail.json.quotes
		.concat(e.detail.json.backlinks)
		.map(q => `${e.detail.json.board}-${q.postId}`)
		.filter(y => savedYous.has(y))
		.map(y => {
			const [board, postId] = y.split('-');
			return e.detail.post.querySelectorAll(`.quote[href^="/${board}/"][href$="#${postId}"]`)
		}).reduce((acc, array) => {
			return acc.concat(Array.from(array)); //handle duplicate of same quote
		}, []);
	//toggle for any quotes in a new post that quote (you)
	toggleQuotes(youHoverQuotes, yousEnabled);
	//if not a hover newpost, and enabled/for yous, send notification
	if (!e.detail.hover && notificationsEnabled && !isYou) {
		if (notificationYousOnly && !quotesYou) {
			return; //only send notif for (you) if setting
		}
		try {
			console.log('attempting to send notification', postYou);
			new Notification(`${quotesYou ? 'New quote in: ' : ''}${document.title}`, {
				body: postData.nomarkup ? postData.nomarkup.substring(0,100) : ''
			});
		} catch (e) { /* notification cant send for some reason -- user revoked perms in browser? */ }
	}
}

window.addEventListener('addPost', handleNewYous, false);
window.addEventListener('updatePostMessage', handleNewYous, false);

window.addEventListener('settingsReady', () => {

	yousList = document.getElementById('youslist-setting');
	yousList.value = [...savedYous];
	const yousListClearButton = document.getElementById('youslist-clear');
	const clearYousList = () => {
		if (yousEnabled) {
			toggleAll(false);
		}
		savedYous = new Set();
		yousList.value = '';
		setLocalStorage('yous', '[]');
		console.log('cleared yous');
	}
	yousListClearButton.addEventListener('click', clearYousList, false);

	const yousSetting = document.getElementById('yous-setting');
	const toggleYousSetting = () => {
		yousEnabled = !yousEnabled;
		setLocalStorage('yous-setting', yousEnabled);
		toggleAll(yousEnabled);
		console.log('toggling yous', yousEnabled);
	}
	yousSetting.checked = yousEnabled;
	yousSetting.addEventListener('change', toggleYousSetting, false);

	const notificationYousOnlySetting = document.getElementById('notification-yous-only');
	const toggleNotificationYousOnlySetting = () => {
		notificationYousOnly = !notificationYousOnly;
		setLocalStorage('notification-yous-only', notificationYousOnly);
		console.log('toggling notification only for yous', yousEnabled);
	}
	notificationYousOnlySetting.checked = notificationYousOnly;
	notificationYousOnlySetting.addEventListener('change', toggleNotificationYousOnlySetting, false);

	const notificationSetting = document.getElementById('notification-setting');
	const toggleNotifications = async () => {
		notificationsEnabled = !notificationsEnabled;
		if (notificationsEnabled) {
			const result = await Notification.requestPermission()
			if (result != 'granted') {
				//user denied permission popup
				notificationsEnabled = false;
				notificationSetting.checked = false;
				return;
			}
		}
		console.log('toggling notifications', notificationsEnabled);
		setLocalStorage('notifications', notificationsEnabled);
	}
	notificationSetting.checked = notificationsEnabled;
	notificationSetting.addEventListener('change', toggleNotifications, false);

});


code:
'use strict';

const gulp = require('gulp')
	, fs = require('fs-extra')
	, semver = require('semver')
	, formatSize = require(__dirname+'/helpers/files/formatsize.js')
	, uploadDirectory = require(__dirname+'/helpers/files/uploadDirectory.js')
	, configs = require(__dirname+'/configs/main.js')
	, { themes, codeThemes } = require(__dirname+'/helpers/themes.js')
	, commit = require(__dirname+'/helpers/commit.js')
	, less = require('gulp-less')
	, concat = require('gulp-concat')
	, cleanCSS = require('gulp-clean-css')
	, uglify = require('gulp-uglify-es').default
	, del = require('del')
	, pug = require('pug')
	, gulppug = require('gulp-pug')
	, { migrateVersion } = require(__dirname+'/package.json')
	, { randomBytes } = require('crypto')
	, paths = {
		styles: {
			src: 'gulp/res/css/',
			dest: 'static/css/'
		},
		images: {
			src: 'gulp/res/img/*',
			dest: 'static/file/'
		},
		icons: {
			src: 'gulp/res/icons/*',
			dest: 'static/file/'
		},
		scripts: {
			src: 'gulp/res/js',
			dest: 'static/js/'
		},
		pug: {
			src: 'views/',
			dest: 'static/html/'
		}
	};

async function wipe() {

	const Mongo = require(__dirname+'/db/db.js')
	const Redis = require(__dirname+'/redis.js')
	await Mongo.connect();
	const db = Mongo.db;

	//make these because mongo is dumb and doesnt make them automatically
	await db.createCollection('accounts');
	await db.createCollection('bans');
	await db.createCollection('boards');
	await db.createCollection('captcha');
	await db.createCollection('files');
	await db.createCollection('modlog');
	await db.createCollection('news');
	await db.createCollection('posts');
	await db.createCollection('poststats');
	await db.createCollection('ratelimit');
	await db.createCollection('webring');
	await db.createCollection('bypass');

	const { Webring, Boards, Posts, Captchas, Ratelimits, News,
		Accounts, Files, Stats, Modlogs, Bans, Bypass } = require(__dirname+'/db/');

	//wipe db shit
	await Promise.all([
		Redis.deletePattern('*'),
		Captchas.deleteAll(),
		Ratelimits.deleteAll(),
		Accounts.deleteAll(),
		Posts.deleteAll(),
		Boards.deleteAll(),
		Webring.deleteAll(),
		Bans.deleteAll(),
		Files.deleteAll(),
		Stats.deleteAll(),
		Modlogs.deleteAll(),
		Bypass.deleteAll(),
		News.deleteAll(),
	]);

	//add indexes - should profiled and changed at some point if necessary
	await Stats.db.createIndex({board:1, hour:1})
	await Boards.db.createIndex({ips: 1, pph:1, sequence_value:1})
	await Boards.db.createIndex({'settings.tags':1})
	await Boards.db.createIndex({lastPostTimestamp:1})
	await Webring.db.createIndex({uniqueUsers:1, postsPerHour:1, totalPosts:1})
	await Webring.db.createIndex({tags:1})
	await Webring.db.createIndex({lastPostTimestamp:1})
	await Bans.db.dropIndexes()
	await Captchas.db.dropIndexes()
	await Ratelimits.db.dropIndexes()
	await Posts.db.dropIndexes()
	await Modlogs.db.dropIndexes()
	await Modlogs.db.createIndex({ 'board': 1 })
	await Files.db.createIndex({ 'count': 1 })
	await Bans.db.createIndex({ 'ip.single': 1 , 'board': 1 })
	await Bans.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }) //custom expiry, i.e. it will expire when current date > than this date
	await Bypass.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 })
	await Captchas.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 300 }) //captchas valid for 5 minutes
	await Ratelimits.db.createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 60 }) //per minute captcha ratelimit
	await Posts.db.createIndex({ 'postId': 1,'board': 1,})
	await Posts.db.createIndex({ 'board': 1,	'thread': 1, 'bumped': -1 })
	await Posts.db.createIndex({ 'board': 1, 'reports.0': 1 }, { 'partialFilterExpression': { 'reports.0': { '$exists': true } } })
	await Posts.db.createIndex({ 'globalreports.0': 1 }, { 'partialFilterExpression': {	'globalreports.0': { '$exists': true } } })
	const randomPassword = randomBytes(20).toString('base64')
	await Accounts.insertOne('admin', 'admin', randomPassword, 0);
	console.log('\n\n=====LOGIN DETAILS=====\nusername: admin\npassword:', randomPassword, '\n=======================');

	await db.collection('version').replaceOne({
		'_id': 'version'
	}, {
		'_id': 'version',
		'version': migrateVersion
	}, {
		upsert: true
	});

	await Mongo.client.close();
	Redis.redisClient.quit();

	//delete all the static files
	return Promise.all([
		del([ 'static/html/*' ]),
		del([ 'static/json/*' ]),
		del([ 'static/banner/*' ]),
		del([ 'static/captcha/*' ]),
		del([ 'static/file/*' ]),
		del([ 'static/css/*' ]),
		fs.ensureDir(`${uploadDirectory}/captcha`),
	]);

}

//update the css file
async function css() {
	try {
		//a little more configurable
		let bypassHeight = configs.captchaOptions.type === 'google' ? 500
			: configs.captchaOptions.type === 'grid' ? 330
			: 235;
		let captchaHeight = configs.captchaOptions.type === 'text' ? 80
			: configs.captchaOptions.type === 'grid' ? configs.captchaOptions.grid.imageSize+30
			: 200; //'google' doesnt need this set
		let captchaWidth = configs.captchaOptions.type === 'text' ? 210
			: configs.captchaOptions.type === 'grid' ? configs.captchaOptions.grid.imageSize+30
			: 200; //'google' doesnt need this set
		const cssLocals = `:root {
    --attachment-img: url('/file/attachment.png');
    --spoiler-img: url('/file/spoiler.png');
    --audio-img: url('/file/audio.png');
    --thumbnail-size: ${configs.thumbSize}px;
    --captcha-w: ${captchaWidth}px;
    --captcha-h: ${captchaHeight}px;
    --bypass-height: ${bypassHeight}px;
}`;
		fs.writeFileSync('gulp/res/css/locals.css', cssLocals);
		fs.symlinkSync(__dirname+'/node_modules/highlight.js/styles', __dirname+'/gulp/res/css/codethemes', 'dir');
	} catch (e) {
		if (e.code !== 'EEXIST') {
			//already exists, ignore error
			console.log(e);
		}
	}
	await gulp.src([
			`${paths.styles.src}/themes/*.css`,
		])
		.pipe(less())
		.pipe(cleanCSS())
		.pipe(gulp.dest(`${paths.styles.dest}/themes/`));
	await gulp.src([
			`${paths.styles.src}/codethemes/*.css`,
		])
		.pipe(less())
		.pipe(cleanCSS())
		.pipe(gulp.dest(`${paths.styles.dest}/codethemes/`));
	await gulp.src([
			`${paths.styles.src}/locals.css`,
			`${paths.styles.src}/nscaptcha.css`,
		])
		.pipe(concat('nscaptcha.css'))
		.pipe(less())
		.pipe(cleanCSS())
		.pipe(gulp.dest(paths.styles.dest));
	return gulp.src([
			`${paths.styles.src}/locals.css`,
			`${paths.styles.src}/style.css`,
			`${paths.styles.src}/*.css`,
			`!${paths.styles.src}/nscaptcha.css`,
		])
		.pipe(concat('style.css'))
		.pipe(less())
		.pipe(cleanCSS())
		.pipe(gulp.dest(paths.styles.dest));
}

//spoiler/deleted image, default banner, spoiler/sticky/sage/cycle icons
function images() {
	return gulp.src(paths.images.src)
		.pipe(gulp.dest(paths.images.dest));
}

//favicon/safari/chrome/mstiles, etc
function icons() {
	return gulp.src(paths.icons.src)
		.pipe(gulp.dest(paths.icons.dest));
}

async function cache() {
	const Redis = require(__dirname+'/redis.js')
	await Promise.all([
		Redis.deletePattern('boards:listed'),
		Redis.deletePattern('board:*'),
		Redis.deletePattern('banners:*'),
		Redis.deletePattern('users:*'),
		Redis.deletePattern('blacklisted:*'),
	]);
	Redis.redisClient.quit();
}

function deletehtml() {
	return del([ 'static/html/*' ]);
}

function custompages() {
	return gulp.src([
		`${paths.pug.src}/custompages/*.pug`,
		`${paths.pug.src}/pages/404.pug`,
		`${paths.pug.src}/pages/500.pug`,
		`${paths.pug.src}/pages/502.pug`,
		`${paths.pug.src}/pages/503.pug`,
		`${paths.pug.src}/pages/504.pug`
	])
	.pipe(gulppug({
		locals: {
			meta: configs.meta,
			enableWebring: configs.enableWebring,
			globalLimits: configs.globalLimits,
			codeLanguages: configs.highlightOptions.languageSubset,
			defaultTheme: configs.boardDefaults.theme,
			defaultCodeTheme: configs.boardDefaults.codeTheme,
			postFilesSize: formatSize(configs.globalLimits.postFilesSize.max),
			captchaType: configs.captchaOptions.type,
			googleRecaptchaSiteKey: configs.captchaOptions.google.siteKey,
			captchaGridSize: configs.captchaOptions.grid.size,
			commit,
		}
	}))
	.pipe(gulp.dest(paths.pug.dest));
}

function scripts() {
	try {
		const locals = `const themes = ['${themes.join("', '")}'];
const codeThemes = ['${codeThemes.join("', '")}'];
const captchaType = '${configs.captchaOptions.type}';
const captchaGridSize = ${configs.captchaOptions.grid.size};
const SERVER_TIMEZONE = '${Intl.DateTimeFormat().resolvedOptions().timeZone}';
const settings = ${JSON.stringify(configs.frontendScriptDefault)};
`;
		fs.writeFileSync('gulp/res/js/locals.js', locals);
		fs.writeFileSync('gulp/res/js/post.js', pug.compileFileClient(`${paths.pug.src}/includes/post.pug`, { compileDebug: false, debug: false, name: 'post' }));
		fs.writeFileSync('gulp/res/js/modal.js', pug.compileFileClient(`${paths.pug.src}/includes/modal.pug`, { compileDebug: false, debug: false, name: 'modal' }));
		fs.writeFileSync('gulp/res/js/uploaditem.js', pug.compileFileClient(`${paths.pug.src}/includes/uploaditem.pug`, { compileDebug: false, debug: false, name: 'uploaditem' }));
		fs.writeFileSync('gulp/res/js/pugfilters.js', pug.compileFileClient(`${paths.pug.src}/includes/filters.pug`, { compileDebug: false, debug: false, name: 'filters' }));
		fs.writeFileSync('gulp/res/js/captchaformsection.js', pug.compileFileClient(`${paths.pug.src}/includes/captchaformsection.pug`, { compileDebug: false, debug: false, name: 'captchaformsection' }));
		fs.symlinkSync(__dirname+'/node_modules/socket.io-client/dist/socket.io.slim.js', __dirname+'/gulp/res/js/socket.io.js', 'file');
	} catch (e) {
		if (e.code !== 'EEXIST') {
			console.log(e);
		}
	}
	gulp.src([
			//put scripts in order for dependencies
			`${paths.scripts.src}/locals.js`,
			`${paths.scripts.src}/localstorage.js`,
			`${paths.scripts.src}/modal.js`,
			`${paths.scripts.src}/pugfilters.js`,
			`${paths.scripts.src}/post.js`,
			`${paths.scripts.src}/settings.js`,
			`${paths.scripts.src}/live.js`,
			`${paths.scripts.src}/captcha.js`,
			`${paths.scripts.src}/forms.js`,
			`${paths.scripts.src}/*.js`,
			`!${paths.scripts.src}/hidefileinput.js`,
			`!${paths.scripts.src}/dragable.js`,
			`!${paths.scripts.src}/filters.js`,
			`!${paths.scripts.src}/yous.js`,
			`!${paths.scripts.src}/catalog.js`,
			`!${paths.scripts.src}/time.js`,
			`!${paths.scripts.src}/themelist.js`, //dont include files from my fuck up with git. todo: make this a migration?
			`!${paths.scripts.src}/timezone.js`,
		])
		.pipe(concat('all.js'))
		.pipe(uglify({compress:false}))
		.pipe(gulp.dest(paths.scripts.dest));
	return gulp.src([
			`${paths.scripts.src}/hidefileinput.js`,
			`${paths.scripts.src}/dragable.js`,
			`${paths.scripts.src}/yous.js`,
			`${paths.scripts.src}/filters.js`,
			`${paths.scripts.src}/catalog.js`,
			`${paths.scripts.src}/time.js`,
		])
		.pipe(concat('render.js'))
		.pipe(uglify({compress:false}))
		.pipe(gulp.dest(paths.scripts.dest));
}

async function migrate() {

	const Mongo = require(__dirname+'/db/db.js')
	const Redis = require(__dirname+'/redis.js')
	await Mongo.connect();
	const db = Mongo.db;

	//get current version from db if present (set in 'reset' task in recent versions)
	let currentVersion = await db.collection('version').findOne({
		'_id': 'version'
	}).then(res => res ? res.version : '0.0.0'); // 0.0.0 for old versions

	if (semver.lt(currentVersion, migrateVersion)) {
		console.log(`Current version: ${currentVersion}`);
		const migrations = require(__dirname+'/migrations/');
		const migrationVersions = Object.keys(migrations)
			.sort(semver.compare)
			.filter(v => semver.gt(v, currentVersion));
		console.log(`Migrations needed: ${currentVersion} -> ${migrationVersions.join(' -> ')}`);
		for (let ver of migrationVersions) {
			console.log(`=====\nStarting migration to version ${ver}`);
			try {
				await migrations[ver](db, Redis);
				await db.collection('version').replaceOne({
					'_id': 'version'
				}, {
					'_id': 'version',
					'version': ver
				}, {
					upsert: true
				});
			} catch (e) {
				console.error(e);
				console.warn(`Migration to ${ver} encountered an error`);
			}
			console.log(`Finished migrating to version ${ver}`);
		}
	} else {
		console.log(`Migration not required, you are already on the current version (${migrateVersion})`)
	}

	await Mongo.client.close();
	Redis.redisClient.quit();

}

const build = gulp.parallel(gulp.series(scripts, css), images, icons, gulp.series(deletehtml, custompages));
const reset = gulp.series(wipe, build);
const html = gulp.series(deletehtml, custompages);

module.exports = {
	html,
	css,
	images,
	icons,
	reset,
	custompages,
	scripts,
	wipe,
	cache,
	migrate,
	default: build,
};


code:
'use strict';

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

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

	//get a map of boards to threads affected
	const boardThreadMap = {};
	for (let i = 0; i < posts.length; i++) {
		const post = posts[i];
		if (!boardThreadMap[post.board]) {
			boardThreadMap[post.board] = {
				'directThreads': new Set(),
				'threads': new Set()
			};
		}
		if (!post.thread) {
			//a thread was directly selected on this board, not just posts. so we handle deletes differently
			boardThreadMap[post.board].directThreads.add(post.postId);
		}
		const threadId = post.thread || post.postId;
		boardThreadMap[post.board].threads.add(threadId);
	}

	const beforePages = {};
	const threadBoards = Object.keys(boardThreadMap);
	//get number of pages for each before actions for deleting old pages and changing page nav numbers incase number of pages changes
	if (deleting) {
		await Promise.all(threadBoards.map(async board => {
			beforePages[board] = Math.ceil((await Posts.getPages(board)) / 10);
		}));
	}

	return { boardThreadMap, beforePages, threadBoards };

}


code:
'use strict';

const gm = require('gm').subClass({ imageMagick: true })
	, { promisify } = require('util')
	, randomBytes = promisify(require('crypto').randomBytes)
	, { Captchas } = require(__dirname+'/../../../db/')
	, { captchaOptions } = require(__dirname+'/../../../configs/main.js')
	, uploadDirectory = require(__dirname+'/../../files/uploadDirectory.js')
	, randomRange = require(__dirname+'/../../randomrange.js')
	, padding = 30
	, width = captchaOptions.grid.imageSize+padding
	, height = captchaOptions.grid.imageSize+padding
	, gridSize = captchaOptions.grid.size
	, zeros = ['○','□','♘','♢','▽','△','♖','✧','♔','♘','♕','♗','♙','♧']
	, ones = ['●','■','♞','♦','▼','▲','♜','✦','♚','♞','♛','♝','♟','♣']
	, colors = ['#FF8080', '#80FF80', '#8080FF', '#FF80FF', '#FFFF80', '#80FFFF']

module.exports = async () => {
	//number of inputs in grid
	const numInputs = gridSize**2;
	//random buffer to get true/false for grid from
	const randBuffer = await randomBytes(numInputs);
	//array of true/false, for each grid input
	const boolArray = Array.from(randBuffer).map(x => x < 80);
	if (!boolArray.some(b => b === true)) {
		boolArray[(await randomRange(0,numInputs-1))] = true;
	}

	const captchaId = await Captchas.insertOne(boolArray).then(r => r.insertedId);

	const distorts = [];
	if (captchaOptions.distortion > 0) {
		const numDistorts = await randomRange(captchaOptions.numDistorts.min,captchaOptions.numDistorts.max);
		const div = width/numDistorts;
		for (let i = 0; i < numDistorts; i++) {
			const divStart = (div*i)
				, divEnd = (div*(i+1));
			const originx = await randomRange(divStart, divEnd)
				, originy = await randomRange(0,height);
			const destx = await randomRange(Math.max(captchaOptions.distortion,originx-captchaOptions.distortion),Math.min(width-captchaOptions.distortion,originx+captchaOptions.distortion)).catch(e => console.error(e))
				, desty = await randomRange(Math.max(captchaOptions.distortion,originy-captchaOptions.distortion*2),Math.min(height-captchaOptions.distortion,originy+captchaOptions.distortion*2)).catch(e => console.error(e));
			distorts.push([
				{x:originx,y:originy},
				{x:destx,y:desty}
			]);
		}
	}

	const captcha = gm(width,height,'#ffffff')
	.fill('#000000')
	.font(__dirname+'/../font.ttf');

	const spaceSize = (width-padding)/gridSize;
	for(let j = 0; j < gridSize; j++) {
		let cxOffset = await randomRange(0, spaceSize*1.5);
		for(let i = 0; i < gridSize; i++) {
			const index = (j*gridSize)+i;
			const cyOffset = await randomRange(0, captchaOptions.grid.iconYOffset);
			const charIndex = await randomRange(0, ones.length-1);
			const character = (boolArray[index] ? ones : zeros)[charIndex];
			captcha.fontSize((await randomRange(20,30)))
			captcha.drawText(
				spaceSize*(i)+cxOffset,
				spaceSize*(j+1)+cyOffset,
				character
			);
		}
	}

	if (captchaOptions.distortion > 0) {
		captcha.distort(distorts, 'Shepards');
	}

	captcha
	.edge(25);

	return new Promise((resolve, reject) => {
		captcha
		.write(`${uploadDirectory}/captcha/${captchaId}.jpg`, (err) => {
			if (err) {
				return reject(err);
			}
			return resolve({ captchaId });
		});
	});

}


code:
'use strict';

const gm = require('gm').subClass({ imageMagick: true })
	, { Captchas } = require(__dirname+'/../../../db/')
	, { captchaOptions } = require(__dirname+'/../../../configs/main.js')
	, uploadDirectory = require(__dirname+'/../../files/uploadDirectory.js')
	, characterWidth = (char) => {
		switch (char) {
			case 'w':
			case 'm':
				return 45;
			case 'i':
			case 'l':
				return 12;
			case 'f':
			case 'j':
			case 't':
				return 15;
			default:
				return 30;
		}
	}
	, totalWidth = (text) => {
		return text.split('').reduce((acc, char) => {
			return characterWidth(char) + acc + 1;
		}, 0);
	}
	, width = 210
	, height = 80
	, distortion = captchaOptions.distortion
	, minVal = parseInt('1000000', 36)
	, maxVal = parseInt('1zzzzzz', 36)
	, randomRange = require(__dirname+'/../../randomrange.js');

module.exports = async () => {
	// generate between 1000000 and 1zzzzzz and not 0 and zzzzzz, so toString
	// will have enough characters
	const textInt = await randomRange(minVal, maxVal);
	const text = textInt.toString(36).substr(-6, 6);
	const captchaId = await Captchas.insertOne(text).then(r => r.insertedId);
	const distorts = [];
	const numDistorts = await randomRange(
		captchaOptions.numDistorts.min,captchaOptions.numDistorts.max);
	const div = width/numDistorts;

	for (let i = 0; i < numDistorts; i++) {
		const divStart = (div*i)
			, divEnd = (div*(i+1));
		const originx = await randomRange(divStart, divEnd)
			, originy = await randomRange(0,height);
		const destx = await randomRange(Math.max(distortion,originx-distortion),Math.min(width-distortion,originx+distortion))
			, desty = await randomRange(Math.max(distortion,originy-distortion*2),Math.min(height-distortion,originy+distortion*2));
		distorts.push([
			{x:originx,y:originy}, //origin
			{x:destx,y:desty} //dest
		]);

	}

	const lineY = await randomRange(35,45);
	return new Promise((resolve, reject) => {
		const captcha = gm(width,height,'#ffffff')
		.fill('#000000')
		.fontSize(65);
		const startX = (width-totalWidth(text))/2;
		let charX = startX;
		for (let i = 0; i < 6; i++) {
			captcha.drawText(charX, 60, text[i]);
			charX += characterWidth(text[i]);
		}
		captcha
		.drawRectangle(startX, lineY, charX, lineY+4)
		.distort(distorts, 'Shepards')
		.paint(2)
		.write(`${uploadDirectory}/captcha/${captchaId}.jpg`, (err) => {
			if (err) {
				return reject(err);
			}
			return resolve({ captchaId });
		});
	});

}


code:
'use strict';

const { Ratelimits } = require(__dirname+'/../../db/')
	, { ObjectId } = require(__dirname+'/../../db/db.js')
	, checkCaptcha = require(__dirname+'/../checks/captcha.js')
	, { captchaOptions } = require(__dirname+'/../../configs/main.js')
	, remove = require('fs-extra').remove
	, dynamicResponse = require(__dirname+'/../dynamic.js')
	, deleteTempFiles = require(__dirname+'/../files/deletetempfiles.js')
	, uploadDirectory = require(__dirname+'/../files/uploadDirectory.js');

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

	//already solved in pre stage for getting bypassID for "ip"
	if (res.locals.tor && res.locals.solvedCaptcha) {
		return next();
	}

	//skip captcha if disabled on board for posts only
	if (res.locals.board
		&& req.path === `/board/${res.locals.board._id}/post`) {
		if (res.locals.board.settings.captchaMode === 0 //if captcha disabled
			|| (res.locals.board.settings.captchaMode === 1 && req.body.thread)) { //or if enabled for threads, and not a thread
			return next(); //then skip checking captcha
		}
	}

	const captchaInput = req.body.captcha || req.body['g-recaptcha-response'] || req.body["h-captcha-response"];
	const captchaId = req.cookies.captchaid;
	try {
		await checkCaptcha(captchaInput, captchaId);
	} catch (err) {
		deleteTempFiles(req).catch(e => console.error);
		if (err instanceof Error) {
			return next(err);
		}
		const page = (req.body.minimal || req.path === '/blockbypass' ? 'bypass' : 'message');
		return dynamicResponse(req, res, 403, page, {
			'title': 'Forbidden',
			'message': err,
			'redirect': req.headers.referer,
		});
	}

	//it was correct, so mark as solved for other middleware
	res.locals.solvedCaptcha = true;

	if (captchaOptions.type !== 'google') {
		//for builtin captchas, clear captchaid cookie, delete file and reset quota
		res.clearCookie('captchaid');
		await Promise.all([
			!res.locals.tor && Ratelimits.resetQuota(res.locals.ip.single, 'captcha'),
			remove(`${uploadDirectory}/captcha/${captchaId}.jpg`)
		]);
	}

	//completed captcha successfully, continue
	return next();

}


code:
'use strict';

const actions = [
	{name:'unlink_file', global:true, auth:4, passwords:true, build:true},
	{name:'delete_file', global:true, auth:1, passwords:false, build:true},
	{name:'spoiler', global:true, auth:4, passwords:true, build:true},
	{name:'edit', global:true, auth:3, passwords:false, build:true},
	{name:'delete', global:true, auth:4, passwords:true, build:true},
	{name:'lock', global:false, auth:3, passwords:false, build:true},
	{name:'sticky', global:false, auth:3, passwords:false, build:true},
	{name:'cyclic', global:false, auth:3, passwords:false, build:true},
	{name:'bumplock', global:false, auth:3, passwords:false, build:true},
	{name:'report', global:false, auth:4, passwords:false, build:false},
	{name:'global_report', global:true, auth:4, passwords:false, build:false},
	{name:'move', global:false, auth:3, passwords:false, build:true},
	{name:'delete_ip_board', global:true, auth:3, passwords:false, build:true},
	{name:'delete_ip_thread', global:true, auth:3, passwords:false, build:true},
	{name:'delete_ip_global', global:true, auth:1, passwords:false, build:true},
	{name:'dismiss', global:false, auth:3, passwords:false, build:false},
	{name:'global_dismiss', global:true, auth:1, passwords:false, build:false},
	{name:'report_ban', global:false, auth:3, passwords:false, build:false},
	{name:'global_report_ban', global:true, auth:1, passwords:false, build:false},
	{name:'ban', global:false, auth:3, passwords:false, build:false},//will build if there is a ban message
	{name:'global_ban', global:true, auth:1, passwords:false, build:false},//will build if there is a ban message
];

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

	let numGlobal = 0
		, authRequired = 4
		, numPasswords = 0
		, numBuild = 0
		, validActions = [];

	for (let i = 0; i < actions.length; i++) {
		const action = actions[i];
		const bodyHasAction = req.body[action.name];
		if (bodyHasAction) {
			validActions.push(action.name);
			if (action.global) {
				numGlobal++;
			}
			if (action.auth && action.auth < authRequired) {
				authRequired = action.auth;
			}
			if (action.passwords) {
				numPasswords++;
			}
			if (action.build) {
				numBuild++
			}
		}
	}

	return { numGlobal, authRequired, validActions, numPasswords, numBuild };

}


code:
'use strict';

//literally just alphanumeric ¯\_(ツ)_/¯
module.exports = /^[a-zA-Z0-9]+$/


code:
'use strict';

const { Bans } = require(__dirname+'/../../db/')
	, hasPerms = require(__dirname+'/hasperms.js')
	, dynamicResponse = require(__dirname+'/../dynamic.js');

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

	if (res.locals.permLevel > 1) {//global staff or admin bypass
		const bans = await Bans.find(res.locals.ip, res.locals.board ? res.locals.board._id : null);
		if (bans && bans.length > 0) {
			const globalBans = bans.filter(ban => { return ban.board === null });
			if (globalBans.length > 0 || (res.locals.permLevel >= 4 && globalBans.length !== bans.length)) {
				//board staff bypass bans on their own board, but not global bans
				const unseenBans = bans.filter(b => !b.seen).map(b => b._id);
				await Bans.markSeen(unseenBans); //mark bans as seen
				bans.forEach(ban => ban.seen = true); //mark seen as true in memory for user viewed ban page
				return res.status(403).render('ban', {
					bans: bans,
				});
			}
		}
	}
	next();

}


code:
'use strict';

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

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

	if (res.locals.preFetchedBypassId || !blockBypass.enabled && !res.locals.tor) { //for now, tor MUST solve a bypass
		return next();
	}

	//check if blockbypass exists and right length
	const bypassId = req.signedCookies.bypassid;
	if (!res.locals.solvedCaptcha && (!bypassId || bypassId.length !== 24)) {
		deleteTempFiles(req).catch(e => console.error);
		return dynamicResponse(req, res, 403, 'message', {
			'title': 'Forbidden',
			'message': 'Please complete a block bypass to continue',
			'frame': '/bypass_minimal.html',
			'link': {
				'href': '/bypass.html',
				'text': 'Get block bypass',
			},
		});
	}

	//try to get bypass from db and make sure uses < maxUses
	let bypass;
	if (bypassId && bypassId.length === 24) {
		try {
			const bypassMongoId = ObjectId(bypassId);
			bypass = await Bypass.checkBypass(bypassMongoId);
			res.locals.blockBypass = bypass;
		} catch (err) {
			return next(err);
		}
	}

	if (bypass && bypass.uses < blockBypass.expireAfterUses) {
		return next();
	}

	if (res.locals.solvedCaptcha) {
		//they dont have a valid bypass, but just solved board captcha, so give them a new one
		const newBypass = await Bypass.getBypass();
		const newBypassId = newBypass.insertedId;
		res.locals.blockBypass = newBypass.ops[0];
		res.cookie('bypassid', newBypassId.toString(), {
			'maxAge': blockBypass.expireAfterTime,
			'secure': production && secureCookies && (req.headers['x-forwarded-proto'] === 'https'),
			'sameSite': 'strict',
			'signed': true
		});
		return next();
	}

	deleteTempFiles(req).catch(e => console.error);
	return dynamicResponse(req, res, 403, 'message', {
		'title': 'Forbidden',
		'message': 'Block bypass expired or exceeded max uses',
		'frame': '/bypass_minimal.html',
		'link': {
			'href': '/bypass.html',
			'text': 'Get block bypass',
		},
	});

}


code:
'use strict';

const hasPerms = require(__dirname+'/hasperms.js');

module.exports = (req, res, next) => {
	res.locals.permLevel = hasPerms(req, res);
	next();
}


code:
'use strict';

const { Captchas } = require(__dirname+'/../../db/')
    , { ObjectId } = require(__dirname+'/../../db/db.js')
    , { captchaOptions } = require(__dirname+'/../../configs/main.js')
	, FormData = require('form-data')
	, fetch = require('node-fetch')
	, { timingSafeEqual } = require('crypto')

module.exports = async (captchaInput, captchaId) => {

	//check if captcha field in form is valid
	if (!captchaInput
		|| (captchaInput.length !== 6 && !captchaOptions.type === 'text')) {
		throw 'Incorrect captcha answer';
	}

	//make sure they have captcha cookie and its 24 chars
	if (captchaOptions.type !== 'google'
		&& (!captchaId || captchaId.length !== 24)) {
		throw 'Captcha expired';
	}

	captchaInput = Array.isArray(captchaInput) ? captchaInput : [captchaInput];

	switch (captchaOptions.type) {
		case 'grid': //grid captcha
			const gridCaptchaMongoId = ObjectId(captchaId);
			const normalisedAnswer = new Array(captchaOptions.grid.size**2).fill(false);
			captchaInput.forEach(num => {
				normalisedAnswer[+num] = true;
			});
			let gridCaptcha = await Captchas.findOneAndDelete(gridCaptchaMongoId, normalisedAnswer);
			if (!gridCaptcha || !gridCaptcha.value
				|| !timingSafeEqual(
					Buffer.from(gridCaptcha.value.answer.join(',')),
					Buffer.from(normalisedAnswer.join(','))
				)
			) {
				throw 'Incorrect captcha answer';
			}
			break;
		case 'text': //text captcha
			const textCaptchaMongoId = ObjectId(captchaId);
			let textCaptcha = await Captchas.findOneAndDelete(textCaptchaMongoId, captchaInput[0]);
			if (!textCaptcha || !textCaptcha.value
				|| !timingSafeEqual(
					Buffer.from(textCaptcha.value.answer),
					Buffer.from(captchaInput[0])
				)
			) {
				throw 'Incorrect captcha answer';
			}
			break;
		case 'google': //google captcha
			let recaptchaResponse;
			try {
				const form = new FormData();
				form.append('secret', captchaOptions.google.secretKey);
				form.append('response', captchaInput[0]);
				recaptchaResponse = await fetch('https://www.google.com/recaptcha/api/siteverify', {
					method: 'POST',
					body: form,
				}).then(res => res.json());
			} catch (e) {
				throw 'Captcha error occurred';
			}
			if (!recaptchaResponse || !recaptchaResponse.success) {
				throw 'Incorrect captcha answer';
			}
			break;
		case 'hcaptcha':
			let hcaptchaResponse;
			try {
				const form = new FormData();
				form.append('secret', captchaOptions.hcaptcha.secretKey);
				form.append('sitekey', captchaOptions.hcaptcha.siteKey);
				form.append('response', captchaInput[0]);
				hcaptchaResponse = await fetch('https://hcaptcha.com/siteverify', {
					method: 'POST',
					body: form,
				}).then(res => res.json());
			} catch (e) {
				throw 'Captcha error occurred';
			}
			if (!hcaptchaResponse || !hcaptchaResponse.success) {
				throw 'Incorrect captcha answer';
			}
			break;
		default:
			throw 'Captcha config error';
			break;
	}

	return true;

}


code:
'use strict';

const csrf = require('csurf')();

module.exports = csrf;


code:
'use strict';

const cache = require(__dirname+'/../../redis.js')
	, dynamicResponse = require(__dirname+'/../dynamic.js')
	, deleteTempFiles = require(__dirname+'/../files/deletetempfiles.js')
	, { ipHeader, dnsbl, blockBypass } = require(__dirname+'/../../configs/main.js')
	, { batch } = require('dnsbl');

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

	if (dnsbl.enabled && dnsbl.blacklists.length > 0 //if dnsbl enabled and has more than 0 blacklists
		&& !res.locals.tor //tor cant be dnsbl'd
		&& (!res.locals.blockBypass || !blockBypass.bypassDnsbl)) { //and there is no valid block bypass, or they do not bypass dnsbl
		const ip = req.headers[ipHeader] || req.connection.remoteAddress;
		let isBlacklisted = await cache.get(`blacklisted:${ip}`);
		if (isBlacklisted === null) { //not cached
			const dnsblResp = await batch(ip, dnsbl.blacklists);
			isBlacklisted = dnsblResp.some(r => r.listed === true);
			await cache.set(`blacklisted:${ip}`, isBlacklisted, dnsbl.cacheTime);
		}
		if (isBlacklisted) {
			deleteTempFiles(req).catch(e => console.error);
			return dynamicResponse(req, res, 403, 'message', {
				'title': 'Forbidden',
				'message': 'Your IP address is listed on a blacklist',
				'redirect': req.headers.referer || '/'
			});
		}
	}
	return next();

}


code:
'use strict';

module.exports = (req, res) => {
	if (req.session) {
		const { user } = res.locals;
		if (user != null) {
			if (user.authLevel < 4) { //assigned levels
				return user.authLevel;
			}
			if (res.locals.board != null) {
				if (res.locals.board.owner === user.username) {
					return 2; //board owner 2
				} else if (res.locals.board.settings.moderators.includes(user.username) === true) {
					return 3; //board staff 3
				}
			}
		}
	}
	return 4; //not logged in, not staff or moderator
}


code:
'use strict';

const cache = {};

module.exports = (requiredLevel) => {

	return cache[requiredLevel] || (cache[requiredLevel] = function(req, res, next) {
		if (res.locals.permLevel > requiredLevel) {
			return res.status(403).render('message', {
				'title': 'Forbidden',
				'message': 'No Permission',
				'redirect': req.headers.referer || '/'
			});
		}
		next();
	});

}


code:
'use strict';

module.exports = async (req, res, next) => {
	if (res.locals.user) {
		return next();
	}
	let goto;
	if (req.method === 'GET' && req.path) {
		//coming from a GET page isLoggedIn middleware check
		goto = encodeURIComponent(req.path);
	}
	return res.redirect(`/login.html${goto ? '?goto='+goto : ''}`);
}


code:
'use strict';

const Mongo = require(__dirname+'/../../db/db.js')
	, { Posts } = require(__dirname+'/../../db/')
	, timeUtils = require(__dirname+'/../timeutils.js')
	, { sameContentSameIp, sameContentAnyIp, anyContentSameIp } = require(__dirname+'/../../configs/main.js').floodTimers;

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

	if (res.locals.permLevel <= 1) { //global staff bypass spam check
		return false;
	}

	if (sameContentSameIp === 0
		&& sameContentAnyIp === 0
		&& anyContentSameIp === 0) {
		return false;
	}

	const now = Date.now();
	const ors = [];
	const contentOr = [];
	if (res.locals.numFiles > 0) {
		contentOr.push({
			'files': {
				'$elemMatch': {
					'hash': { //any file hash will match, doesnt need to be all
						'$in': req.files.file.map(f => f.sha256)
					}
				}
			}
		});
	}
	if (req.body.message) {
		contentOr.push({
			'nomarkup':  req.body.message
		})
	}

	if (sameContentAnyIp) {
		//matching content from any IP
		const sameContentAnyIpMongoId = Mongo.ObjectId.createFromTime(Math.floor((now - sameContentAnyIp)/1000));
		ors.push({
			'_id': {
				'$gt': sameContentAnyIpMongoId
			},
			'$or': contentOr
		});
	}

	if (sameContentSameIp > 0) {
		//matching content from same IP
		const sameContentSameIpMongoId = Mongo.ObjectId.createFromTime(Math.floor((now - sameContentSameIp)/1000));
		ors.push({
			'_id': {
				'$gt': sameContentSameIpMongoId
			},
			'ip.single': res.locals.ip.single,
			'$or': contentOr
		});
	}

	if (anyContentSameIp > 0) {
		//any posts from same IP
		const anyContentSameIpMongoId = Mongo.ObjectId.createFromTime(Math.floor((now - anyContentSameIp)/1000));
		ors.push({
			'_id': {
				'$gt': anyContentSameIpMongoId
			},
			'ip.single': res.locals.ip.single
		})
	}

	let flood = await Posts.db.find({
		'$or': ors
	}).toArray();

	return flood.length > 0;

}


code:
'use strict';

const { Bypass } = require(__dirname+'/../../db/')
	, { ObjectId } = require(__dirname+'/../../db/db.js')
	, { secureCookies, blockBypass } = require(__dirname+'/../../configs/main.js')
	, checkCaptcha = require(__dirname+'/../checks/captcha.js')
	, remove = require('fs-extra').remove
	, uploadDirectory = require(__dirname+'/../files/uploadDirectory.js')
	, dynamicResponse = require(__dirname+'/../dynamic.js')
	, deleteTempFiles = require(__dirname+'/../files/deletetempfiles.js')
	, production = process.env.NODE_ENV === 'production';

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

	//early byapss is only needed for tor users
	if (!res.locals.tor) {
		return next();
	}

	const input = req.body.captcha;
	const captchaId = req.cookies.captchaid;
	let bypassId = req.signedCookies.bypassid;
	if (input && !bypassId) {
		// try to get the captcha from the DB
		try {
			await checkCaptcha(input, captchaId);
		} catch (err) {
			deleteTempFiles(req).catch(e => console.error);
			if (err instanceof Error) {
				return next(err);
			}
			const page = (req.body.minimal || req.path === '/blockbypass' ? 'bypass' : 'message');
			return dynamicResponse(req, res, 403, page, {
				'title': 'Forbidden',
				'message': err,
				'redirect': req.headers.referer,
			});
		}
		res.locals.solvedCaptcha = true;
		res.clearCookie('captchaid');
		remove(`${uploadDirectory}/captcha/${captchaId}.jpg`).catch(e => { console.error(e) });
	}

	if (res.locals.solvedCaptcha) {
		//they dont have a valid bypass, but just solved a captcha, so give them a new one
		const newBypass = await Bypass.getBypass();
		const newBypassId = newBypass.insertedId;
		bypassId = newBypassId.toString();
		res.locals.preFetchedBypassId = bypassId;
		res.locals.blockBypass = newBypass.ops[0];
		res.cookie('bypassid', newBypassId.toString(), {
			'maxAge': blockBypass.expireAfterTime,
			'secure': production && secureCookies && (req.headers['x-forwarded-proto'] === 'https'),
			'sameSite': 'strict',
			'signed': true
		});
		return next();
	}

	//check if blockbypass exists and right length
	if (!bypassId || bypassId.length !== 24) {
		res.clearCookie('bypassid');
		deleteTempFiles(req).catch(e => console.error);
		return dynamicResponse(req, res, 403, 'message', {
			'title': 'Forbidden',
			'message': 'Please complete a block bypass to continue',
			'frame': '/bypass_minimal.html',
			'link': {
				'href': '/bypass.html',
				'text': 'Get block bypass',
			},
		});
	}

	return next();

}


code:
'use strict';

module.exports = require('child_process')
	.execSync('git rev-parse --short HEAD')
	.toString()
	.trim();


code:
'use strict';

const countries = require('i18n-iso-countries')
	, countryNamesMap = countries.getNames('en')
	, countryCodes = ['XX', 'T1', 'TOR']
		.concat(Object.keys(countryNamesMap));

//this dumb library conveniently includes 2 names for some countries...
Object.entries(countryNamesMap)
	.filter(e => Array.isArray(e[1])) //for any country with an array of names,
	.forEach(c => countryNamesMap[c[0]] = c[1][0]) //use the first name

countryNamesMap['XX'] = 'Unknown';
countryNamesMap['T1'] = 'Tor Exit Node';
countryNamesMap['TOR'] = 'Tor Onion';

module.exports = {
	countryNamesMap,
	countryCodes,
}


code:
'use strict';

//https://stackoverflow.com/a/4413721
module.exports = (startDate, stopDate) => {
	const dateArray = new Array();
    let currentDate = startDate;
    while (currentDate <= stopDate) {
        dateArray.push(new Date (currentDate.valueOf()));
		currentDate.setDate(currentDate.getDate() + 1);
    }
    return dateArray;
}


code:
'use strict';

const escapeRegExp = require(__dirname+'/escaperegexp.js')
	, { isIP } = require('net')
	, { ipHashPermLevel } = require(__dirname+'/../configs/main.js')

module.exports = (query, permLevel) => {
	if (query.ip && typeof query.ip === 'string') {
		const decoded = decodeURIComponent(query.ip);
		if (permLevel <= ipHashPermLevel && (isIP(decoded) || decoded.match(/[a-z0-9]{24}/i))) { //if perms to view raw ip or bypass, allow querying
			return decoded;
		} else if (decoded.length === 10) { //otherwise, only allow last 10 char substring
			return new RegExp(`${escapeRegExp(decoded)}$`);
		}
	}
	return null; //else, no ip filter
}


code:
'use strict';

module.exports = (func, interval, runFirst) => {
	if (runFirst) {
		func();
	}
	setInterval(func, interval);
}


code:
'use strict';

module.exports = (req, res, code, page, data) => {
	res.status(code);
	if (req.body.minimal) {
		data.minimal = true;
	}
	if (req.headers['x-using-xhr'] != null) {
		//if sending header with js, and not a bypass_minimal page, show modal
		return res.json(data);
	} else {
		return res.render(page, data);
	}
}


code:
'use strict';

module.exports = (string) => {
	return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}


code:
'use strict';

const { globalLimits, debugLogs, filterFileNames, spaceFileNameReplacement } = require(__dirname+'/../configs/main.js')
	, dynamicResponse = require(__dirname+'/dynamic.js')
	, uploadLimitFunction = (req, res, next) => {
		return dynamicResponse(req, res, 413, 'message', {
			'title': 'Payload Too Large',
			'message': 'Your upload was too large',
			'redirect': req.headers.referer
		});
	}
	, numFilesUploadLimitFunction = (req, res, next) => {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Too many files',
			'message': res.locals.board ? `Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is ${res.locals.board.settings.maxFiles}`
				: `Max files per request is ${globalLimits.postFiles.max}`, //because of difference in TOR body parsing, we dont populate res.locals.board at this point. something to address later.
			'redirect': req.headers.referer
		});
	}
	, numBannersUploadLimitFunction = (req, res, next) => {
		return dynamicResponse(req, res, 400, 'message', {
			'title': 'Too many files',
			'message': `Max banners per request is ${globalLimits.bannerFiles.max}`,
			'redirect': req.headers.referer
		});
	}
	, upload = require('express-fileupload')
	, postFiles = upload({
		debug: debugLogs,
		createParentPath: true,
		safeFileNames: filterFileNames,
		spaceFileNameReplacement,
		preserveExtension: 4,
		limits: {
			totalSize: globalLimits.postFilesSize.max,
			fileSize: globalLimits.postFilesSize.max,
			files: globalLimits.postFiles.max
		},
		numFilesLimitHandler: numFilesUploadLimitFunction,
		limitHandler: uploadLimitFunction,
		useTempFiles: true,
		tempFileDir: __dirname+'/../tmp/'
	});

module.exports = {

	handleBannerFiles: upload({
		debug: debugLogs,
		createParentPath: true,
		safeFileNames: filterFileNames,
		spaceFileNameReplacement,
		preserveExtension: 4,
		limits: {
			totalSize: globalLimits.bannerFilesSize.max,
			fileSize: globalLimits.bannerFilesSize.max,
			files: globalLimits.bannerFiles.max
		},
		numFilesLimitHandler: numBannersUploadLimitFunction,
		limitHandler: uploadLimitFunction,
		useTempFiles: true,
		tempFileDir: __dirname+'/../tmp/'
	}),

	handlePostFilesEarlyTor:  (req, res, next) => {
		if (res.locals.tor) {
			return postFiles(req, res, next);
		}
		return next();
	},

	handlePostFiles: (req, res, next) => {
		if (res.locals.tor) {
			return next();
		}
		return postFiles(req, res, next);
	},

}


code:
const ffmpeg = require('fluent-ffmpeg')
	, { thumbSize } = require(__dirname+'/../../configs/main.js')
	, uploadDirectory = require(__dirname+'/uploadDirectory.js');

module.exports = (file) => {

	return new Promise((resolve, reject) => {
		ffmpeg(`${uploadDirectory}/file/${file.filename}`)
		.on('end', () => {
			return resolve();
		})
		.on('error', function(err, stdout, stderr) {
			return reject(err);
		})
		.complexFilter([{
			filter: 'showwavespic',
			options: { split_channels: 1, s: `${thumbSize}x${thumbSize}` }
		}])
		.save(`${uploadDirectory}/file/thumb-${file.hash}${file.thumbextension}`);
	});

};


code:
'use strict';

const { remove } = require('fs-extra')
	, uploadDirectory = require(__dirname+'/uploadDirectory.js');

module.exports = async (filenames, folder) => {

	await Promise.all(filenames.map(async filename => {
		remove(`${uploadDirectory}/${folder}/${filename}`)
	}));

}


code:
'use strict';

const { stat, remove, readdir } = require('fs-extra')
    , uploadDirectory = require(__dirname+'/uploadDirectory.js');

//takes directory name and timestamp to delete files older than
module.exports = async (directory, olderThan) => {
	const dir = `${uploadDirectory}/${directory}`
	const files = await readdir(dir);
	if (files.length > 0) {
		return Promise.all(files.map(async file => {
			const filePath = `${dir}/${file}`;
			try {
				const stats = await stat(filePath);
				const expiry = new Date(olderThan).getTime();
				if (stats.ctime.getTime() < expiry) {
					await remove(filePath);
				}
			} catch (e) {
				console.error(e);
			}
		}));
	}
}


code:
'use strict';

const { remove } = require('fs-extra')
	, uploadDirectory = require(__dirname+'/uploadDirectory.js')

module.exports = (files) => {

	//delete all the files and thumbs
	return Promise.all(files.map(async file => {
		return Promise.all([
			remove(`${uploadDirectory}/file/${file.filename}`),
			file.hasThumb ? remove(`${uploadDirectory}/file/thumb-${file.hash}${file.thumbextension}`) : void 0,
		]).catch(e => console.error);
	}));

}


code:
'use strict';

const { remove } = require('fs-extra')

module.exports = async (req) => {

	if (req.files != null) {
		let files = [];
		const keys = Object.keys(req.files);
		for (let i = 0; i < keys.length; i++) {
			const val = req.files[keys[i]];
			if (Array.isArray(val)) {
				files = files.concat(val);
			} else {
				files.push(val);
			}
		}
		return Promise.all(files.map(async file => {
			remove(file.tempFilePath);
		}));
	}

}


code:
const ffmpeg = require('fluent-ffmpeg')
	, configs = require(__dirname+'/../../configs/main.js')
	, uploadDirectory = require(__dirname+'/uploadDirectory.js');

module.exports = (filename, folder, temp) => {

	return new Promise((resolve, reject) => {
		ffmpeg.ffprobe(temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`, (err, metadata) => {
			if (err) {
				return reject(err)
			}
			return resolve(metadata);
		});
	});

};


code:
'use strict';

module.exports = (file) => {
	//handle gifs with multiple geometry and size
	if (Array.isArray(file.geometry)) {
		file.geometry = file.geometry[0];
	}
	if (Array.isArray(file.sizeString)) {
		file.sizeString = file.sizeString[0];
	}
	if (Array.isArray(file.geometryString)) {
		file.geometryString = file.geometryString[0];
	}
	return file;
}


code:
'use strict';

const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
	, k = 1024;

module.exports = (bytes) => {
	if (bytes === 0)  {
		return '0B';
	}
	const i = Math.floor(Math.log(bytes) / Math.log(k));
	return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`;
};


code:
const gm = require('gm')
	, configs = require(__dirname+'/../../configs/main.js')
	, uploadDirectory = require(__dirname+'/uploadDirectory.js');

module.exports = (filename, folder, temp) => {

	return new Promise((resolve, reject) => {
		const filePath = temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`;
		gm(`${filePath}[0]`) //0 for first frame of gifs, much faster
		.identify(function (err, data) {
			if (err) {
				return reject(err);
			}
			return resolve(data)
		});
	});

};


code:
const gm = require('gm')
	, ffmpeg = require('fluent-ffmpeg')
	, { thumbSize, ffmpegGifThumbnails } = require(__dirname+'/../../configs/main.js')
	, uploadDirectory = require(__dirname+'/uploadDirectory.js');

module.exports = (file, firstFrameOnly=true) => {

	return new Promise((resolve, reject) => {
		if (ffmpegGifThumbnails && !firstFrameOnly) {
			const thumbSizeFilter = file.geometry.width > file.geometry.height ? `${thumbSize}:-1` : `-1:${thumbSize}`;
			const complexFilters = [
					/* this complex filter scales (resizes), and works some magic to preserve transparency,
						with an additional filter to reduce the aliasing on outlines of transparent parts. */
					`[0:v] scale=${thumbSizeFilter},split [a][b]; [a] palettegen=reserve_transparent=on:transparency_color=ffffff [p]; [b][p] paletteuse`
			];
			ffmpeg(`${uploadDirectory}/file/${file.filename}`)
				.on('end', () => {
					return resolve();
				})
				.on('error', function(err, stdout, stderr) {
					return reject(err);
				})
				.complexFilter(complexFilters)
				.save(`${uploadDirectory}/file/thumb-${file.hash}${file.thumbextension}`);
		} else {
			//[0] for first frame (gifs, etc)
			const thumbnailing = gm(`${uploadDirectory}/file/${file.filename}${firstFrameOnly ? '[0]' : ''}`);
			if (!firstFrameOnly) {
				//try (and fail) to make gm thumbnailing less shit.
				thumbnailing.coalesce();
			}
			thumbnailing.resize(Math.min(thumbSize, file.geometry.width), Math.min(thumbSize, file.geometry.height))
			.write(`${uploadDirectory}/file/thumb-${file.hash}${file.thumbextension}`, function (err) {
				if (err) {
					return reject(err);
				}
				return resolve();
			});
		}
	});

};


code:
'use strict';

const FileType = require('file-type')
	, { allowMimeNoMatch } = require(__dirname+'/../../configs/main.js');

const image = new Set([
	'image/jpeg',
	'image/pjpeg',
	'image/png',
	'image/bmp',
]);

const animatedImage = new Set([
	'image/gif',
	'image/webp',
	'image/apng',
]);

const video = new Set([
	'video/mpeg',
	'video/quicktime',
	'video/mp4',
	'video/webm',
	'video/x-matroska',
]);

const audio = new Set([
	'audio/flac',
	'audio/mp3',
	'audio/mpeg',
	'audio/ogg',
	'audio/wave',
	'audio/wav',
]);

const other = new Set(require(__dirname+'/../../configs/main.js').otherMimeTypes);

module.exports = {

	allowed: (mimetype, options) => {
		return (options.image && image.has(mimetype)) ||
			(options.animatedImage && animatedImage.has(mimetype)) ||
			(options.video && video.has(mimetype)) ||
			(options.audio && audio.has(mimetype)) ||
			(options.other && other.has(mimetype));
	},

	realMimeCheck: async (file) => {
		const supposedMimeType = file.mimetype;
		const realMimeType = await FileType.fromFile(file.tempFilePath);
		if (!realMimeType) {
			return allowMimeNoMatch;
		}
		return supposedMimeType === realMimeType.mime;
	},

	image, animatedImage, video, audio, other

};


code:
'use strict';

const uploadDirectory = require(__dirname+'/uploadDirectory.js');

module.exports = (file, filename, folder) => {

	return new Promise((resolve, reject) => {
		file.mv(`${uploadDirectory}/${folder}/${filename}`, function (err) {
			if (err) {
				return reject(err);
			}
			return resolve();
		});
	});

};


code:
'use strict';

const path = require('path')
	, directory = path.join(__dirname+'/../../static')

module.exports = directory;


code:
const ffmpeg = require('fluent-ffmpeg')
	, { thumbSize } = require(__dirname+'/../../configs/main.js')
	, uploadDirectory = require(__dirname+'/uploadDirectory.js');

module.exports = (file, geometry, timestamp) => {

	return new Promise((resolve, reject) => {
		ffmpeg(`${uploadDirectory}/file/${file.filename}`)
		.on('end', () => {
			return resolve();
		})
		.on('error', function(err, stdout, stderr) {
			return reject(err);
		})
		.screenshots({
			timestamps: [timestamp],
			count: 1,
			filename: `thumb-${file.hash}${file.thumbextension}`,
			folder: `${uploadDirectory}/file/`,
			size: geometry.width > geometry.height ? `${thumbSize}x?` : `?x${thumbSize}`
			//keep aspect ratio, but also making sure taller/wider thumbs dont exceed thumbSize in either dimension
		});
	});

};


code:
'use strict'

const { countryNamesMap } = require(__dirname+'/countries.js')
	, { countryCodeHeader } = require(__dirname+'/../configs/main.js');

module.exports = (req, res, next) => {
	const code = req.headers[countryCodeHeader] || 'XX';
	res.locals.tor = code === 'TOR';
	res.locals.country = {
		code,
		name: countryNamesMap[code],
	}
	return next();
}


code:
'use strict';

const { ipHashSecret } = require(__dirname+'/../configs/main.js')
	, { createHash } = require('crypto');

module.exports = (ip) => createHash('sha256').update(ipHashSecret + ip).digest('base64');


code:
'use strict';

module.exports = (req, res, next) => {
	res.locals.numFiles = 0;
	if (req.files && req.files.file) {
		if (Array.isArray(req.files.file)) {
			res.locals.numFiles = req.files.file.filter(file => file.size > 0).length;
		} else {
			res.locals.numFiles = req.files.file.size > 0 ? 1 : 0;
			req.files.file = [req.files.file];
		}
	}
	next();
}


code:
'use strict';

module.exports = (query, limit) => {
	const nopage = { ...query };
	delete nopage.page;
	const queryString = new URLSearchParams(nopage).toString();
	let page;
	if (query.page && Number.isSafeInteger(parseInt(query.page))) {
		page = parseInt(query.page);
		if (page <= 0) {
			page = 1;
		}
	} else {
		page = 1;
	}
	const offset = (page-1) * limit;
	return { queryString, page, offset };
}


code:
'use strict';

const { ObjectId } = require(__dirname+'/../db/db.js')
	, allowedArrays = new Set(['captcha', 'checkednews', 'checkedposts', 'globalcheckedposts', 'spoiler', 'strip_filename',
		'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts', 'countries']) //only these should be arrays, since express bodyparser can output arrays
	, trimFields = ['tags', 'uri', 'moderators', 'filters', 'announcement', 'description', 'message',
		'name', 'subject', 'email', 'postpassword', 'password', 'default_name', 'report_reason', 'ban_reason', 'log_message', 'custom_css'] //trim if we dont want filed with whitespace
	, numberFields = ['lock_reset', 'captcha_reset', 'filter_mode', 'lock_mode', 'message_r9k_mode', 'file_r9k_mode', 'captcha_mode',
		'tph_trigger', 'pph_trigger', 'pph_trigger_action', 'tph_trigger_action', 'bump_limit', 'reply_limit', 'move_to_thread', 'postId',
		'max_files', 'thread_limit', 'thread', 'max_thread_message_length', 'max_reply_message_length', 'min_thread_message_length', 'min_reply_message_length', 'auth_level'] //convert these to numbers before they hit our routes
	, banDurationRegex = /^(?<YEAR>[\d]+y)?(?<MONTH>[\d]+mo)?(?<WEEK>[\d]+w)?(?<DAY>[\d]+d)?(?<HOUR>[\d]+h)?(?<MINUTE>[\d]+m)?(?<SECOND>[\d]+s)?$/
	, timeUtils = require(__dirname+'/timeutils.js')
	, makeArrayIfSingle = (obj) => !Array.isArray(obj) ? [obj] : obj;

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

	const bodyfields = Object.keys(req.body);
	for (let i = 0; i < bodyfields.length; i++) {
		const key = bodyfields[i];
		const val = req.body[key];
		/*
			bodyparser can form arrays e.g. for multiple files, but we only want arrays in fields we
			expect, to prevent issues when validating/using them later on.
		*/
		if (!allowedArrays.has(key) && Array.isArray(val)) {
			return res.status(400).render('message', {
				'title': 'Bad request',
				'message': 'Malformed input'
			});
		} else if (allowedArrays.has(key) && !Array.isArray(val)) {
			req.body[key] = makeArrayIfSingle(req.body[key]); //convert to arrays with single item for simpler case batch handling later
		}
	}

	for (let i = 0; i < trimFields.length; i++) {
		const field = trimFields[i];
		if (req.body[field]) {
			//trimEnd() because trailing whitespace doesnt affect how a post appear and if it is all whitespace, trimEnd will get it all anyway
			req.body[field] = req.body[field].trimEnd();
		}
	}

	//proper length check for CRLF vs just LF, because browsers dont count CRLF as 2 characters like the server does (and like it technically is)
	if (req.body.message) {
		res.locals.messageLength = req.body.message.replace(/\r\n/igm, '\n').length;
	}

	for (let i = 0; i < numberFields.length; i++) {
		const field = numberFields[i];
		if (req.body[field]) {
			const num = parseInt(req.body[field]);
			if (Number.isSafeInteger(num)) {
				req.body[field] = num;
			} else {
				req.body[field] = null;
			}
		}
	}

	//convert checked reports to number
	if (req.body.checkedposts) {
		req.body.checkedposts = req.body.checkedposts.map(Number);
	}
	//convert checked global reports to mongoid
	if (req.body.globalcheckedposts) {
		req.body.globalcheckedposts = req.body.globalcheckedposts.map(ObjectId)
	}
	if (req.body.checkednews) {
		req.body.checkednews = req.body.checkednews.map(ObjectId)
	}
	//convert checked bans to mongoid
	if (req.body.checkedbans) {
		req.body.checkedbans = req.body.checkedbans.map(ObjectId)
	}
/*
	//convert checked reports to mongoid
	if (req.body.checkedreports) {
		req.body.checkedreports = req.body.checkedreports.map(ObjectId)
	}
*/

	//ban duration convert to ban time in ms
	if (req.body.ban_duration) {
		const matches = req.body.ban_duration.match(banDurationRegex);
		if (matches && matches.groups) {
			const groups = matches.groups;
			let banDuration = 0;
			const groupKeys = Object.keys(groups);
			for (let i = 0; i < groupKeys.length; i++) {
				const key = groupKeys[i];
				if (!groups[key]) {
					continue;
				}
				const mult = +groups[key].substring(0,groups[key].length-1); //remove the d, m, y, etc from end of the value
				if (Number.isSafeInteger(mult) //if the multiplier is safe int
					&& Number.isSafeInteger(mult*timeUtils[key]) //and multiplying it is safe int
					&& Number.isSafeInteger((mult*timeUtils[key])+banDuration)) { //and adding it to the total is safe
					banDuration += mult*timeUtils[key];
				}
			}
			req.body.ban_duration = banDuration;
		} else {
			req.body.ban_duration = null;
		}
	}

	//thread id
	if (req.params.id) {
		req.params.id = +req.params.id;
	}
	//moglog date
	if (req.params.date) {
		let [ month, day, year ] = req.params.date.split('-');
		month = month-1;
		const date = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
		if (date !== 'Invalid Date') {
			res.locals.date = { month, day, year, date };
		}
	}

	next();

}


code:
'use strict';

module.exports = {

	regexPrepare: /##(?<numdice>[1-9][0-9]{0,1})d(?<numsides>1[0-9]{1,8}|[2-9][0-9]{0,8})(?:(?<operator>[+-])(?<modifier>[1-9][0-9]{0,8}))?(?<value>=[1-9][0-9]{0,8})?/gmi,
	regexMarkdown: /##(?<numdice>[1-9][0-9]{0,1})d(?<numsides>1[0-9]{1,8}|[2-9][0-9]{0,8})(?:(?<operator>[+-])(?<modifier>[1-9][0-9]{0,8}))?=(?<value>[1-9][0-9]{0,8})/gmi,

	prepare: (force) => (match, numdice, numsides, operator, modifier, value) => {
		if (!force && value) {
			return match;
		}

		numdice = parseInt(numdice);
		numsides = parseInt(numsides);
		let matchWithoutValue = match.replace(/=.*/, '');

		let sum = 0;
		for (let i = 0; i < numdice; ++i) {
			sum += Math.floor(Math.random() * numsides) + 1;
		}
		if (modifier && operator) {
			modifier = parseInt(modifier);
			//do i need to make sure it doesnt go negative or maybe give absolute value?
			if (operator === '+') {
				sum += modifier;
			} else {
				sum -= modifier;
			}
		}
		return `${matchWithoutValue}=${sum}${value ? value : ''}`;
	},

	markdown: (match, numdice, numsides, operator, modifier, value) => {
		numdice = parseInt(numdice);
		numsides = parseInt(numsides);
		value = parseInt(value);
		let matchWithoutValue = match.replace(/=.*/, '');
		return `(${matchWithoutValue}) Rolled ${numdice} dice with ${numsides} sides${modifier ? ' and modifier '+operator+modifier : '' } = ${value}`;
	},
}


code:
'use strict';

const parenPairRegex = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]\))*\))*\)/g

module.exports = (match) => {
	let trimmedMatch;
	let excess = '';
	const parensPairs = match.match(parenPairRegex);
	//naive solution to conflict with detected markdown
	if (parensPairs) {
		const lastMatch = parensPairs[parensPairs.length-1];
		const lastIndex = match.lastIndexOf(lastMatch);
		trimmedMatch = match.substring(0, lastIndex+lastMatch.length);
		excess = match.substring(lastIndex+lastMatch.length);
	} else if (match.indexOf(')') !== -1){
		trimmedMatch = match.substring(0, match.indexOf(')'));
		excess = match.substring(match.indexOf(')'));
	} else {
		trimmedMatch = match;
	}
	trimmedMatch = trimmedMatch
		.replace(/\(/g, '%28')
		.replace(/\)/g, '%29');
	return `<a rel='nofollow' referrerpolicy='same-origin' target='_blank' href='${trimmedMatch}'>${trimmedMatch}</a>${excess}`;
};


code:
'use strict';

const greentextRegex = /^>((?!>\d+|>>/\w+(/\d*)?).*)/gm
	, pinktextRegex = /^<(.+)/gm
	, boldRegex = /''(.+?)''/gm
	, titleRegex = /==(.+?)==/gm
	, monoRegex = /`(.+?)`/gm
	, underlineRegex = /__(.+?)__/gm
	, strikeRegex = /~~(.+?)~~/gm
	, italicRegex = /\*\*(.+?)\*\*/gm
	, spoilerRegex = /\|\|([\s\S]+?)\|\|/gm
	, detectedRegex = /(\(\(\(.+?\)\)\))/gm
	, linkRegex = /https?\://[^\s<>\[\]{}|\\^]+/g
	, codeRegex = /(?:(?<language>[a-z+]{1,10})\r?\n)?(?<code>[\s\S]+)/i
	, includeSplitRegex = /(```[\s\S]+?```)/gm
	, splitRegex = /```([\s\S]+?)```/gm
	, trimNewlineRegex = /^\s*(\r?\n)*|(\r?\n)*$/g
	, getDomain = (string) => string.split(/\/\/|\//)[1] //unused atm
	, escape = require(__dirname+'/escape.js')
	, { highlight, highlightAuto } = require('highlight.js')
	, { highlightOptions } = require(__dirname+'/../../configs/main.js')
	, diceroll = require(__dirname+'/diceroll.js')
	, replacements = [
		{ regex: pinktextRegex, cb: (match, pinktext) => `<span class='pinktext'><${pinktext}</span>` },
		{ regex: greentextRegex, cb: (match, greentext) => `<span class='greentext'>>${greentext}</span>` },
		{ regex: boldRegex, cb: (match, bold) => `<span class='bold'>${bold}</span>` },
		{ regex: underlineRegex, cb: (match, underline) => `<span class='underline'>${underline}</span>` },
		{ regex: strikeRegex, cb: (match, strike) => `<span class='strike'>${strike}</span>` },
		{ regex: titleRegex, cb: (match, title) => `<span class='title'>${title}</span>` },
		{ regex: italicRegex, cb: (match, italic) => `<span class='em'>${italic}</span>` },
		{ regex: spoilerRegex, cb: (match, spoiler) => `<span class='spoiler'>${spoiler}</span>` },
		{ regex: monoRegex, cb: (match, mono) => `<span class='mono'>${mono}</span>` },
		{ regex: linkRegex, cb: require(__dirname+'/linkmatch.js') },
		{ regex: detectedRegex, cb: (match, detected) => `<span class='detected'>${detected}</span>` },
		{ regex: diceroll.regexMarkdown, cb: diceroll.markdown },
	];

module.exports = {

	prepareMarkdown: (text, force) => {
		if (!text || text.length === 0) {
			return text;
		}
		const chunks = text.split(includeSplitRegex);
		for (let i = 0; i < chunks.length; i++) {
			//every other chunk will be a code block
			if (i % 2 === 0) {
				chunks[i] = chunks[i].replace(
					diceroll.regexPrepare, diceroll.prepare(force));
			}
		}
		return chunks.join('');
	},

	markdown: (text) => {
		const chunks = text.split(splitRegex);
		for (let i = 0; i < chunks.length; i++) {
			//every other chunk will be a code block
			if (i % 2 === 0) {
				const escaped = escape(chunks[i]);
				const newlineFix = escaped.replace(/^\r?\n/,''); //fix ending newline because of codeblock
				chunks[i] = module.exports.processRegularChunk(newlineFix);
			} else {
				chunks[i] = module.exports.processCodeChunk(chunks[i]);
			}
		}
		return chunks.join('');
	},

	processCodeChunk: (text) => {
		const matches = text.match(codeRegex);
		const trimFix = matches.groups.code.replace(trimNewlineRegex, '');
		let lang;
		if (matches.groups.language && matches.groups.language.length > 0) {
			lang = matches.groups.language.toLowerCase();
		}
		if (!lang) {
			const { language, relevance, value } = highlightAuto(trimFix, highlightOptions.languageSubset);
			if (relevance > highlightOptions.threshold) {
				return `<span class='code hljs'><small>possible language: ${language}, relevance: ${relevance}</small>\n${value}</span>`;
			}
		} else if (lang !== 'plain' && highlightOptions.languageSubset.includes(lang)) {
			const { value } = highlight(lang, trimFix, true);
			return `<span class='code hljs'><small>language: ${lang}</small>\n${value}</span>`;
		}
		return `<span class='code'>${escape(trimFix)}</span>`;
	},

	processRegularChunk: (text) => {
		for (let i = 0; i < replacements.length; i++) {
			text = text.replace(replacements[i].regex, replacements[i].cb);
		}
		return text;
	},

}


code:
'use strict';

const quoteHandler = require(__dirname+'/quotes.js')
	, { markdown } = require(__dirname+'/markdown.js')
	, sanitizeOptions = require(__dirname+'/sanitizeoptions.js')
	, sanitize = require('sanitize-html');

module.exports = async (inputMessage, boardName, threadId=null) => {

	let message = inputMessage;
	let quotes = [];
	let crossquotes = [];

	//markdown a post, link the quotes, sanitize and return message and quote arrays
	if (message && message.length > 0) {
		message = markdown(message);
		const { quotedMessage, threadQuotes, crossQuotes } = await quoteHandler.process(boardName, message, threadId);
		message = quotedMessage;
		quotes = threadQuotes;
		crossquotes = crossQuotes;
		message = sanitize(message, sanitizeOptions.after);
	}

	return { message, quotes, crossquotes };

}


code:
'use strict';

const getTripCode = require(__dirname+'/tripcode.js')
	, nameRegex = /^(?<name>(?!##).*?)?(?:##(?<tripcode>[^ ]{1}.*?))?(?<capcode>##(?<capcodetext> .*?)?)?$/
	, staffLevels = ['Admin', 'Global Staff', 'Board Owner', 'Board Mod']
	, staffLevelsRegex = new RegExp(`(${staffLevels.join('|')})+`, 'igm')

module.exports = async (inputName, permLevel, boardSettings, boardOwner, username) => {

	const { forceAnon, defaultName, moderators } = boardSettings;
	const isBoardOwner = username === boardOwner;
	const isBoardMod = moderators.includes(username);

	let name = defaultName;
	let tripcode = null;
	let capcode = null;
	if ((permLevel < 4 || !forceAnon) && inputName && inputName.length > 0) {
		// get matches with named groups for name, trip and capcode in 1 regex
		const matches = inputName.match(nameRegex);
		if (matches && matches.groups) {
			const groups = matches.groups;
			//name
			if (groups.name) {
				name = groups.name;
			}
			//tripcode
			if (groups.tripcode) {
				tripcode = `!!${(await getTripCode(groups.tripcode))}`;
			}
			//capcode
			if (permLevel < 4 && groups.capcode) {
				let capcodeInput = groups.capcodetext ? groups.capcodetext.trim() : '';
				//by default, use board staff if ismod/owner, else use higher staff
				let staffLevel = staffLevels.find((sl, l) => capcodeInput.toLowerCase().startsWith(sl.toLowerCase()) && l === permLevel)
					|| (isBoardOwner ? staffLevels[2]
					: isBoardMod ? staffLevels[3]
					: staffLevels[permLevel]); //kill me
				capcode = staffLevel;
				if (capcodeInput && capcodeInput.toLowerCase() !== staffLevel.toLowerCase()) {
					capcode = `${staffLevel} ${capcodeInput.replace(staffLevelsRegex, '').trim()}`;
				}
				capcode = `## ${capcode}`;
			}
		}
	}

	return { name, tripcode, capcode };

}


code:
'use strict';

const Posts = require(__dirname+'/../../db/posts.js')
	, Boards = require(__dirname+'/../../db/boards.js')
	, quoteRegex = />>(?<quotenum>\d+)/g
	, crossQuoteRegex = />>>/(?<board>\w+)(?:/(?<quotenum>\d*))?/gm;

module.exports = {

	quoteRegex, crossQuoteRegex,

	process: async (board, text, thread) => {

		//get the matches
		const quotes = text.match(quoteRegex);
		const crossQuotes = text.match(crossQuoteRegex);
		if (!quotes && !crossQuotes) {
			return { quotedMessage: text, threadQuotes: [], crossQuotes: [] };
		}

		//make query for db including crossquotes
		const postQueryOrs = []
		const boardQueryIns = []
		const crossQuoteMap = {};
		if (quotes && board) {
			const quoteIds = [...new Set(quotes.map(q => { return Number(q.substring(8)) }))];
			postQueryOrs.push({
				'board': board,
				'postId': {
					'$in': quoteIds
				}
			});
		}

		if (crossQuotes) {
			for (let i = 0; i < crossQuotes.length; i++) {
				const crossQuote = crossQuotes[i].split('/');
				const crossQuoteBoard = crossQuote[1];
				const crossQuotePostId = +crossQuote[2];
				if (crossQuoteBoard === board) {
					continue;
				}
				if (!crossQuoteMap[crossQuoteBoard]) {
					crossQuoteMap[crossQuoteBoard] = [];
				}
				if (!isNaN(crossQuotePostId) && crossQuotePostId > 0) {
					crossQuoteMap[crossQuoteBoard].push(crossQuotePostId);
				}
			}
			const crossQuoteBoards = Object.keys(crossQuoteMap)
			for (let i = 0; i < crossQuoteBoards.length; i++) {
				const crossQuoteBoard = crossQuoteBoards[i];
				boardQueryIns.push(crossQuoteBoard);
				const crossQuoteBoardPostIds = crossQuoteMap[crossQuoteBoard];
				if (crossQuoteBoardPostIds.length > 0) {
					postQueryOrs.push({
						'board': crossQuoteBoard,
						'postId': {
							'$in': crossQuoteBoardPostIds
						}
					});
				}
			}
		}

		//get all the posts from quotes
		const postThreadIdMap = {};
		const [ posts, boards ] = await Promise.all([
			postQueryOrs.length > 0 ? Posts.getPostsForQuotes(postQueryOrs) : [],
			boardQueryIns.length > 0 ? Boards.db.find({ '_id': { '$in': boardQueryIns } }, { projection: { '_id': 1 } }).toArray() : []
		]);

		//turn the result into a map of postId => threadId/postId
		for (let i = 0; i < posts.length; i++) {
			const post = posts[i];
			if (!postThreadIdMap[post.board]) {
				postThreadIdMap[post.board] = {};
			}
			postThreadIdMap[post.board][post.postId] = {
				'_id': post._id,
				'thread': post.thread || post.postId,
				'postId': post.postId
			};
		}
		for (let i = 0; i < boards.length; i++) {
			const boardName = boards[i]._id;
			if (!postThreadIdMap[boardName]) {
				postThreadIdMap[boardName] = {};
			}
		}

		//then replace the quotes with only ones that exist
		const threadQuotes = new Set();
		const nonThreadQuotes = new Set();
		if (quotes) {
			text = text.replace(quoteRegex, (match, quotenum) => {
				if (postThreadIdMap[board] && postThreadIdMap[board][quotenum]) {
					if (postThreadIdMap[board][quotenum].thread === thread) {
						threadQuotes.add(postThreadIdMap[board][quotenum]);
					} else {
						nonThreadQuotes.add(postThreadIdMap[board][quotenum]);
					}
					return `>>${quotenum}${postThreadIdMap[board][quotenum].postId == thread ? ' (OP) ' : ''}`;
				}
				return `<span class='invalid-quote'>>>${quotenum}</span>`;
			});
		}
		if (crossQuotes) {
			text = text.replace(crossQuoteRegex, (match, quoteboard, quotenum) => {
				if (postThreadIdMap[quoteboard]) {
					if (!quotenum) {
						return `<a class='quote' href='/${quoteboard}/index.html'>>>>/${quoteboard}/</a>`;
					} else if (!isNaN(quotenum) && quotenum > 0 && postThreadIdMap[quoteboard][quotenum]) {
						return `<a class='quote' href='/${quoteboard}/thread/${postThreadIdMap[quoteboard][quotenum].thread}.html#${quotenum}'>>>>/${quoteboard}/${quotenum}</a>`;
					}
				}
				return `<span class='invalid-quote'>>>>/${quoteboard}/${quotenum || ''}</span>`;
			});
		}

		return { quotedMessage: text, threadQuotes: [...threadQuotes], crossQuotes: [...nonThreadQuotes] };

	},

};


code:
'use strict';

module.exports = {

	before: {
		allowedTags: [],
		allowedAttributes: {}
	},

	after: {
		allowedTags: [ 'span', 'a', 'img', 'small'],
		allowedAttributes: {
			'a': [ 'href', 'rel', 'class', 'referrerpolicy', 'target' ],
			'span': [ 'class' ],
			'img': ['src', 'height', 'width']
		}
	}

}


code:
'use strict';

const { tripcodeSecret } = require(__dirname+'/../../configs/main.js')
    , { createHash } = require('crypto')

module.exports = async (password) => {

	const tripcodeHash = createHash('sha256').update(password + tripcodeSecret).digest('base64');
	const tripcode = tripcodeHash.substring(tripcodeHash.length-10);
	return tripcode;

}


code:
'use strict';

const { ipHeader, ipHashPermLevel } = require(__dirname+'/../configs/main.js')
	, { parse } = require('ip6addr')
	, deleteTempFiles = require(__dirname+'/files/deletetempfiles.js')
	, dynamicResponse = require(__dirname+'/dynamic.js')
	, hashIp = require(__dirname+'/haship.js');

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

	//tor user ip uses bypass id, if they dont have one send to blockbypass
	if (res.locals.tor) {
		const pseudoIp = res.locals.preFetchedBypassId || req.signedCookies.bypassid;
		res.locals.ip = {
			raw: pseudoIp,
			single: pseudoIp,
			qrange: pseudoIp,
			hrange: pseudoIp,
		};
		return next();
	}

	//ip for normal user
	const ip = req.headers[ipHeader] || req.connection.remoteAddress;
	try {
		const ipParsed = parse(ip);
		const ipStr = ipParsed.toString({
			format: ipParsed.kind() === 'ipv4' ? 'v4' : 'v6',
			zeroElide: false,
			zeroPad: false,
		});
		const delimiter = ipParsed.kind() === 'ipv4' ? '.' : ':';
		let split = ipStr.split(delimiter);
		const qrange = split.slice(0,Math.floor(split.length*0.75)).join(delimiter);
		const hrange = split.slice(0,Math.floor(split.length*0.5)).join(delimiter);
		res.locals.ip = {
			raw: ipHashPermLevel === -1 ? hashIp(ipStr) : ipStr,
			single: hashIp(ipStr),
			qrange: hashIp(qrange),
			hrange: hashIp(hrange),
		}
		next();
	} catch(e)  {
		console.error('Ip parse failed', e);
		return res.status(400).render('message', {
			'title': 'Bad request',
			'message': 'Malformed IP' //should never get here
		});
	}

}


code:
'use strict';

const  { promisify } = require('util')
	, randomBytes = promisify(require('crypto').randomBytes);

//NOTE: should only be used for ints, floors your inputs
module.exports = async (min, max) => {
	min = Math.floor(min);
	max = Math.floor(max);
	if (max <= min) return min;
	const mod = max - min + 1;
	const div = (((0xffffffff - (mod-1)) / mod) | 0) + 1;
	let g
	do {
		g = (await randomBytes(4)).readUInt32LE();
	} while (g > div * mod - 1);
	return ((g / div) | 0) + min;
};


code:
'use strict';

const { refererCheck, allowedHosts } = require(__dirname+'/../configs/main.js')
	, dynamicResponse = require(__dirname+'/dynamic.js')
	, allowedHostSet = new Set(allowedHosts);

module.exports = (req, res, next) => {
	if (req.method !== 'POST') {
		return next();
	}
	let validReferer = false;
	try {
		const url = new URL(req.headers.referer);
		validReferer = allowedHostSet.has(url.hostname);
	} catch(e) {
		//referrer is invalid url
	}
	if (refererCheck === true && (!req.headers.referer || !validReferer)) {
        return dynamicResponse(req, res, 403, 'message', {
			'title': 'Forbidden',
			'message': 'Invalid or missing "Referer" header. Are you posting from the correct URL?'
		});
	}
	next();
}


code:
'use strict';

const { enableUserBoardCreation, enableUserAccountCreation,
	lockWait, globalLimits, boardDefaults, cacheTemplates,
	meta, enableWebring, captchaOptions } = require(__dirname+'/../configs/main.js')
	, { outputFile } = require('fs-extra')
	, formatSize = require(__dirname+'/files/formatsize.js')
	, pug = require('pug')
	, path = require('path')
	, commit = require(__dirname+'/commit.js')
	, uploadDirectory = require(__dirname+'/files/uploadDirectory.js')
	, redlock = require(__dirname+'/../redlock.js')
	, templateDirectory = path.join(__dirname+'/../views/pages/')
	, renderLocals = {
		cache: cacheTemplates,
		meta,
		commit,
		defaultTheme: boardDefaults.theme,
		defaultCodeTheme: boardDefaults.codeTheme,
		postFilesSize: formatSize(globalLimits.postFilesSize.max),
		enableUserAccountCreation,
		enableUserBoardCreation,
		globalLimits,
		enableWebring,
		captchaType: captchaOptions.type
	}

switch (captchaOptions.type) {
	case 'google':
		renderLocals.googleRecaptchaSiteKey = captchaOptions.google.siteKey;
		break;
	case 'hcaptcha':
		renderLocals.hcaptchaSitekey = captchaOptions.hcaptcha.siteKey;
		break;
	case 'grid':
		renderLocals.captchaGridSize = captchaOptions.grid.size;
		break;
	default:
		break;
}

module.exports = async (htmlName, templateName, options, json=null) => {
	const html = pug.renderFile(`${templateDirectory}${templateName}`, {
		...options,
		...renderLocals,
	});
	const lock = await redlock.lock(`locks:${htmlName}`, lockWait);
	const htmlPromise = outputFile(`${uploadDirectory}/html/${htmlName}`, html);
	let jsonPromise;
	if (json !== null) {
		jsonPromise = outputFile(`${uploadDirectory}/json/${json.name}`, JSON.stringify(json.data));
	}
	await Promise.all([htmlPromise, jsonPromise]);
	await lock.unlock();
	return { html, json: json ? json.data : null };
};


code:
'use strict';

const { Accounts } = require(__dirname+'/../db/')
	, cache = require(__dirname+'/../redis.js');

module.exports = async (req, res, next) => {
	if (req.session && req.session.user) {
		res.locals.user = await cache.get(`users:${req.session.user}`);

		if (!res.locals.user) {
			const account = await Accounts.findOne(req.session.user);
			if (!account) {
				req.session.destroy();
			} else {
				await Accounts.updateLastActiveDate(req.session.user);
				res.locals.user = {
					'username': account._id,
					'authLevel': account.authLevel,
					'modBoards': account.modBoards,
					'ownedBoards': account.ownedBoards,
				};
				cache.set(`users:${req.session.user}`, res.locals.user, 3600);
			}
		}
	}
	next();
}


code:
'use strict';

module.exports = (req, res, next) => {
	res.locals.minimal = true;
	next();
}


code:
'use strict';

const Mongo = require(__dirname+'/../db/db.js')
	, timeUtils = require(__dirname+'/timeutils.js')
	, uploadDirectory = require(__dirname+'/files/uploadDirectory.js')
	, { remove } = require('fs-extra')
	, { debugLogs, pruneModlogs, pruneAfterDays, enableWebring, maxRecentNews } = require(__dirname+'/../configs/main.js')
	, { Stats, Posts, Files, Boards, News, Modlogs } = require(__dirname+'/../db/')
	, cache = require(__dirname+'/../redis.js')
	, render = require(__dirname+'/render.js')
	, buildQueue = require(__dirname+'/../queue.js')
	, timeDiffString = require(__dirname+'/timediffstring.js');

module.exports = {

	buildBanners: async (options) => {
		const label = `/${options.board._id}/banners.html`;
		const start = process.hrtime();
		const { html } = await render(label, 'banners.pug', options, {
			'name': `/${options.board._id}/banners.json`,
			'data': options.board.banners
		});
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
		return html;
	},

	buildCatalog: async (options) => {
		const label = `/${options.board._id || options.board}/catalog.html`;
		const start = process.hrtime();
		if (!options.board._id) {
			options.board = await Boards.findOne(options.board);
		}
		const threads = await Posts.getCatalog(options.board._id);
		const { html, json } = await render(label, 'catalog.pug', {
			...options,
			threads,
		}, {
			'name': `/${options.board._id}/catalog.json`,
			'data': threads
		});
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
		return { html, json };
	},

	buildThread: async (options) => {
		const label = `/${options.board._id || options.board}/thread/${options.threadId}.html`;
		const start = process.hrtime();
		if (!options.board._id) {
			options.board = await Boards.findOne(options.board);
		}
		const thread = await Posts.getThread(options.board._id, options.threadId);
		if (!thread) {
			return; //this thread may have been an OP that was deleted
		}
		const { html, json } = await render(label, 'thread.pug', {
			...options,
			thread,
		}, {
			'name': `/${options.board._id}/thread/${options.threadId}.json`,
			'data': thread
		});
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
		return { html, json };
	},

	buildBoard: async (options) => {
		const label = `/${options.board._id}/${options.page === 1 ? 'index' : options.page}.html`;
		const start = process.hrtime();
		const threads = await Posts.getRecent(options.board._id, options.page);
		if (!options.maxPage) {
			options.maxPage = Math.min(Math.ceil((await Posts.getPages(options.board._id)) / 10), Math.ceil(options.board.settings.threadLimit/10));
		}
		const { html, json } = await render(label, 'board.pug', {
			...options,
			threads,
		}, {
			'name': `/${options.board._id}/${options.page === 1 ? 'index' : options.page}.json`,
			'data': threads
		});
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
		return { html, json };
	},

	//building multiple pages (for rebuilds)
	buildBoardMultiple: async (options) => {
		const start = process.hrtime();
		if (!options.board._id) {
			options.board = await Boards.findOne(options.board);
		}
		const maxPage = Math.min(Math.ceil((await Posts.getPages(options.board._id)) / 10), Math.ceil(options.board.settings.threadLimit/10)) || 1;
		if (options.endpage === 0) {
			//deleted only/all posts, so only 1 page will remain
			options.endpage = 1;
		} else if (maxPage < options.endpage || !options.endpage) {
			//else just build up to the max page if it is greater than input page number
			options.endpage = maxPage
		}
		const difference = options.endpage-options.startpage + 1; //+1 because for single pagemust be > 0
		const threads = await Posts.getRecent(options.board._id, options.startpage, difference*10);
		const label = `/${options.board._id}/${options.startpage === 1 ? 'index' : options.startpage}${options.endpage === options.startpage ? '' : '->'+(options.endpage === 1 ? 'index' : options.endpage)}.html`;
		const buildArray = [];
		for (let i = options.startpage; i <= options.endpage; i++) {
			let spliceStart = (i-1)*10;
			if (spliceStart > 0) {
				spliceStart = spliceStart - 1;
			}
			const pageThreads = threads.splice(0,10);
			buildArray.push(
				render(`${options.board._id}/${i === 1 ? 'index' : i}.html`, 'board.pug', {
					board: options.board,
					threads: pageThreads,
					maxPage,
					page: i,
				}, {
					'name': `/${options.board._id}/${i === 1 ? 'index' : i}.json`,
					'data': pageThreads
				})
			);
		}
		await Promise.all(buildArray);
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
	},

	buildNews: async () => {
		const label = '/news.html';
		const start = process.hrtime();
		const news = await News.find();
		const { html } = await render('news.html', 'news.pug', {
			news
		});
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
		return html;
	},

	buildModLog: async (options) => {
		if (!options.startDate || !options.endDate) {
			const d = new Date();
			const month = d.getUTCMonth()
				, day = d.getUTCDate()
				, year = d.getUTCFullYear();
			options.startDate = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
			options.endDate = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));
		}
		const day = ('0'+options.startDate.getDate()).slice(-2);
		const month = ('0'+(options.startDate.getMonth()+1)).slice(-2);
		const year = options.startDate.getFullYear();
		const label = `/${options.board._id}/logs/${month}-${day}-${year}.html`;
		const start = process.hrtime();
		if (!options.logs) {
			options.logs = await Modlogs.findBetweenDate(options.board, options.startDate, options.endDate);
		}
		const { html } = await render(label, 'modlog.pug', {
			...options
		});
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
		return html;
	},

	buildModLogList: async (options) => {
		const label = `/${options.board._id}/logs.html`;
		const start = process.hrtime();
		let dates = await Modlogs.getDates(options.board);
		if (pruneModlogs === true) {
			const pruneLogs = [];
			const pruneAfter = new Date(Date.now()-timeUtils.DAY*pruneAfterDays);
			dates = dates.filter(date => {
				const { year, month, day } = date.date;
				if (new Date(year, month-1, day) > pruneAfter) { //-1 for 0-index months
					return true;
				}
				const logName = `${month}-${day}-${year}`;
				debugLogs && console.log('Pruning log', `${options.board._id}/${logName}`)
				pruneLogs.push(logName);
				return false;
			});
			if (pruneLogs.length > 0) {
				await Promise.all(pruneLogs.map(log => {
					remove(`${uploadDirectory}/html/${options.board._id}/logs/${log}.html`)
				}));
				await Modlogs.deleteOld(options.board, pruneAfter);
			}
		}
		const { html } = await render(label, 'modloglist.pug', {
			board: options.board,
			dates
		});
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
		return html;
	},

	buildHomepage: async () => {
		const label = '/index.html';
		const start = process.hrtime();
		let [ totalStats, boards, fileStats, recentNews ] = await Promise.all([
			Boards.totalStats(), //overall total posts ever made
			Boards.boardSort(0, 20), //top 20 boards sorted by users, pph, total posts
			Files.activeContent(), //size ans number of files
			News.find(maxRecentNews), //some recent newsposts
		]);
		const { html } = await render('index.html', 'home.pug', {
			totalStats,
			boards,
			fileStats,
			recentNews,
		});
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
		return html;
	},

	updateStats: async () => {
		const label = 'Hourly stats rollover';
		const start = process.hrtime();
		await Stats.updateBoards();
		await Stats.resetStats();
		buildQueue.push({
 			'task': 'buildHomepage',
		});
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
		module.exports.resetTriggers();
	},

	resetTriggers: async() => {
		const label = 'Resetting pph/tph triggers';
		const start = process.hrtime();
		const triggeredBoards = await cache.sgetall('triggered'); //boards triggered pph/tph mode
		if (triggeredBoards.length === 0) {
			return; //no label is no triggers
		}
		await cache.del('triggered');
		const triggerModes = await Boards.triggerModes(triggeredBoards);
		const bulkWrites = triggerModes.map(p => {
			return {
				'updateOne': {
					'filter': {
						'_id': p._id
					},
					'update': {
						'$set': {
							/* reset=0 is "no change", the options go from 0-2, and get reset to 0 or 1,
							so if >0, we subtract 1 otherwise no change */
							'settings.lockMode': (p.lockReset > 0 ? Math.min(p.lockReset-1, p.lockMode) : p.lockMode),
							'settings.captchaMode': (p.captchaReset > 0 ? Math.min(p.captchaReset-1, p.captchaMode) : p.captchaMode),
						}
					}
				}
			}
		});
		await Boards.db.bulkWrite(bulkWrites);
		const promises = [];
		triggerModes.forEach(async (p) => {
			await cache.del(`board:${p._id}`);
			if (p.captchaReset > 0 && p.captchaReset-1 < p.captchaMode) {
				if (p.captchaReset-1 <= 1) {
					promises.push(remove(`${uploadDirectory}/html/${p._id}/thread/`));
				}
				if (p.captchaReset-1 === 0) {
					buildQueue.push({
						'task': 'buildBoardMultiple',
						'options': {
							'board': p._id,
							'startpage': 1,
							'endpage': Math.ceil(p.threadLimit/10)
						}
					})
					buildQueue.push({
						'task': 'buildCatalog',
						'options': {
							'board': p._id
						}
					});
				}
			}
		})
		await Promise.all(promises);
		const end = process.hrtime(start);
		debugLogs && console.log(timeDiffString(label, end));
	},

	buildChangePassword: async () => {
		const { html } = await render('changepassword.html', 'changepassword.pug');
		return html;
	},

	buildRegister: async () => {
		const { html } = await render('register.html', 'register.pug');
		return html;
	},

	buildBypass: async (minimal=false) => {
		const { html } = await render(`bypass${minimal ? '_minimal' : ''}.html`, 'bypass.pug', {
			minimal,
		});
		return html;
	},

	buildCreate: async () => {
		const { html } = await render('create.html', 'create.pug');
		return html;
	},

	buildCaptcha: async() => {
		const { html } = await render('captcha.html', 'captcha.pug');
		return html;
	},

}


code:
'use strict';

const { readdirSync } = require('fs')
	, { themes, codeThemes } = require(__dirname+'/../configs/main.js');

module.exports = {

	themes: themes.length > 0 ? themes : readdirSync(__dirname+'/../gulp/res/css/themes/').map(x => x.substring(0,x.length-4)),

	codeThemes: codeThemes.length > 0 ? codeThemes : readdirSync(__dirname+'/../node_modules/highlight.js/styles/').map(x => x.substring(0,x.length-4)),

}


code:
'use strict';

module.exports = (label, end) => {
	return (`${end[0] > 0 ? end[0]+'s' : ''}${Math.trunc(end[1]/1000000)}ms `).padStart(9) + label;
}


code:
'use strict';

const YEAR = 31536000000
	, MONTH = 2629800000
	, WEEK = 604800000
	, DAY = 86400000
	, HOUR = 3600000
	, MINUTE = 60000
	, SECOND = 1000;

module.exports = {

	YEAR, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND,

	//ms until next hour
	'nextHour': () => {
		return 3600000 - new Date().getTime() % 3600000;
	},

	//string representing how long since date A to date B
	'relativeString': (now, relativeTo) => {
		let difference = now.getTime() - relativeTo.getTime();
		let amount = 0;
		let ret = '';
		let isFuture = false;
		if (difference < 0) {
			difference = Math.abs(difference);
			isFuture = true;
		}
		if (difference < MINUTE) {
			return 'Now';
		} else if (difference < MINUTE*59.5) {
			amount = Math.round(difference / MINUTE);
			ret += `${amount} minute`;
		} else if (difference < HOUR*23.5) {
			amount = Math.round(difference / HOUR);
			ret += `${amount} hour`;
		} else if (difference < DAY*6.5) {
			amount = Math.round(difference / DAY);
			ret += `${amount} day`;
		} else if (difference < WEEK*3.5) {
			amount = Math.round(difference / WEEK);
			ret += `${amount} week`;
		} else if (difference < MONTH*11.5) {
			amount = Math.round(difference / MONTH);
			ret += `${amount} month`;
		} else {
			amount = Math.round(difference / YEAR);
			ret = `${amount} year`;
		}
		return `${ret}${amount > 1 ? 's' : ''} ${isFuture ? 'from now' :  'ago'}`;
	},

	'relativeColor': (now, relativeTo) => {
		const difference = now.getTime() - relativeTo.getTime();
		const minutesAgo = Math.floor(difference / MINUTE);
		let r = 0
			, g = 0
			, b = 0;
		if (difference < MINUTE) {
			g = 0.7;
			b = 1;
		} else if (difference < HOUR) {
			r = (minutesAgo / 60) * 0.5;
			g = 1;
		} else if (difference < DAY) {
			r = 0.5 + (minutesAgo / 1440) * 0.5;
			g = 1;
		} else if (difference < WEEK) {
			g = 1 - (minutesAgo / 10080) * 0.5;
			r = 1;
		} else if (difference < MONTH) {
			g = 0.5 - (minutesAgo / 43830) * 0.5;
			r = 1;
		} else if (difference < YEAR) {
			r = 1 - (minutesAgo / 525960);
		} //else, leave it black for >1 year
		r = (Math.round(r*255*0.85).toString(16)).padStart(2, '0');
		g = (Math.round(g*255*0.85).toString(16)).padStart(2, '0');
		b = (Math.round(b*255).toString(16)).padStart(2, '0');
		return `#${r}${g}${b}`;
	},

	'durationString': (ms) => {
		const durString = new Date(ms).toISOString();
		if (ms < DAY) {
			return durString.substr(11,8).replace(/^00:/, '');
		} else {
			const hours = Math.floor(ms/HOUR);
			return `${hours}:${durString.substring(14,19)}`;
		}
	}

};


code:
'use strict';

const session = require('express-session')
	, redisStore = require('connect-redis')(session)
	, { cookieSecret, secureCookies } = require(__dirname+'/../configs/main.js')
	, { redisClient } = require(__dirname+'/../redis.js')
	, production = process.env.NODE_ENV === 'production'
	, { DAY } = require(__dirname+'/timeutils.js')
	, sessionMiddlewareCache = {};

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

	const proto = req.headers['x-forwarded-proto'];
	const sessionMiddleware = sessionMiddlewareCache[proto] || (sessionMiddlewareCache[proto] = session({
			secret: cookieSecret,
			store: new redisStore({
				client: redisClient,
			}),
			resave: false,
			saveUninitialized: false,
			rolling: true,
			cookie: {
				httpOnly: true,
				secure: secureCookies && production && (proto === 'https'),
				sameSite: 'strict',
				maxAge: DAY,
			}
	}));
	return sessionMiddleware(req, res, next);

}