code:
'use strict';

const { Boards } = require(__dirname+'/../../../db/')
	, { relativeColor, relativeString } = require(__dirname+'/../../../lib/converter/timeutils.js')
	, pageQueryConverter = require(__dirname+'/../../../lib/input/pagequeryconverter.js')
	, limit = 20;

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

	const { page, offset, queryString } = pageQueryConverter(req.query, limit);
	const direction = req.query.direction && req.query.direction === 'asc' ? 1 : -1;
	const search = (typeof req.query.search === 'string' ? req.query.search : null);
	let sort = req.query.sort && req.query.sort === 'activity' ? 'activity' : 'popularity';

	if (sort === 'activity') {
		sort = {
			'lastPostTimestamp': direction
		};
	} else {
		sort = {
			'ips': direction,
			'pph': direction,
			'sequence_value': direction
		};
	}

	let filter = {};
	if (req.query.search && search) {
		filter = {
			'search': search
		};
	}
	filter.filter_sfw = req.query.filter_sfw != null;
	filter.filter_abandoned = req.query.filter_abandoned != null;
	filter.filter_unlisted = req.query.filter_unlisted != null;

	let localBoards, pages, maxPage;
	try {
		[ localBoards, pages ] = await Promise.all([
			Boards.boardSort(offset, limit, sort, filter, true),
			Boards.count(filter, true),
		]);
		maxPage = Math.ceil(pages / limit);
	} catch (err) {
		return next(err);
	}

	const now = new Date();
	if (localBoards) {
		for (let i = 0; i < localBoards.length; i++) {
			if (localBoards[i].lastPostTimestamp) {
				const lastPostDate = new Date(localBoards[i].lastPostTimestamp);
				localBoards[i].lastPostTimestamp = {
					text: relativeString(now, lastPostDate, res.locals),
					color: relativeColor(now, lastPostDate)
				};
			}
		}
	}
	res.set('Cache-Control', 'private, max-age=60');

	if (req.path.endsWith('/boards.json')) {
		res.json({
			localBoards,
			page,
			maxPage,
		});
	} else {
		res.render('globalmanageboardlist', {
			localBoards,
			permissions: res.locals.permissions,
			page,
			maxPage,
			query: req.query,
			search,
			queryString,
		});
	}

};


code:
'use strict';

const { Accounts } = require(__dirname+'/../../../db/')
	, roleManager = require(__dirname+'/../../../lib/permission/rolemanager.js')
	, Permission = require(__dirname+'/../../../lib/permission/permission.js');

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

	const editingAccount = await Accounts.findOne(req.params.accountusername);

	if (editingAccount == null) {
		//account does not exist
		return next();
	}

	const accountPermissions = new Permission(editingAccount.permissions);
	//accountPermissions.applyInheritance();

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('editaccount', {
			csrf: req.csrfToken(),
			board: res.locals.board,
			accountUsername: req.params.accountusername,
			accountPermissions,
			roles: roleManager.roles,
			permissions: res.locals.permissions,
		});

};

repo_name: Marzisoft/jschan

license: mit


code:
'use strict';

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

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

	let filter;
	try {
		filter = await Filters.findOne(null, req.params.filterid);
	} catch (err) {
		return next(err);
	}

	if (!filter) {
		return next();
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('globaleditfilter', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			filter,
		});

};


code:
'use strict';

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

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

	let news;
	try {
		news = await News.findOne(req.params.newsid);
	} catch (err) {
		return next(err);
	}

	if (!news) {
		return next();
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('editnews', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			news,
		});

};


code:
'use strict';

const { Roles } = require(__dirname+'/../../../db/')
	, roleManager = require(__dirname+'/../../../lib/permission/rolemanager.js')
	, Permission = require(__dirname+'/../../../lib/permission/permission.js');

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

	const role = await Roles.findOne(req.params.roleid);

	if (role == null) {
		//role does not exist
		return next();
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('editrole', {
			csrf: req.csrfToken(),
			role,
			rolePermissions: new Permission(role.permissions),
			roleNameMap: roleManager.roleNameMap,
			rolePermissionMap: roleManager.rolePermissionMap,
			permissions: res.locals.permissions,
		});

};


code:
'use strict';

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

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

	let filters;
	try {
		filters = await Filters.findForBoard(null);
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('globalmanagefilters', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			filters,
		});

};


code:
'use strict';

module.exports = {
	globalManageReports: require(__dirname+'/reports.js'),
	globalManageBans: require(__dirname+'/bans.js'),
	globalManageLogs: require(__dirname+'/logs.js'),
	globalManageBoards: require(__dirname+'/boards.js'),
	globalManageRecent: require(__dirname+'/recent.js'),
	globalManageNews: require(__dirname+'/news.js'),
	globalManageFilters: require(__dirname+'/filters.js'),
	globalManageAccounts: require(__dirname+'/accounts.js'),
	globalManageSettings: require(__dirname+'/settings.js'),
	globalManageRoles: require(__dirname+'/roles.js'),
	globalEditFilter: require(__dirname+'/editfilter.js'),
	editNews: require(__dirname+'/editnews.js'),
	editAccount: require(__dirname+'/editaccount.js'),
	editRole: require(__dirname+'/editrole.js'),
};


code:
'use strict';

const { Modlogs } = require(__dirname+'/../../../db/')
	, pageQueryConverter = require(__dirname+'/../../../lib/input/pagequeryconverter.js')
	, { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js')
	, decodeQueryIP = require(__dirname+'/../../../lib/input/decodequeryip.js')
	, { isIP } = require('net')
	, limit = 50;

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

	const { page, offset, queryString } = pageQueryConverter(req.query, limit);

	let filter = {};
	const username = (typeof req.query.username === 'string' ? req.query.username : null);
	if (username && !Array.isArray(username)) {
		filter.user = username;
	}
	const uri = (typeof req.query.uri === 'string' ? req.query.uri : null);
	if (uri && !Array.isArray(uri)) {
		filter.board = uri;
	}
	const ipMatch = decodeQueryIP(req.query, res.locals.permissions);
	if (ipMatch != null) {
		if (isIP(ipMatch)) {
			filter['ip.raw'] = ipMatch;
		} else {
			filter['ip.cloak'] = ipMatch;
		}
	}

	let logs, maxPage;
	try {
		[logs, maxPage] = await Promise.all([
			Modlogs.find(filter, offset, limit),
			Modlogs.count(filter),
		]);
		maxPage = Math.ceil(maxPage/limit);
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('globalmanagelogs', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			queryString,
			username,
			uri,
			viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP),
			ip: ipMatch ? req.query.ip : null,
			logs,
			page,
			maxPage,
		});

};


code:
'use strict';

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

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

	let news;
	try {
		news = await News.find();
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('globalmanagenews', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			news,
		});

};


code:
'use strict';

const { Posts } = require(__dirname+'/../../../db/')
	, config = require(__dirname+'/../../../lib/misc/config.js')
	, pageQueryConverter = require(__dirname+'/../../../lib/input/pagequeryconverter.js')
	, { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js')
	, decodeQueryIP = require(__dirname+'/../../../lib/input/decodequeryip.js')
	, limit = 20;

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

	const { dontStoreRawIps } = config.get;
	const { page, offset, queryString } = pageQueryConverter(req.query, limit);
	let ipMatch = decodeQueryIP(req.query, res.locals.permissions);

	let posts;
	try {
		posts = await Posts.getBoardRecent(offset, limit, ipMatch, null, res.locals.permissions);
	} catch (err) {
		return next(err);
	}

	res.set('Cache-Control', 'private, max-age=5');

	if (req.path.endsWith('.json')) {
		res.json(posts.reverse());
	} else {
		res.render('globalmanagerecent', {
			csrf: req.csrfToken(),
			posts,
			permissions: res.locals.permissions,
			viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP) && !dontStoreRawIps,
			page,
			ip: ipMatch ? req.query.ip : null,
			queryString,
		});
	}
};


code:
'use strict';

const { Posts } = require(__dirname+'/../../../db/')
	, pageQueryConverter = require(__dirname+'/../../../lib/input/pagequeryconverter.js')
	, { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js')
	, decodeQueryIP = require(__dirname+'/../../../lib/input/decodequeryip.js')
	, limit = 20;

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

	const { page, offset, queryString } = pageQueryConverter(req.query, limit);
	let ipMatch = decodeQueryIP(req.query, res.locals.permissions);

	let reports;
	try {
		reports = await Posts.getGlobalReports(offset, limit, ipMatch, res.locals.permissions);
	} catch (err) {
		return next(err);
	}

	res.set('Cache-Control', 'private, max-age=5');

	if (req.path.endsWith('/reports.json')) {
		res.json({
			reports,
			page,
			ip: ipMatch ? req.query.ip : null,
			queryString,
		});
	} else {
		res.render('globalmanagereports', {
			csrf: req.csrfToken(),
			reports,
			permissions: res.locals.permissions,
			viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP),
			page,
			ip: ipMatch ? req.query.ip : null,
			queryString,
		});
	}

};


code:
'use strict';

const { Roles } = require(__dirname+'/../../../db/')
	, roleManager = require(__dirname+'/../../../lib/permission/rolemanager.js');

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

	const allRoles = await Roles.find();

	res.set('Cache-Control', 'private, max-age=5');

	if (req.path.endsWith('.json')) {
		res.json(allRoles);
	} else {
		res.render('globalmanageroles', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			allRoles,
			roleNameMap: roleManager.roleNameMap,
			rolePermissionMap: roleManager.rolePermissionMap,
		});
	}

};


code:
'use strict';

const config = require(__dirname+'/../../../lib/misc/config.js')
	, { fontList } = require(__dirname+'/../../../lib/misc/fonts.js')
	, { themes, codeThemes } = require(__dirname+'/../../../lib/misc/themes.js')
	, i18n = require(__dirname+'/../../../lib/locale/locale.js')
	, { getCountryNames, countryCodes } = require(__dirname+'/../../../lib/misc/countries.js');

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

	const { forceActionTwofactor } = config.get;

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('globalmanagesettings', {
			csrf: req.csrfToken(),
			settings: config.get,
			permissions: res.locals.permissions,
			countryNamesMap: getCountryNames(res.locals.locale, { select: 'official' }),
			countryCodes,
			themes,
			codeThemes,
			fontList,
			languages: i18n.getLocales(),
			forceActionTwofactor,
		});

};


code:
'use strict';

const { buildGlobalSettings } = require(__dirname+'/../../lib/build/tasks.js');

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

	let json;
	try {
		json = await buildGlobalSettings();
	} catch (err) {
		return next(err);
	}

	return res.json(json);

};


code:
'use strict';

const { buildHomepage } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html;
	try {
		html = await buildHomepage();
	} catch (err) {
		return next(err);
	}

	return res.set('Cache-Control', 'max-age=0').send(html);

};


code:
'use strict';

module.exports = {
	changePassword: require(__dirname+'/changepassword.js'),
	blockBypass: require(__dirname+'/blockbypass.js'),
	register: require(__dirname+'/register.js'),
	account: require(__dirname+'/account.js'),
	sessions: require(__dirname+'/sessions.js'),
	setupTwoFactor: require(__dirname+'/twofactor.js'),
	myPermissions: require(__dirname+'/mypermissions.js'),
	home: require(__dirname+'/home.js'),
	login: require(__dirname+'/login.js'),
	create: require(__dirname+'/create.js'),
	board: require(__dirname+'/board.js'),
	catalog: require(__dirname+'/catalog.js'),
	banners: require(__dirname+'/banners.js'),
	boardSettings: require(__dirname+'/boardsettings.js'),
	globalSettings: require(__dirname+'/globalsettings.js'),
	customPage: require(__dirname+'/custompage.js'),
	csrfPage: require(__dirname+'/csrf.js'),
	noncePage: require(__dirname+'/nonce.js'),
	randombanner: require(__dirname+'/randombanner.js'),
	news: require(__dirname+'/news.js'),
	captchaPage: require(__dirname+'/captchapage.js'),
	captcha: require(__dirname+'/captcha.js'),
	thread: require(__dirname+'/thread.js'),
	modlog: require(__dirname+'/modlog.js'),
	modloglist: require(__dirname+'/modloglist.js'),
	boardlist: require(__dirname+'/boardlist.js'),
	overboard: require(__dirname+'/overboard.js'),
	overboardCatalog: require(__dirname+'/overboardcatalog.js'),
};


code:
'use strict';

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

	res.render('login', {
		'goto': (typeof req.query.goto === 'string' ? req.query.goto : null)
	});

};


code:
'use strict';

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

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('manageassets', {
			csrf: req.csrfToken(),
		});

};


code:
'use strict';

const Bans = require(__dirname+'/../../../db/bans.js')
	, { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js');

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

	let bans;
	try {
		const showGlobal = res.locals.permissions.get(Permissions.VIEW_BOARD_GLOBAL_BANS);
		const bansBoard = showGlobal ? req.params.board : { '$eq': req.params.board };
		bans = await Bans.getBoardBans(bansBoard);
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('managebans', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP),
			bans,
		});

};


code:
'use strict';

const Posts = require(__dirname+'/../../../db/posts.js');

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

	const page = req.params.page === 'index' ? 1 : Number(req.params.page);
	let maxPage;
	let threads;
	try {
		maxPage = Math.min(Math.ceil((await Posts.getPages(req.params.board)) / 10), Math.ceil(res.locals.board.settings.threadLimit/10)) || 1;
		if (page > maxPage) {
			return next();
		}
		threads = await Posts.getRecent(req.params.board, page, 10, true);
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('board', {
			modview: true,
			page,
			maxPage,
			threads,
			board: res.locals.board,
			csrf: req.csrfToken(),
		});

};


code:
'use strict';

const Posts = require(__dirname+'/../../../db/posts.js');

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

	let threads;
	try {
		threads = await Posts.getCatalog(req.params.board);
	} catch (err) {
		return next(err);
	}

	res
		//.set('Cache-Control', 'private, max-age=5')
		.render('catalog', {
			modview: true,
			threads,
			board: res.locals.board,
			csrf: req.csrfToken(),
		});

};


code:
'use strict';

const CustomPages = require(__dirname+'/../../../db/custompages.js');

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

	let customPages;
	try {
		customPages = await CustomPages.find(req.params.board);
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('managecustompages', {
			csrf: req.csrfToken(),
			customPages,
		});

};


code:
'use strict';

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

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

	let customPage;
	try {
		customPage = await CustomPages.findOneId(req.params.custompageid, req.params.board);
	} catch (err) {
		return next(err);
	}

	if (!customPage) {
		return next();
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('editcustompage', {
			csrf: req.csrfToken(),
			page: customPage,
			board: res.locals.board,
		});

};


code:
'use strict';

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

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

	let filter;
	try {
		filter = await Filters.findOne(req.params.board, req.params.filterid);
	} catch (err) {
		return next(err);
	}

	if (!filter) {
		return next();
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('editfilter', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			filter,
		});

};


code:
'use strict';

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

	return res
		.set('Cache-Control', 'private, max-age=5')
		.render('editpost', {
			'csrf': req.csrfToken(),
			'post': res.locals.post,
			'board': res.locals.board,
			'referer': (req.headers.referer || `/${res.locals.post.board}/manage/thread/${res.locals.post.thread || res.locals.post.postId}.html`) + `#${res.locals.post.postId}`,
		});

};


code:
'use strict';

const Permission = require(__dirname+'/../../../lib/permission/permission.js')
	, { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js');

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

	let staffData = res.locals.board.staff[req.params.staffusername];

	if (staffData == null) {
		//staff does not exist
		return next();
	}

	res
//	.set('Cache-Control', 'private, max-age=5')
		.render('editstaff', {
			csrf: req.csrfToken(),
			board: res.locals.board,
			permissions: res.locals.permissions,
			staffUsername: req.params.staffusername,
			staffPermissions: new Permission(staffData.permissions),
			manageBoardBits: Permissions._MANAGE_BOARD_BITS,
		});

};


code:
'use strict';

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

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

	let filters;
	try {
		filters = await Filters.findForBoard(req.params.board);
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('managefilters', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			filters,
		});

};


code:
'use strict';

module.exports = {
	manageReports: require(__dirname+'/reports.js'),
	manageRecent: require(__dirname+'/recent.js'),
	manageSettings: require(__dirname+'/settings.js'),
	manageFilters: require(__dirname+'/filters.js'),
	manageBans: require(__dirname+'/bans.js'),
	manageLogs: require(__dirname+'/logs.js'),
	manageAssets: require(__dirname+'/assets.js'),
	manageBoard: require(__dirname+'/board.js'),
	manageCatalog: require(__dirname+'/catalog.js'),
	manageThread: require(__dirname+'/thread.js'),
	manageCustomPages: require(__dirname+'/custompages.js'),
	manageMyPermissions: require(__dirname+'/mypermissions.js'),
	editCustomPage: require(__dirname+'/editcustompage.js'),
	editFilter: require(__dirname+'/editfilter.js'),
	editPost: require(__dirname+'/editpost.js'),
	manageStaff: require(__dirname+'/staff.js'),
	editStaff: require(__dirname+'/editstaff.js'),
};


code:
'use strict';

const { Modlogs } = require(__dirname+'/../../../db/')
	, pageQueryConverter = require(__dirname+'/../../../lib/input/pagequeryconverter.js')
	, { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js')
//	, decodeQueryIP = require(__dirname+'/../../../lib/input/decodequeryip.js')
	, limit = 50;

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

	const { page, offset, queryString } = pageQueryConverter(req.query, limit);

	let filter = {
		board: req.params.board
	};
	const username = typeof req.query.username === 'string' ? req.query.username : null;
	if (username) {
		filter.user = username;
	}

	let logs, maxPage;
	try {
		[logs, maxPage] = await Promise.all([
			Modlogs.find(filter, offset, limit),
			Modlogs.count(filter),
		]);
		maxPage = Math.ceil(maxPage/limit);
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('managelogs', {
			csrf: req.csrfToken(),
			queryString,
			username,
			permissions: res.locals.permissions,
			viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP),
			logs,
			page,
			maxPage,
		});

};


code:
'use strict';

const { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js');

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

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('managemypermissions', {
			user: res.locals.user,
			board: res.locals.board,
			permissions: res.locals.permissions,
			manageBoardBits: Permissions._MANAGE_BOARD_BITS,
		});

};


code:
'use strict';

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

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('managemypermissions', {
			permissions: res.locals.permissions,
		});

};


code:
'use strict';

const { Posts } = require(__dirname+'/../../../db/')
	, config = require(__dirname+'/../../../lib/misc/config.js')
	, decodeQueryIP = require(__dirname+'/../../../lib/input/decodequeryip.js')
	, { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js')
	, pageQueryConverter = require(__dirname+'/../../../lib/input/pagequeryconverter.js')
	, limit = 20;

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

	const { dontStoreRawIps } = config.get;
	const { page, offset, queryString } = pageQueryConverter(req.query, limit);
	let ip = decodeQueryIP(req.query, res.locals.permissions);
	const postId = typeof req.query.postid === 'string' ? req.query.postid : null;
	if (postId && +postId === parseInt(postId) && Number.isSafeInteger(+postId)) {
		const fetchedPost = await Posts.getPost(req.params.board, +postId, true);
		if (fetchedPost) {
			ip = decodeQueryIP({ ip: fetchedPost.ip.cloak }, res.locals.permissions);
		}
	}

	let posts;
	try {
		posts = await Posts.getBoardRecent(offset, limit, ip, req.params.board, res.locals.permissions);
	} catch (err) {
		return next(err);
	}

	res.set('Cache-Control', 'private, max-age=5');

	if (req.path.endsWith('.json')) {
		res.json(posts.reverse());
	} else {
		res.render('managerecent', {
			csrf: req.csrfToken(),
			posts,
			permissions: res.locals.permissions,
			viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP) && !dontStoreRawIps,
			page,
			postId,
			queryIp: ip ? req.query.ip : null,
			queryString,
		});
	}
};


code:
'use strict';

const Posts = require(__dirname+'/../../../db/posts.js')
	, { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js');

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

	let reports;
	try {
		reports = await Posts.getReports(req.params.board, res.locals.permissions);
	} catch (err) {
		return next(err);
	}

	res.set('Cache-Control', 'private, max-age=5');

	if (req.path.endsWith('/reports.json')) {
		res.json({
			reports,
		});
	} else {
		res.render('managereports', {
			csrf: req.csrfToken(),
			reports,
			permissions: res.locals.permissions,
			viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP),
		});
	}

};


code:
'use strict';

const { themes, codeThemes } = require(__dirname+'/../../../lib/misc/themes.js')
	, i18n = require(__dirname+'/../../../lib/locale/locale.js')
	, config = require(__dirname+'/../../../lib/misc/config.js')
	, { getCountryNames, countryCodes } = require(__dirname+'/../../../lib/misc/countries.js');

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

	const { forceActionTwofactor } = config.get;

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('managesettings', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			countryNamesMap: getCountryNames(res.locals.locale, { select: 'official' }),
			countryCodes,
			themes,
			codeThemes,
			forceActionTwofactor,
			languages: i18n.getLocales(),
		});

};


code:
'use strict';

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

	res
//	.set('Cache-Control', 'private, max-age=5')
		.render('managestaff', {
			csrf: req.csrfToken(),
			permissions: res.locals.permissions,
			board: res.locals.board,
			user: res.locals.user,
		});

};


code:
'use strict';

const Posts = require(__dirname+'/../../../db/posts.js');

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

	let thread;
	try {
		thread = await Posts.getThread(res.locals.board._id, res.locals.thread.postId, true);
		if (!thread) {
			return next(); //deleted between exists
		}
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('thread', {
			modview: true,
			upLevel: true,
			board: res.locals.board,
			thread,
			csrf: req.csrfToken(),
		});

};


code:
'use strict';

const { Modlogs } = require(__dirname+'/../../db/')
	, { buildModLog } = require(__dirname+'/../../lib/build/tasks.js');

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

	if (!res.locals.date) {
		return next();
	}

	const startDate = res.locals.date.date;
	const { year, month, day } = res.locals.date;
	const endDate = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));

	let html, json;
	try {
		const logs = await Modlogs.findBetweenDate(res.locals.board, startDate, endDate);
		if (!logs || logs.length === 0) {
			return next();
		}
		({ html, json } = await buildModLog({
			board: res.locals.board,
			startDate,
			endDate,
			logs
		}));
	} catch (err) {
		return next(err);
	}

	if (req.path.endsWith('.json')) {
		return res.set('Cache-Control', 'max-age=0').json(json);
	} else {
		return res.set('Cache-Control', 'max-age=0').send(html);
	}

};


code:
'use strict';

const { buildModLogList } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html, json;
	try {
		({ html, json } = await buildModLogList({ board: res.locals.board }));
	} catch (err) {
		return next(err);
	}

	if (req.path.endsWith('.json')) {
		return res.set('Cache-Control', 'max-age=0').json(json);
	} else {
		return res.set('Cache-Control', 'max-age=0').send(html);
	}

};


code:
'use strict';

const { Permissions } = require(__dirname+'/../../lib/permission/permissions.js');

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

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('mypermissions', {
			user: res.locals.user,
			permissions: res.locals.permissions,
			manageBoardBits: Permissions._MANAGE_BOARD_BITS,
		});

};


code:
'use strict';

const { buildNews } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html;
	try {
		html = await buildNews();
	} catch (err) {
		return next(err);
	}

	return res.set('Cache-Control', 'max-age=0').send(html);

};


code:
'use strict';

const { randomBytes } = require('crypto')
	, cache = require(__dirname+'/../../lib/redis/redis.js');

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

	const address = req.params.address;
	const newNonce = (await randomBytes(32)).toString('base64');
	await cache.set(`nonce:${address}:${newNonce}`, 1, 60);

	res.json({
		nonce: newNonce,
	});

};


code:
'use strict';

const { Posts, Boards } = require(__dirname+'/../../db/')
	, cache = require(__dirname+'/../../lib/redis/redis.js')
	, config = require(__dirname+'/../../lib/misc/config.js');

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

	const { overboardLimit, allowCustomOverboard, overboardReverseLinks } = config.get;

	let selectedBoards = []
		, addBoards = []
		, removeBoards = []
		, removeBoardsSet = new Set()
		, includeDefault = true
		, cacheQueryString = '';

	if (allowCustomOverboard === true) {
		//sliced to overboardLimit optimisation because there could only be max 1 thread from each of unique boards anyway
		const addList = (req.query.add ? (typeof req.query.add === 'string' ? req.query.add.split(',') : req.query.add) : [])
			.slice(0, overboardLimit)
			.map(b => b.trim())
			.filter(b => b)
			.sort();
		const removeList = (req.query.rem ? (typeof req.query.rem === 'string' ? req.query.rem.split(',') : req.query.rem) : [])
			.slice(0, overboardLimit)
			.map(b => b.trim())
			.filter(b => b)
			.sort();
		addBoards = [...new Set(addList)];
		removeBoardsSet = new Set(removeList);
		removeBoards = [...removeBoardsSet];
		includeDefault = req.query.include_default === 'true';
		if (!includeDefault && addBoards.length === 0) {
			//...really?
			includeDefault = true;
		}
		//similar to board list
		const cacheQuery = new URLSearchParams({ include_default: includeDefault, add: addBoards, rem: removeBoards });
		cacheQuery.sort();
		cacheQueryString = cacheQuery.toString();
	}

	let threads = (await cache.get(`overboard:${cacheQueryString}`)) || [];
	if (!threads || threads.length === 0) {
		try {
			let listedBoards = [];
			if (includeDefault) {
				listedBoards = await Boards.getLocalListed();
			}
			selectedBoards = listedBoards
				.concat(addBoards)
				.filter(b => !removeBoardsSet.has(b));
			threads = await Posts.getRecent(selectedBoards, 1, overboardLimit, false, false);
			cache.set(`overboard:${cacheQueryString}`, threads, 60);
		} catch (err) {
			return next(err);
		}
	}

	res
		.set('Cache-Control', 'public, max-age=60');

	if (req.path === '/overboard.json') {
		res.json({
			threads,
		});
	} else {
		res.render('overboard', {
			threads,
			includeDefault,
			addBoards,
			removeBoards,
			selectedBoards,
			cacheQueryString,
			allowCustomOverboard,
			overboardReverseLinks,
		});
	}

};


code:
'use strict';

const { Posts, Boards } = require(__dirname+'/../../db/')
	, cache = require(__dirname+'/../../lib/redis/redis.js')
	, config = require(__dirname+'/../../lib/misc/config.js');

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

	const { overboardCatalogLimit, allowCustomOverboard } = config.get;

	let selectedBoards = []
		, addBoards = []
		, removeBoards = []
		, removeBoardsSet = new Set()
		, includeDefault = true
		, cacheQueryString = '';

	if (allowCustomOverboard === true) {
		const addList = (req.query.add ? (typeof req.query.add === 'string' ? req.query.add.split(',') : req.query.add) : [])
			.slice(0, overboardCatalogLimit)
			.map(b => b.trim())
			.filter(b => b)
			.sort();
		const removeList = (req.query.rem ? (typeof req.query.rem === 'string' ? req.query.rem.split(',') : req.query.rem) : [])
			.slice(0, overboardCatalogLimit)
			.map(b => b.trim())
			.filter(b => b)
			.sort();
		addBoards = [...new Set(addList)];
		removeBoardsSet = new Set(removeList);
		removeBoards = [...removeBoardsSet];
		includeDefault = req.query.include_default === 'true';
		if (!includeDefault && addBoards.length === 0) {
			includeDefault = true;
		}
		const cacheQuery = new URLSearchParams({ include_default: includeDefault, add: addBoards, rem: removeBoards });
		cacheQuery.sort();
		cacheQueryString = cacheQuery.toString();
	}

	let threads = (await cache.get(`catalog:${cacheQueryString}`)) || [];
	if (!threads || threads.length === 0) {
		try {
			let listedBoards = [];
			if (includeDefault) {
				listedBoards = await Boards.getLocalListed();
			}
			selectedBoards = listedBoards
				.concat(addBoards)
				.filter(b => !removeBoardsSet.has(b));
			threads = await Posts.getCatalog(selectedBoards, false, overboardCatalogLimit);
			cache.set(`catalog:${cacheQueryString}`, threads, 60);
		} catch (err) {
			return next(err);
		}
	}

	res
		.set('Cache-Control', 'public, max-age=60');

	if (req.path === '/catalog.json') {
		res.json({
			threads,
		});
	} else {
		res.render('overboardcatalog', {
			threads,
			includeDefault,
			addBoards,
			removeBoards,
			selectedBoards,
			cacheQueryString,
			allowCustomOverboard,
		});
	}

};


code:
'use strict';

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

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('mypermissions', {
			user: res.locals.user,
			permissions: res.locals.permissions,
		});

};


code:
'use strict';

const Boards = require(__dirname+'/../../db/boards.js');

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

	if (!req.query.board || typeof req.query.board !== 'string') {
		return next();
	}

	let banner;
	try {
		banner = await Boards.randomBanner(req.query.board);
	} catch (err) {
		return next(err);
	}

	if (!banner) {
		//non existing boards will show default banner, but it doesnt really matter.
		return res.redirect('/file/defaultbanner.png');
	}

	return res.redirect(`/banner/${req.query.board}/${banner}`);

};


code:
'use strict';

const { buildRegister } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html;
	try {
		html = await buildRegister();
	} catch (err) {
		return next(err);
	}

	return res.set('Cache-Control', 'max-age=0').send(html);

};


code:
'use strict';

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

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

	let sessions;
	try {
		sessions = await redis.getPattern(`sess:*:${res.locals.user.username}`);
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'private, max-age=5')
		.render('sessions', {
			csrf: req.csrfToken(),
			user: res.locals.user,
			permissions: res.locals.permissions,
			currentSessionKey: `sess:${req.sessionID}`,
			sessions,
		});

};


code:
'use strict';

const { buildThread } = require(__dirname+'/../../lib/build/tasks.js');

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

	let html, json;
	try {
		const buildThreadData = await buildThread({
			threadId: res.locals.thread.postId,
			board: res.locals.board
		});
		/* unlikely, but postsExists middleware can be true, but this can be null if deleted. so just next() to 404
		wont matter in the build-workers that call this because they dont destructure and never cause the bug */
		if (!buildThreadData) {
			return next();
		}
		({ html, json } = buildThreadData);
	} catch (err) {
		return next(err);
	}

	if (req.path.endsWith('.json')) {
		return res.set('Cache-Control', 'max-age=0').json(json);
	} else {
		return res.set('Cache-Control', 'max-age=0').send(html);
	}

};


code:
'use strict';

const redis = require(__dirname+'/../../lib/redis/redis.js')
	, { Ratelimits } = require(__dirname+'/../../db/')
	, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, OTPAuth = require('otpauth')
	, QRCode = require('qrcode');

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

	if (res.locals.user.twofactor) {
		// User already has 2fa enabled
		return res.redirect('/account.html');
	}

	// Ratelimit QR code generation
	const username = res.locals.user.username;
	const ratelimit = await Ratelimits.incrmentQuota(username, '2fa', 10);
	if (ratelimit > 100) {
		const { __ } = res.locals;
		return dynamicResponse(req, res, 429, 'message', {
			'title': __('Ratelimited'),
			'message': __('Please wait before generating another 2FA QR code.'),
		});
	}

	const { meta } = config.get;

	let qrCodeText = ''
		, secretBase32 = '';
	try {
		const totp = new OTPAuth.TOTP({
			issuer: meta.url || 'jschan',
			label: meta.siteName || 'jschan',
			algorithm: 'SHA256',
		});
		const secret = totp.secret;
		secretBase32 = secret.base32;
		await redis.set(`twofactor_tempsecret:${username}`, secretBase32, 300); //store validation secret temporarily in redis
		const qrCodeURL = totp.toString();
		qrCodeText = await QRCode.toString(qrCodeURL, { type: 'utf8' });
	} catch (err) {
		return next(err);
	}

	res
		.set('Cache-Control', 'no-cache')
		.render('twofactor', {
			csrf: req.csrfToken(),
			qrCodeText,
			secretBase32,
		});

};


jschan

license: MIT

type: opencollective

license: ISC

rlp: bin/rlp

deprecated: Use @eslint/config-array instead

type: github

deprecated: Use @eslint/object-schema instead

node-pre-gyp: bin/node-pre-gyp

deprecated: Use your platform's native atob() and btoa() methods instead

deprecated: This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions

deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

deprecated: Use your platform's native DOMException instead

deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.

deprecated: Upgrade to fsevents v2 to mitigate potential security issues

deprecated: Rimraf versions prior to v4 are no longer supported

deprecated: Glob versions prior to v9 are no longer supported

deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.

os: darwin linux win32 freebsd openbsd netbsd sunos android

deprecated: Use your platform's native performance.now() and performance.timeOrigin.


test: npm run test:unit

test:coverage: jest --verbose --testPathIgnorePatterns=./test/ --collectCoverage --coverageReporters cobertura

test:integration-verbose: jest --verbose --detectOpenHandles --testPathPattern=./test/

test:integration: jest --testPathPattern=./test/

test:all: jest --verbose

setup: npx gulp generate-favicon && npx gulp default

start: pm2 start ecosystem.config.js --env production

start-dev: pm2 start ecosystem.config.js --env development

author: Thomas Lynch (fatchan) <tom@69420.me>


code:
'use strict';

const config = require(__dirname+'/../lib/misc/config.js');

module.exports = class Schedule {

	constructor (func, interval, immediate, condition) {
		this.func = func;
		this.interval = interval;
		this.immediate = immediate;
		this.condition = condition;
		this.intervalId = null;
		this.update();
	}

	//start the schedule
	start () {
		if (!this.intervalId) {
			if (this.immediate) {
				this.func();
			}
			this.intervalId = setInterval(this.func, this.interval);
		}
	}

	//stop the schedule
	stop () {
		clearInterval(this.interval);
		this.intervalId = null;
	}

	//check config and either start or stop
	update () {
		if (!this.condition || config.get[this.condition]) {
			this.start();
		} else {
			this.stop();
		}
	}

};


code:
'use strict';

process
	.on('uncaughtException', console.error)
	.on('unhandledRejection', console.error);

const Mongo = require(__dirname+'/../db/db.js')
	, config = require(__dirname+'/../lib/misc/config.js')
	, { addCallback } = require(__dirname+'/../lib/redis/redis.js');

(async () => {

	await Mongo.connect();
	await Mongo.checkVersion();
	await config.load();

	//start all the scheduled tasks
	const schedules = require(__dirname+'/tasks/index.js');

	//update the schedules to start/stop timer after config change
	addCallback('config', () => {
		Object.values(schedules).forEach(sc => {
			sc.update();
		});
	});

	//update board stats and homepage task, use cron and bull for proper timing
	require(__dirname+'/../lib/build/queue.js').push({
		'task': 'updateStats',
		'options': {}
	}, {
		'repeat': {
			'cron': '0 * * * *'
		}
	});

})();


code:
'use strict';

const { debugLogs } = require(__dirname+'/../../configs/secrets.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, { Boards } = require(__dirname+'/../../db/')
	, timeUtils = require(__dirname+'/../../lib/converter/timeutils.js')
	, deleteBoard = require(__dirname+'/../../models/forms/deleteboard.js');

module.exports = {

	func: async () => {

		if (config.get.abandonedBoardAction === 0) {
			return;
		}

		const abandonedBoards = await Boards.getAbandoned(config.get.abandonedBoardAction);
		if (abandonedBoards.length === 0) {
			return;
		}

		if (config.get.abandonedBoardAction <= 2) {
			debugLogs && console.log(`Locking${config.get.abandonedBoardAction === 2 ? '+Unlisting' : ''} ${abandonedBoards.length} abandoned boards.`);
			const abandonedURIs = abandonedBoards.map(b => b._id);
			Boards.unlistMany(abandonedURIs);
		} else { //must be 2
			//run delete board model for each
			debugLogs && console.log(`Deleting ${abandonedBoards.length} abandoned boards.`);
			for (let board of abandonedBoards) {
				try {
					await deleteBoard(board._id, board);
				} catch (err) {
					debugLogs && console.log('Error deleting abandoned board:', err);
				}
			}
		}

	},

	interval: timeUtils.DAY,
	immediate: true,
	condition: 'abandonedBoardAction'

};


code:
'use strict';

const deleteOld = require(__dirname+'/../../lib/file/deleteold.js')
	, timeUtils = require(__dirname+'/../../lib/converter/timeutils.js');

module.exports = {

	func: async () => {
		return deleteOld('captcha', Date.now()-(timeUtils.MINUTE*5));
	},
	interval: timeUtils.MINUTE*5,
	immediate: true,
	condition: null

};


code:
'use strict';

const { debugLogs } = require(__dirname+'/../../configs/secrets.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, Redis = require(__dirname+'/../../lib/redis/redis.js')
	, { Boards, Accounts } = require(__dirname+'/../../db/')
	, timeUtils = require(__dirname+'/../../lib/converter/timeutils.js');

module.exports = {

	func: async () => {

		if (config.get.inactiveAccountAction === 0) {
			return;
		}

		const inactiveAccounts = await Accounts.getInactive(config.get.inactiveAccountTime);
		if (inactiveAccounts.length === 0) {
			return;
		}

		const cacheDeleteSet = new Set()
			, boardBulkWrites = []
			, inactiveWithBoards = inactiveAccounts.filter(acc => {
				//only deal with boards if they have any (acc deletes still processed later)
				return acc.ownedBoards.length > 0 || acc.staffBoards.length > 0;
			});

		let boardsPromise = null
			, accountsPromise = null;

		//create promise for boards (remove staff.${username})
		inactiveWithBoards.forEach(acc => {
			//remove account from staff and owner of all their boards
			const accountBoards = [...acc.ownedBoards, ...acc.staffBoards];
			accountBoards.forEach(b => cacheDeleteSet.add(b));
			boardBulkWrites.push({
				updateOne: {
					filter: {
						'_id': {
							'$in': accountBoards,
							//better to do per board for staff unsets? or per-account...
						}
					},
					update: {
						$unset: {
							[`staff.${acc._id}`]: '',
						},
					}
				}
			});
			acc.ownedBoards.forEach(ob => {
				boardBulkWrites.push({
					updateOne: {
						filter: {
							'_id': ob,
						},
						update: {
							'$set': {
								'owner': null,
							},
						}
					}
				});
			});
		});
		if (boardBulkWrites.length > 0) {
			boardsPromise = Boards.db.bulkWrite(boardBulkWrites);
		}

		//create promise for accounts (clearing staff positions or deleting fuly)
		if (config.get.inactiveAccountAction === 2) {
			debugLogs && console.log(`Deleting ${inactiveAccounts.length} inactive accounts`);
			const inactiveUsernames = inactiveAccounts.map(acc => acc._id);
			if (inactiveUsernames.length > 0) {
				accountsPromise = Accounts.deleteMany(inactiveUsernames);
			}
		} else {
			debugLogs && console.log(`Removing staff positions from ${inactiveWithBoards.length} inactive accounts`);
			const inactiveUsernames = inactiveWithBoards.map(acc => acc._id);
			if (inactiveUsernames.length > 0) {
				accountsPromise = Accounts.clearStaffAndOwnedBoards(inactiveUsernames);
			}
		}

		//execute promises
		await Promise.all([
			accountsPromise,
			boardsPromise,
		]);

		//clear caches
		cacheDeleteSet.forEach(b => Redis.del(`board:${b}`));
		//users: cache already handled by Accounts.deleteMany or Accounts.clearStaffAndOwnedBoards

	},

	interval: timeUtils.DAY,
	immediate: true,
	condition: 'inactiveAccountAction'

};


code:
'use strict';

const fs = require('fs-extra')
	, Schedule = require(__dirname+'/../Schedule.js');

fs.readdirSync(__dirname).forEach(file => {
	if (file === 'index.js') { return; }
	const name = file.substring(0, file.length-3);
	const { func, interval, immediate, condition } = require(__dirname+'/'+file);
	module.exports[name] = new Schedule(func, interval, immediate, condition);
});


code:
'use strict';

const Mongo = require(__dirname+'/../../db/db.js')
	, { Posts, Modlogs } = require(__dirname+'/../../db/')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, { createHash, randomBytes } = require('crypto')
	, timeUtils = require(__dirname+'/../../lib/converter/timeutils.js');

module.exports = {

	func: async (days) => {

		//make a mongoid set to the date before which we prunt IPs on, because _id holds date and is always indexed
		const beforeDate = new Date();
		beforeDate.setDate(beforeDate.getDate() - (days || config.get.pruneIps));
		const beforeDateMongoId = Mongo.ObjectId.createFromTime(Math.floor(beforeDate.getTime()/1000));

		//use random secret for this schedule run and generate the bulkwrite for pruning IP
		const tempIpHashSecret = randomBytes(20).toString('base64');
		const mapBulkWrite = (document) => {
			const randomIP = createHash('sha256').update(tempIpHashSecret + document.ip.cloak).digest('base64');
			return {
				updateOne: {
					filter: {
						_id: document._id,
					},
					update: {
						$set: {
							'ip.pruned': true,
							'ip.raw': `${randomIP.slice(-10)}.PRUNED`,
							'ip.cloak': `${randomIP.slice(-10)}.PRUNED`,
						}
					}
				}
			};
		};

		const pruneQuery = {
			'_id': {
				'$lte': beforeDateMongoId,
			},
			'ip.pruned': {
				'$ne': true
			}
		};

		//No need for promise.all
		[Posts, Modlogs].forEach(async (coll, i) => {
			const bulkWrites = (await coll.db
				.find(pruneQuery)
				.toArray())
				.map(mapBulkWrite);
			if (bulkWrites.length > 0) {
				await coll.db.bulkWrite(bulkWrites);
				console.log(`Randomised ips on ${bulkWrites.length} ${i === 0 ? 'posts' : 'modlog entries'}`);
			}
		});

	},

	interval: timeUtils.DAY,
	immediate: true,
	condition: 'pruneIps',

};


code:
'use strict';

const Files = require(__dirname+'/../../db/files.js')
	, { debugLogs } =  require(__dirname+'/../../configs/secrets.js')
	, { remove } = require('fs-extra')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, timeUtils = require(__dirname+'/../../lib/converter/timeutils.js');

module.exports = {

	func: async (fileNames) => {
		const query = {
			'count': {
				'$lte': 0
			}
		};
		if (fileNames) {
			query['_id'] = {
				'$in': fileNames
			};
		}
		const unreferenced = await Files.db.find(query, {
			'projection': {
				'count': 0,
				'size': 0
			}
		}).toArray();
		await Files.db.deleteMany(query);
		await Promise.all(unreferenced.map(async file => {
			debugLogs && console.log('Pruning', file._id);
			return Promise.all(
				[remove(`${uploadDirectory}/file/${file._id}`)]
					.concat(file.exts ? file.exts.filter(ext => ext).map(ext => {
						remove(`${uploadDirectory}/file/thumb/${file._id.split('.')[0]}${ext}`);
					}) : [])
			);
		}));
	},
	interval: timeUtils.DAY,
	immediate: true,
	condition: null,

};


code:
'use strict';

const fetch = require('node-fetch')
	, { debugLogs } = require(__dirname+'/../../configs/secrets.js')
	, config = require(__dirname+'/../../lib/misc/config.js')
	, Redis = require(__dirname+'/../../lib/redis/redis.js')
	, { Boards } = require(__dirname+'/../../db/')
	, { outputFile } = require('fs-extra')
	, { SocksProxyAgent } = require('socks-proxy-agent')
	, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
	, { version } = require(__dirname+'/../../package.json')
	, timeUtils = require(__dirname+'/../../lib/converter/timeutils.js');

module.exports = {

	func: async () => {

		const { meta, logo, following, blacklist, proxy } = config.get;
		const label = 'updating webring';
		const start = process.hrtime();

		const agent = proxy.enabled ? new SocksProxyAgent(require('url').parse(proxy.address)) : null;
		const visited = new Map();
		const siteNames = new Set();
		let known = new Set(following);
		let webringBoards = []; //list of webring boards
		while (known.size > visited.size) {
			//get sites we havent visited yet
			const toVisit = [...known].filter(url => !visited.has(url));
			let rings = await Promise.all(toVisit.map(url => {
				visited.set(url, (visited.get(url)||1));
				return fetch(url, {
					timeout: 20000,
					agent,
					headers: {
						'User-Agent': `jschan/${version} (https://gitgud.io/fatchan/jschan/)`,
					}
				})
					.then(res => res.json())
					.catch(() => {});
			}));
			for (let i = 0; i < rings.length; i++) {
				const ring = rings[i];
				if (!ring || !ring.name || !ring.endpoint || !ring.url //malformed
					|| ring.endpoint.includes(meta.url) //own site
					|| blacklist.some(x => ring.endpoint.includes(x)) //blacklisted (for the case of a mirror to the endpoint)
					|| visited.get(ring.endpoint) > 1) { //already seen endpoint (for multiple domain sites)
					continue;
				}
				visited.set(ring.endpoint, (visited.get(ring.endpoint)||1)+1);
				if (ring.following && ring.following.length > 0) {
					//filter their folowing by blacklist/self and add to known sites
					ring.following
						.filter(url => !blacklist.some(x => url.includes(x)) && !url.includes(meta.url))
						.forEach(url => known.add(url));
				}
				siteNames.add(ring.name);
				if (ring.boards && ring.boards.length > 0) {
					//add some stuff for the boardlist and then add their boards
					ring.boards.forEach(board => {
						board.siteName = ring.name;
						//convert to numbers because old infinity webring plugin returns string
						board.sequence_value = parseInt(board.totalPosts) || 0;
						board.pph = parseInt(board.postsPerHour) || 0;
						if (board.postsPerDay != null) {
							board.ppd = parseInt(board.postsPerDay) || 0;
						}
						board.ips = parseInt(board.uniqueUsers) || 0;
						board.settings = {
							sfw: !board.nsfw,
							name: board.title,
							description: board.subtitle,
						};
						if (board.lastPostTimestamp) {
							board.lastPostTimestamp = new Date(board.lastPostTimestamp.toString());
						}
					});
					webringBoards = webringBoards.concat(ring.boards);
				}
			}
		}

		await Boards.db.deleteMany({ webring: true });
		if (webringBoards.length > 0) {
			webringBoards = webringBoards.map(x => {
				x.webring = true;
				delete x._id; //would cause inserterror, but still dont wanna let that happen
				return x;
			});
			await Boards.db.insertMany(webringBoards);
			await Redis.set('webringsites', [...siteNames].sort((a, b) => a.localeCompare(b)));
		}

		//update webring.json
		const boards = await Boards.webringBoards();
		const json = {
			name: meta.siteName,
			url: meta.url,
			endpoint: `${meta.url}/webring.json`,
			logo,
			following,
			blacklist,
			known: [...known],
			boards: boards.map(b => {
				//map local boards to webring format
				return {
					uri: b._id,
					title: b.settings.name,
					subtitle: b.settings.description,
					path: `${meta.url}/${b._id}/`,
					postsPerHour: b.pph,
					postsPerDay: b.ppd,
					totalPosts: b.sequence_value-1,
					uniqueUsers: b.ips,
					nsfw: !b.settings.sfw,
					tags: b.tags,
					lastPostTimestamp: b.lastPostTimestamp,
				};
			}),
		};
		await outputFile(`${uploadDirectory}/json/webring.json`, JSON.stringify(json));

		const end = process.hrtime(start);
		debugLogs && console.log(timeUtils.timeDiffString(label, end));
	},
	interval: timeUtils.MINUTE*15,
	immediate: true,
	condition: 'enableWebring'

};


code:
'use strict';

process
	.on('uncaughtException', console.error)
	.on('unhandledRejection', console.error);

const config = require(__dirname+'/lib/misc/config.js')
	, express = require('express')
	, path = require('path')
	, app = express()
	, server = require('http').createServer(app)
	, cookieParser = require('cookie-parser')
	, { port, cookieSecret, debugLogs, google, hcaptcha, yandex } = require(__dirname+'/configs/secrets.js')
	, Mongo = require(__dirname+'/db/db.js')
	, dynamicResponse = require(__dirname+'/lib/misc/dynamic.js')
	, commit = require(__dirname+'/lib/misc/commit.js')
	, { version } = require(__dirname+'/package.json')
	, formatSize = require(__dirname+'/lib/converter/formatsize.js')
	, CachePugTemplates = require('cache-pug-templates')
	, { Permissions } = require(__dirname+'/lib/permission/permissions.js')
	, i18n = require(__dirname+'/lib/locale/locale.js');

(async () => {

	const env = process.env.NODE_ENV;
	const production = env === 'production';
	debugLogs && console.log('process.env.NODE_ENV =', env);
	process.env.NO_CAPTCHA && console.warn('WARNING, RUNNING WITH process.env.NO_CAPTCHA, CAPTCHA CHECKS ARE SKIPPED!');

	// connect to mongodb
	debugLogs && console.log('CONNECTING TO MONGODB');
	await Mongo.connect();
	await Mongo.checkVersion();
	await config.load();

	// connect to redis
	debugLogs && console.log('CONNECTING TO REDIS');
	const redis = require(__dirname+'/lib/redis/redis.js');

	// load roles early
	const roleManager = require(__dirname+'/lib/permission/rolemanager.js');
	await roleManager.load();

	// disable useless express header
	app.disable('x-powered-by');
	//query strings
	app.set('query parser', 'simple');
	// parse forms
	app.use(express.urlencoded({extended: false}));
	// parse cookies
	app.use(cookieParser(cookieSecret));

	// session store
	const sessionMiddleware = require(__dirname+'/lib/middleware/permission/usesession.js');

	// connect socketio
	const Socketio = require(__dirname+'/lib/misc/socketio.js');
	debugLogs && console.log('STARTING WEBSOCKET');
	Socketio.connect(server, sessionMiddleware);

	//trust proxy for nginx
	app.set('trust proxy', 1);

	// use pug view engine
	const views = path.join(__dirname, 'views/pages');
	app.set('view engine', 'pug');
	app.set('views', views);

	const loadAppLocals = () => {
		const { language, cacheTemplates, boardDefaults, globalLimits, captchaOptions, archiveLinksURL,
			reverseImageLinksURL, meta, enableWebring, globalAnnouncement, enableWeb3, ethereumLinksURL } = config.get;
		//cache loaded templates
		app.cache = {};
		app[cacheTemplates === true ? 'enable' : 'disable']('view cache');
		//default settings
		app.locals.Permissions = Permissions;
		app.locals.defaultTheme = boardDefaults.theme;
		app.locals.defaultCodeTheme = boardDefaults.codeTheme;
		app.locals.globalLimits = globalLimits;
		app.locals.ethereumLinksURL = ethereumLinksURL;
		app.locals.archiveLinksURL = archiveLinksURL;
		app.locals.reverseImageLinksURL = reverseImageLinksURL;
		app.locals.enableWebring = enableWebring;
		app.locals.enableWeb3 = enableWeb3;
		app.locals.commit = commit;
		app.locals.version = version;
		app.locals.meta = meta;
		app.locals.postFilesSize = formatSize(globalLimits.postFilesSize.max);
		app.locals.googleRecaptchaSiteKey = google ? google.siteKey : '';
		app.locals.hcaptchaSiteKey = hcaptcha ? hcaptcha.siteKey : '';
		app.locals.yandexSiteKey = yandex ? yandex.siteKey : '';
		app.locals.globalAnnouncement = globalAnnouncement;
		app.locals.captchaOptions = captchaOptions;
		app.locals.globalLanguage = language;
		i18n.init(app.locals);
		app.locals.setLocale(app.locals, language);
	};
	loadAppLocals();
	redis.addCallback('config', loadAppLocals);

	// routes
	if (!production) {
		app.use(express.static(__dirname+'/static', { redirect: false }));
		app.use(express.static(__dirname+'/static/html', { redirect: false }));
		app.use(express.static(__dirname+'/static/json', { redirect: false }));
	}

	//localisation
	const { setGlobalLanguage } = require(__dirname+'/lib/middleware/locale/locale.js');
	app.use(i18n.init);
	app.use(setGlobalLanguage);

	//referer check middleware
	const referrerCheck = require(__dirname+'/lib/middleware/misc/referrercheck.js');
	app.use(referrerCheck);

	app.use('/forms', require(__dirname+'/controllers/forms.js'));
	app.use('/', require(__dirname+'/controllers/pages.js'));

	//404 catchall
	app.get('*', (req, res) => {
		res.status(404).render('404');
	});

	// catch any unhandled errors
	app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
		const { __ } = res.locals;
		let errStatus = 500;
		let errMessage = 'Internal Server Error';
		if (err.code === 'EBADCSRFTOKEN') {
			errMessage = 'Invalid CSRF token';
			errStatus= 403;
		}
		if (err.type != null) {
			//body-parser errors
			errStatus = err.status;
			switch (err.type) {
				case 'charset.unsupported':
				case 'entity.parse.failed':
				case 'entity.verify.failed':
				case 'encoding.unsupported':
				case 'request.size.invalid':
				case 'parameters.too.many':
					//no need to give an error for every one, since these will never happen to a legit user anyway
					errMessage = 'Invalid request body';
					break;
				case 'request.aborted':
					errMessage = 'Client aborted request';
					break;
				case 'entity.too.large':
					errMessage = 'Your upload was too large';
					break;
				default:
					break;
			}
		}
		if (errStatus === 500 && errMessage ===  'Internal Server Error') {
			//no specific/friendly error, probably something worth logging
			console.error(err);
		}
		return dynamicResponse(req, res, errStatus, 'message', {
			'title': __(errStatus === 500 ? 'Internal Server Error' : 'Bad Request'),
			'error': __(errMessage),
			'redirect': req.headers.referer || '/'
		});
	});

	//listen
	server.listen(port, (process.env.JSCHAN_IP || '127.0.0.1'), () => {
		new CachePugTemplates({ app, views }).start();
		debugLogs && console.log(`LISTENING ON :${port}`);
		//let PM2 know that this is ready for graceful reloads and to serialise startup
		if (typeof process.send === 'function') {
			//make sure we are a child process of PM2 i.e. not in dev
			debugLogs && console.log('SENT READY SIGNAL TO PM2');
			process.send('ready');
		}
	});

	const gracefulStop = () => {
		debugLogs && console.log('SIGINT SIGNAL RECEIVED');
		// Stops the server from accepting new connections and finishes existing connections.
		Socketio.io.close((err) => {
			// if error, log and exit with error (1 code)
			debugLogs && console.log('CLOSING SERVER');
			if (err) {
				console.error(err);
				process.exit(1);
			}
			// close database connection
			debugLogs && console.log('DISCONNECTING MONGODB');
			Mongo.client.close();
			//close redis connection
			debugLogs && console.log('DISCONNECTING REDIS');
			redis.close();
			// now close without error
			process.exit(0);
		});
	};

	//graceful stop
	process.on('SIGINT', gracefulStop);
	process.on('message', (message) => {
		if (message === 'shutdown') {
			gracefulStop();
		}
	});

})();


code:
const fetch = require('node-fetch');
const FormData = require('form-data');
const fs = require('fs-extra');

module.exports = () => describe('Test post modactions', () => {

	let sessionCookie
		, csrfToken;

	test('login as admin',  async () => {
		const params = new URLSearchParams();
		params.append('username', 'admin');
		params.append('password', process.env.TEST_ADMIN_PASSWORD);
		const response = await fetch('http://localhost/forms/login', {
			headers: {
				'x-using-xhr': 'true',
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		const rawHeaders = response.headers.raw();
		expect(rawHeaders['set-cookie']).toBeDefined();
		expect(rawHeaders['set-cookie'][0]).toMatch(/^connect\.sid/);
		sessionCookie = rawHeaders['set-cookie'][0];
		csrfToken = await fetch('http://localhost/csrf.json', { headers: { 'cookie': sessionCookie }})
			.then(res => res.json())
			.then(json => json.token);
	});

	jest.setTimeout(5*60*1000); //give a generous timeout
	test('make new 10 threads with 10 replies each',  async () => {
		const threadParams = new URLSearchParams();
		threadParams.append('message', Math.random());
		threadParams.append('captcha', '000000');
		const promises = [];
		for (let t = 0; t < 10; t++) {
			const promise = fetch('http://localhost/forms/board/test/post', {
				headers: {
					'x-using-xhr': 'true',
				},
				method: 'POST',
				body: threadParams
			}).then(async (response) => {
				expect(response.ok).toBe(true);
				const thread = (await response.json()).postId;
				for (let r = 0; r < 10; r++) {
					const replyParams = new URLSearchParams();
					replyParams.append('message', Math.random());
					replyParams.append('thread', thread);
					replyParams.append('captcha', '000000');
					const promise2 = await fetch('http://localhost/forms/board/test/post', {
						headers: {
							'x-using-xhr': 'true',
						},
						method: 'POST',
						body: replyParams
					}).then(async (response2) => {
						expect(response2.ok).toBe(true);
					});
					promises.push(promise2);
				}
			});
			promises.push(promise);
		}
		await Promise.all(promises); //wait for all posts to go through
		jest.setTimeout(5*1000); //back to normal timeout
		await new Promise((resolve) => { setTimeout(resolve, 1000); }); //wait for async builds
	});

	test('bumplock, lock, and sticky 5 random posts from /test/',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const params = new URLSearchParams({
			_csrf: csrfToken,
			sticky: '1',
			bumplock: '1',
			lock: '1',
		});
		for (let i = 0; i < 5; i++) {
			const thread = threads[Math.floor(Math.random() * threads.length)];
			params.append('checkedposts', thread.postId);
		}
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
		await new Promise((resolve) => { setTimeout(resolve, 1000); }); //wait for async builds
	});

	test('remove the bumplock, lock and sticky on any threads',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const params = new URLSearchParams({
			_csrf: csrfToken,
			sticky: '0',
			bumplock: '1', //these are a "toggle",
			lock: '1',
		});
		threads.filter(t => t.locked).forEach(t => params.append('checkedposts', t.postId));
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('lower reply limit',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			name: 'test',
			description: '',
			tags: '',
			announcement: '',
			theme: 'yotsuba-b',
			code_theme: 'ir-black',
			custom_css: '',
			language: 'en-GB',
			enable_tegaki: 'true',
			max_files: '5',
			files_allow_video: 'true',
			files_allow_image: 'true',
			files_allow_animated_image: 'true',
			files_allow_audio: 'true',
			user_post_spoiler: 'true',
			user_post_unlink: 'true',
			default_name: 'Anon',
			user_post_delete: 'true',
			min_thread_message_length: '0',
			min_reply_message_length: '0',
			max_thread_message_length: '20000',
			max_reply_message_length: '20000',
			thread_limit: '100',
			reply_limit: '20',
			bump_limit: '500',
			file_r9k_mode: '0',
			message_r9k_mode: '0',
			delete_protection_count: '0',
			delete_protection_age: '0',
			lock_mode: '0',
			captcha_mode: '2',
			pph_trigger: '50',
			pph_trigger_action: '2',
			tph_trigger: '10',
			tph_trigger_action: '1',
			lock_reset: '0',
			captcha_reset: '0',
			filters: '',
			filter_mode: '0',
			ban_duration: '0'
		});
		const response = await fetch('http://localhost/forms/board/test/settings', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.status).toBe(200);
	});

	jest.setTimeout(5*60*1000); //give generous timeout
	test('make new cyclic thread and check it prunes replies after the limit',  async () => {

		//new thread
		const threadParams = new URLSearchParams();
		threadParams.append('message', Math.random());
		threadParams.append('captcha', '000000');
		const response = await fetch('http://localhost/forms/board/test/post', {
			headers: {
				'x-using-xhr': 'true',
			},
			method: 'POST',
			body: threadParams
		});
		expect(response.ok).toBe(true);
		const thread = (await response.json()).postId;

		//make it cyclic
		const params = new URLSearchParams({
			_csrf: csrfToken,
			cyclic: '1',
			checkedposts: thread,
		});
		const response2 = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response2.ok).toBe(true);

		//make the replies
		const promises = [];
		for (let r = 0; r < 25; r++) {
			const replyParams = new URLSearchParams();
			replyParams.append('message', Math.random());
			replyParams.append('thread', thread);
			replyParams.append('captcha', '000000');
			const promise = await fetch('http://localhost/forms/board/test/post', {
				headers: {
					'x-using-xhr': 'true',
				},
				method: 'POST',
				body: replyParams
			}).then(response3 => {
				expect(response3.ok).toBe(true);
			});
			promises.push(promise);
		}
		await Promise.all(promises); //wait for all posts to go through

		//check the replies in json
		const response3 = await fetch(`http://localhost/test/thread/${thread}.json`).then(res => res.json());
		expect(response3.replies.length).toBe(20);
		jest.setTimeout(5*1000); //back to normal timeout

	});

	test('move/merge a thread in same board',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedposts: threads[Math.floor(threads.length/2)].postId,
			move: '1',
			move_to_thread: threads[0].postId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('move/merge a thread cross board',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const threads2 = await fetch('http://localhost/test2/catalog.json').then(res => res.json());
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedposts: threads[0].postId,
			move: '1',
			move_to_board: 'test2',
			move_to_thread: threads2[0].postId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('move/merge a thread cross board (new thread)',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedposts: threads[0].postId,
			move: '1',
			move_to_board: 'test2',
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('move/merge a thread cross board (other way)',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const threads2 = await fetch('http://localhost/test2/catalog.json').then(res => res.json());
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedposts: threads2[0].postId,
			move: '1',
			move_to_board: 'test',
			move_to_thread: threads[0].postId,
		});
		const response = await fetch('http://localhost/forms/board/test2/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('delete 5 random posts from /test/',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const params = new URLSearchParams({
			_csrf: csrfToken,
			delete: '1',
		});
		for (let i = 0; i < 5; i++) {
			const thread = threads[Math.floor(Math.random() * threads.length)];
			params.append('checkedposts', thread.postId);
		}
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	let reportedPost;
	test('test local report + global report',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const randomThread = threads[Math.floor(Math.random() * threads.length)];
		reportedPost = randomThread;
		const randomThreadId = randomThread.postId;
		const params = new URLSearchParams({
			_csrf: csrfToken,
			report: '1',
			global_report: '1',
			report_reason: 'test',
			checkedposts: randomThreadId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('test post editing, add a bunch of markdown',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
		const params = new URLSearchParams({
			_csrf: csrfToken,
			board: 'test',
			postId: randomThreadId,
			subject: 'NEW SUBJECT',
			email: 'NEW EMAIL',
			name: 'NEW NAME',
			message: `>greentext
<pinktext
==title==
''bold''
__underline__
~strikethrough~~
||spoiler text||
**italic**
(((detected)))
##2d9+3
https://example.com
[Board Rules](https://your.imageboard/a/page/rules.html)(staff only)
![embed](https://your.imageboard/image.png)
>>123
>>>/test/
>>>/test/123
\`inline monospace\`
[code]language
int main() {...}
[/code]

[code]aa
∧＿∧
( ・ω・) Let's try that again.
[/code]`,
			log_message: 'test edit',
		});
		const response = await fetch('http://localhost/forms/editpost', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	let postId;
	test('make post with image',  async () => {
		const threadParams = new FormData({
			message: Math.random(),
			captcha: '000000',
		});
		const filePath = 'gulp/res/img/flags.png';
		const fileSizeInBytes = fs.statSync(filePath).size;
		const fileStream = fs.createReadStream(filePath);
		threadParams.append('file', fileStream, { knownLength: fileSizeInBytes });
		const response = await fetch('http://localhost/forms/board/test/post', {
			headers: {
				'x-using-xhr': 'true',
				...threadParams.getHeaders(),
			},
			method: 'POST',
			body: threadParams
		});
		expect(response.ok).toBe(true);
		postId = (await response.json()).postId;
	});

	test('spoiler the file in a post',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			spoiler: '1',
			checkedposts: postId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('unlink file',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			unlink_file: '1',
			checkedposts: postId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('make post with already spoilered image',  async () => {
		const threadParams = new FormData({
			message: Math.random(),
			captcha: '000000',
			spoiler_all: '1',
		});
		const filePath = 'gulp/res/img/flags.png';
		const fileSizeInBytes = fs.statSync(filePath).size;
		const fileStream = fs.createReadStream(filePath);
		threadParams.append('file', fileStream, { knownLength: fileSizeInBytes });
		const response = await fetch('http://localhost/forms/board/test/post', {
			headers: {
				'x-using-xhr': 'true',
				...threadParams.getHeaders(),
			},
			method: 'POST',
			body: threadParams
		});
		expect(response.ok).toBe(true);
		postId = (await response.json()).postId;
	});

	test('delete file',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			delete_file: '1',
			checkedposts: postId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('ban reporter for local report',  async () => {
		const reportsPage = await fetch('http://localhost/test/manage/reports.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedreports" value="';
		const checkIndex = reportsPage.indexOf(checkString);
		const reportId = reportsPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
		const params = new URLSearchParams({
			_csrf: csrfToken,
			report_ban: '1',
			checkedposts: reportedPost.postId,
			checkedreports: reportId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('dismiss local report',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			dismiss: '1',
			checkedposts: reportedPost.postId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('ban reporter for global report',  async () => {
		const reportsPage = await fetch('http://localhost/globalmanage/reports.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedreports" value="';
		const checkIndex = reportsPage.indexOf(checkString);
		const reportId = reportsPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
		const params = new URLSearchParams({
			_csrf: csrfToken,
			global_report_ban: '1',
			globalcheckedposts: reportedPost._id,
			checkedreports: reportId,
		});
		const response = await fetch('http://localhost/forms/global/actions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('remove global report ban',  async () => {
		const banPage = await fetch('http://localhost/globalmanage/bans.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedbans" value="';
		const checkIndex = banPage.indexOf(checkString);
		banId = banPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedbans: banId,
			option: 'unban',
		});
		const response = await fetch('http://localhost/forms/global/editbans', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('test banning',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
		const params = new URLSearchParams({
			_csrf: csrfToken,
			ban: '1',
			checkedposts: randomThreadId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('edit board level ban duration',  async () => {
		const banPage = await fetch('http://localhost/test/manage/bans.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedbans" value="';
		const checkIndex = banPage.indexOf(checkString);
		banId = banPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedbans: banId,
			option: 'edit_duration',
			ban_duration: '3d',
		});
		const response = await fetch('http://localhost/forms/board/test/editbans', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('edit board level ban note',  async () => {
		const banPage = await fetch('http://localhost/test/manage/bans.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedbans" value="';
		const checkIndex = banPage.indexOf(checkString);
		banId = banPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedbans: banId,
			option: 'edit_note',
			ban_note: 'THIS IS A UNIQUE BAN NOTE',
		});
		const response = await fetch('http://localhost/forms/board/test/editbans', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('check updated board level ban note',  async () => {
		const banPage = await fetch('http://localhost/test/manage/bans.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		expect(banPage.includes('THIS IS A UNIQUE BAN NOTE')).toBe(true);
	});

	test('remove board level ban',  async () => {
		const banPage = await fetch('http://localhost/test/manage/bans.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedbans" value="';
		const checkIndex = banPage.indexOf(checkString);
		banId = banPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedbans: banId,
			option: 'unban',
		});
		const response = await fetch('http://localhost/forms/board/test/editbans', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('test global ban',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
		const params = new URLSearchParams({
			_csrf: csrfToken,
			global_ban: '1',
			checkedposts: randomThreadId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	let banId;
	test('deny ban appeal',  async () => {
		const banPage = await fetch('http://localhost/globalmanage/bans.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedbans" value="';
		const checkIndex = banPage.indexOf(checkString);
		banId = banPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedbans: banId,
			option: 'deny_appeal',
		});
		const response = await fetch('http://localhost/forms/global/editbans', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('test upgrade a ban to qrange',  async () => {

		const banPage = await fetch('http://localhost/globalmanage/bans.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedbans" value="';
		const checkIndex = banPage.indexOf(checkString);
		banId = banPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+24);

		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedbans: banId,
			option: 'upgrade',
			upgrade: 1,
		});
		const response = await fetch('http://localhost/forms/global/editbans', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('edit ban duration',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedbans: banId,
			option: 'edit_duration',
			ban_duration: '1s',
		});
		const response = await fetch('http://localhost/forms/global/editbans', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('remove ban',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedbans: banId,
			option: 'unban',
		});
		const response = await fetch('http://localhost/forms/global/editbans', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('test ban + qrange + non-appealable + show post in ban + hide name in modlog + modlog message + ban reason',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
		const params = new URLSearchParams({
			_csrf: csrfToken,
			ban: '1',
			ban_q: '1',
			ban_reason: 'test',
			log_message: 'test',
			preserve_post: '1',
			hide_name: '1',
			no_appeal: '1',
			checkedposts: randomThreadId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
	});

	test('check authed modlog page contains public:false modlogs',  async () => {
		const logsPageText = await fetch('http://localhost/test/manage/logs.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const containsPrivateLogs = logsPageText.toLowerCase().includes('updated settings'); // changing settings is a non-public modlog entry
		expect(containsPrivateLogs).toBe(true);
	});

	test('check public modlog page does not contain public:false modlogs',  async () => {
		const logsList = await fetch('http://localhost/test/logs.json', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.json());
		const firstLogDate = logsList[0].date;
		const logsPageText = await fetch(`http://localhost/test/logs/${firstLogDate.day.toString().padStart(2, 0)}-${firstLogDate.month.toString().padStart(2, 0)}-${firstLogDate.year.toString().padStart(2, 0)}.html`, {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const containsPrivateLogs = logsPageText.toLowerCase().includes('updated settings'); // changing settings is a non-public modlog entry
		expect(containsPrivateLogs).toBe(false);
	});

});


code:
const fetch = require('node-fetch');
const FormData = require('form-data');
const fs = require('fs-extra');

module.exports = () => describe('test some global form submissions', () => {

	let sessionCookie
		, csrfToken;

	test('login as admin',  async () => {
		const params = new URLSearchParams();
		params.append('username', 'admin');
		params.append('password', process.env.TEST_ADMIN_PASSWORD);
		const response = await fetch('http://localhost/forms/login', {
			headers: {
				'x-using-xhr': 'true',
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		const rawHeaders = response.headers.raw();
		expect(rawHeaders['set-cookie']).toBeDefined();
		expect(rawHeaders['set-cookie'][0]).toMatch(/^connect\.sid/);
		sessionCookie = rawHeaders['set-cookie'][0];
		csrfToken = await fetch('http://localhost/csrf.json', { headers: { 'cookie': sessionCookie }})
			.then(res => res.json())
			.then(json => json.token);
	});

	let bannerId, assetId;
	test('add banner',  async () => {
		const fileParams = new FormData();
		const filePath = 'gulp/res/img/flags.png';
		const fileSizeInBytes = fs.statSync(filePath).size;
		const fileStream = fs.createReadStream(filePath);
		fileParams.append('_csrf', csrfToken);
		fileParams.append('file', fileStream, { knownLength: fileSizeInBytes });
		const response = await fetch('http://localhost/forms/board/test/addbanners', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
				...fileParams.getHeaders(),
			},
			method: 'POST',
			body: fileParams
		});
		expect(response.ok).toBe(true);
		const bannerPage = await fetch('http://localhost/test/manage/assets.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedbanners" value="';
		const checkIndex = bannerPage.indexOf(checkString);
		const bannerSubstring = bannerPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+70);
		bannerId = bannerSubstring.substring(0, bannerSubstring.lastIndexOf('"'));
	});

	let flagId;
	test('add flag',  async () => {
		const fileParams = new FormData();
		const filePath = 'gulp/res/img/flags.png';
		const fileSizeInBytes = fs.statSync(filePath).size;
		const fileStream = fs.createReadStream(filePath);
		fileParams.append('_csrf', csrfToken);
		fileParams.append('file', fileStream, { knownLength: fileSizeInBytes });
		const response = await fetch('http://localhost/forms/board/test/addflags', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
				...fileParams.getHeaders(),
			},
			method: 'POST',
			body: fileParams
		});
		expect(response.ok).toBe(true);
		flagId = 'flags';
	});

	test('add asset',  async () => {
		const fileParams = new FormData();
		const filePath = 'gulp/res/img/flags.png';
		const fileSizeInBytes = fs.statSync(filePath).size;
		const fileStream = fs.createReadStream(filePath);
		fileParams.append('_csrf', csrfToken);
		fileParams.append('file', fileStream, { knownLength: fileSizeInBytes });
		const response = await fetch('http://localhost/forms/board/test/addassets', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
				...fileParams.getHeaders(),
			},
			method: 'POST',
			body: fileParams
		});
		expect(response.ok).toBe(true);
		const assetPage = await fetch('http://localhost/test/manage/assets.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkString = 'name="checkedassets" value="';
		const checkIndex = assetPage.indexOf(checkString);
		const assetSubstring = assetPage.substring(checkIndex+checkString.length, checkIndex+checkString.length+70);
		assetId = assetSubstring.substring(0, assetSubstring.lastIndexOf('"'));
	});

	test('delete banner',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedbanners: bannerId,
		});
		const response = await fetch('http://localhost/forms/board/test/deletebanners', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('delete flag',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedflags: flagId,
		});
		const response = await fetch('http://localhost/forms/board/test/deleteflags', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('delete asset',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedassets: assetId,
		});
		const response = await fetch('http://localhost/forms/board/test/deleteassets', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('add custompage',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			page: 'test',
			title: 'test',
			message: `==This is a test custompage==
wow
>very cool
testing 123`
		});
		const response = await fetch('http://localhost/forms/board/test/addcustompages', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
		const response2 = await fetch('http://localhost/test/page/test.html');
		expect(response2.ok).toBe(true);
	});

	test('edit and rename/move custompage',  async () => {
		const custompagesPage = await fetch('http://localhost/test/manage/custompages.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const custompageId = custompagesPage.match(/href="\/test\/manage\/editcustompage\/([0-9a-f]{24}).html"/)[1];
		const params = new URLSearchParams({
			_csrf: csrfToken,
			page_id: custompageId,
			page: 'test2',
			title: 'test2',
			message: 'test2',
		});
		const response = await fetch('http://localhost/forms/board/test/editcustompage', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
		const response2 = await fetch('http://localhost/test/page/test.html');
		expect(response2.status).toBe(404);
		const response3 = await fetch('http://localhost/test/page/test2.html');
		expect(response3.status).toBe(200);
	});

	test('delete custompage',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedcustompages: 'test2',
		});
		const response = await fetch('http://localhost/forms/board/test/deletecustompages', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
		const response2 = await fetch('http://localhost/test/page/test2.html');
		expect(response2.status).toBe(404);
	});

	let filterId;
	test('add filter to test board',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			filters: `notgood
bad words`,
			strict_filtering: 'true',
			filter_mode: '1',
			filter_message: 'Rule+1:+No+fun+allowed',
			filter_ban_duration: '1s',
			filter_ban_appealable: 'true',
		});
		const response = await fetch('http://localhost/forms/board/test/addfilter', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
		const filterPage = await fetch('http://localhost/test/manage/filters.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const checkIndex = filterPage.indexOf('name="checkedfilters" value="');
		filterId = filterPage.substring(checkIndex+29, checkIndex+29+24);
	});

	test('edit filter on test board',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			filter_id: filterId,
			filters: 'edited filters',
			strict_filtering: 'true',
			filter_mode: '1',
			filter_message: 'edited message',
			filter_ban_duration: '1s'
			// filter_ban_appealable omitted to change to false
		});
		const response = await fetch('http://localhost/forms/board/test/editfilter', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
		const filterPage = await fetch('http://localhost/test/manage/filters.html', {
			headers: {
				'cookie': sessionCookie,
			},
		}).then(res => res.text());
		const editTextIndex = filterPage.indexOf('edited filters');
		expect(editTextIndex).not.toBe(-1);
	});

	test('make a post that doesnt hit board filter',  async () => {
		const params = new URLSearchParams();
		params.append('message', 'blahblahblah');
		params.append('captcha', '000000');
		const response = await fetch('http://localhost/forms/board/test/post', {
			headers: {
				'x-using-xhr': 'true',
			},
			method: 'POST',
			body: params
		});
		expect(response.ok).toBe(true);
	});

	test('make a post that hits board filter',  async () => {
		const params = new URLSearchParams();
		params.append('message', 'edited filters');
		params.append('captcha', '000000');
		const response = await fetch('http://localhost/forms/board/test/post', {
			headers: {
				'x-using-xhr': 'true',
			},
			method: 'POST',
			body: params
		});
		expect(response.ok).not.toBe(true);
		await new Promise(res => setTimeout(res, 10000)); //let ban expire
	});

	test('delete test board filter',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedfilters: filterId,
		});
		const response = await fetch('http://localhost/forms/board/test/deletefilter', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('make a post that passes the deleted filter',  async () => {
		const params = new URLSearchParams();
		params.append('message', 'editing filter');
		params.append('captcha', '000000');
		const response = await fetch('http://localhost/forms/board/test/post', {
			headers: {
				'x-using-xhr': 'true',
			},
			method: 'POST',
			body: params
		});
		expect(response.ok).toBe(true);
	});

	test('add staff',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			username: 'test',
		});
		const response = await fetch('http://localhost/forms/board/test/addstaff', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('edit staff permission',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			username: 'test',
			MANAGE_BOARD_BANS: '1',
			MANAGE_BOARD_LOGS: '1',
			MANAGE_BOARD_SETTINGS: '1',
		});
		const response = await fetch('http://localhost/forms/board/test/editstaff', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

	test('remove staff',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedstaff: 'test',
		});
		const response = await fetch('http://localhost/forms/board/test/deletestaff', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect(response.ok).toBe(true);
	});

});


code:
const fetch = require('node-fetch');

module.exports = () => describe('delete tests and cleanup', () => {

	let sessionCookie
		, csrfToken;

	test('login as admin',  async () => {
		const params = new URLSearchParams();
		params.append('username', 'admin');
		params.append('password', process.env.TEST_ADMIN_PASSWORD);
		const response = await fetch('http://localhost/forms/login', {
			headers: {
				'x-using-xhr': 'true',
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		const rawHeaders = response.headers.raw();
		expect(rawHeaders['set-cookie']).toBeDefined();
		expect(rawHeaders['set-cookie'][0]).toMatch(/^connect\.sid/);
		sessionCookie = rawHeaders['set-cookie'][0];
		csrfToken = await fetch('http://localhost/csrf.json', { headers: { 'cookie': sessionCookie }})
			.then(res => res.json())
			.then(json => json.token);
	});

	test('delete_ip_thread test',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		//delete a reply and check if the OP is deleted (ip is the same for all posts atm)
		const randomThreadId = threads.find(t => t.replyposts > 0).postId;
		const thread = await fetch(`http://localhost/test/thread/${randomThreadId}.json`).then(res => res.json());
		const post = thread.replies[Math.floor(Math.random() * thread.replies.length)];
		const params = new URLSearchParams({
			_csrf: csrfToken,
			delete_ip_thread: '1',
			checkedposts: post.postId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
		const response2 = await fetch(`http://localhost/test/thread/${randomThreadId}.json`);
		expect(response2.status).toBe(404);
	});

	test('delete_ip_board test',  async () => {
		const threads = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		const randomThreadId = threads[Math.floor(Math.random() * threads.length)].postId;
		const params = new URLSearchParams({
			_csrf: csrfToken,
			delete_ip_board: '1',
			checkedposts: randomThreadId,
		});
		const response = await fetch('http://localhost/forms/board/test/modactions', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
		});
		expect(response.ok).toBe(true);
		await new Promise((resolve) => { setTimeout(resolve, 1000); }); //wait for async builds
		const response2 = await fetch('http://localhost/test/catalog.json').then(res => res.json());
		expect(response2.length).toBe(0);
	});

	test('delete test board',  async () => {
		const params = new URLSearchParams();
		params.append('_csrf', csrfToken);
		params.append('uri', 'test');
		params.append('confirm', 'true');
		const response = await fetch('http://localhost/forms/board/test/deleteboard', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		expect([200, 404]).toContain(response.status);
	});

	test('delete test account',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedaccounts: 'test',
		});
		const response = await fetch('http://localhost/forms/global/deleteaccounts', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		// console.log((await response.text()))
		expect(response.ok).toBe(true);
	});

	test('delete test account (with delete_owned_boards option)',  async () => {
		const params = new URLSearchParams({
			_csrf: csrfToken,
			checkedaccounts: 'test2',
			delete_owned_boards: 'test2',
		});
		const response = await fetch('http://localhost/forms/global/deleteaccounts', {
			headers: {
				'x-using-xhr': 'true',
				'cookie': sessionCookie,
			},
			method: 'POST',
			body: params,
			redirect: 'manual',
		});
		// console.log((await response.text()))
		expect(response.ok).toBe(true);
	});

});