├── .gitignore ├── LICENSE ├── README.md ├── backup.sh.example ├── configs ├── main.js.example ├── nginx.example ├── nginx_no_https.example └── webring.json.example ├── controllers ├── forms.js ├── forms │ ├── actions.js │ ├── addnews.js │ ├── appeal.js │ ├── boardsettings.js │ ├── changepassword.js │ ├── create.js │ ├── deletebanners.js │ ├── deleteboard.js │ ├── deletenews.js │ ├── editaccounts.js │ ├── editbans.js │ ├── editpost.js │ ├── globalactions.js │ ├── globalsettings.js │ ├── login.js │ ├── makepost.js │ ├── register.js │ ├── transfer.js │ └── uploadbanners.js └── pages.js ├── db ├── accounts.js ├── bans.js ├── boards.js ├── bypass.js ├── captchas.js ├── db.js ├── files.js ├── index.js ├── modlogs.js ├── news.js ├── posts.js ├── ratelimits.js ├── stats.js └── webring.js ├── ecosystem.config.js ├── gulp └── res │ ├── css │ ├── nscaptcha.css │ ├── style.css │ └── themes │ │ ├── amoled.css │ │ ├── chaos.css │ │ ├── choc.css │ │ ├── clear.css │ │ ├── darkblue.css │ │ ├── gurochan.css │ │ ├── lain.css │ │ ├── miku.css │ │ ├── navy.css │ │ ├── pink.css │ │ ├── rei-zero.css │ │ ├── robot.css │ │ ├── tomorrow.css │ │ ├── tomorrow2.css │ │ ├── win95.css │ │ ├── yotsuba b.css │ │ └── yotsuba.css │ ├── icons │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon.ico │ ├── favicon2.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest │ ├── img │ ├── attachment.png │ ├── audio.png │ ├── bumplock.png │ ├── cyclic.png │ ├── defaultbanner.png │ ├── deleted.png │ ├── dice.png │ ├── dice.svg │ ├── flags.png │ ├── lock.png │ ├── ratelimit.png │ ├── spoiler.png │ ├── sticky.png │ └── video.png │ └── js │ ├── captcha.js │ ├── catalog.js │ ├── counter.js │ ├── dragable.js │ ├── expand.js │ ├── forms.js │ ├── hide.js │ ├── hover.js │ ├── live.js │ ├── localstorage.js │ ├── password.js │ ├── quote.js │ ├── settings.js │ ├── theme.js │ ├── threadstat.js │ ├── time.js │ └── titlescroll.js ├── gulpfile.js ├── helpers ├── affectedboards.js ├── captcha │ ├── captchagenerate.js │ └── captchaverify.js ├── checks │ ├── actionchecker.js │ ├── alphanumregex.js │ ├── bancheck.js │ ├── blockbypass.js │ ├── calcpermsmiddleware.js │ ├── csrfmiddleware.js │ ├── dnsbl.js │ ├── hasperms.js │ ├── haspermsmiddleware.js │ ├── isloggedin.js │ └── spamcheck.js ├── commit.js ├── datearray.js ├── decodequeryip.js ├── dnsbl.js ├── dointerval.js ├── dynamic.js ├── escaperegexp.js ├── files │ ├── deletefailed.js │ ├── deleteold.js │ ├── deletepostfiles.js │ ├── deletetempfiles.js │ ├── ffprobe.js │ ├── fixgifs.js │ ├── formatsize.js │ ├── imageidentify.js │ ├── imagethumbnail.js │ ├── mimetypes.js │ ├── moveupload.js │ ├── uploadDirectory.js │ └── videothumbnail.js ├── haship.js ├── numfiles.js ├── pagequeryconverter.js ├── paramconverter.js ├── posting │ ├── diceroll.js │ ├── escape.js │ ├── linkmatch.js │ ├── markdown.js │ ├── message.js │ ├── name.js │ ├── quotes.js │ ├── sanitizeoptions.js │ └── tripcode.js ├── processip.js ├── referrercheck.js ├── render.js ├── sessionrefresh.js ├── setminimal.js ├── tasks.js ├── themes.js ├── timediffstring.js └── timeutils.js ├── migrations ├── index.js ├── migration-0.0.1.js ├── migration-0.0.2.js ├── migration-0.0.3.js ├── migration-0.0.4.js ├── migration-0.0.5.js ├── migration-0.0.6.js ├── migration-0.0.7.js ├── migration-0.0.8.js └── migration-0.0.9.js ├── models ├── forms │ ├── actionhandler.js │ ├── addnews.js │ ├── appeal.js │ ├── banposter.js │ ├── blockbypass.js │ ├── bumplockposts.js │ ├── changeboardsettings.js │ ├── changeglobalsettings.js │ ├── changepassword.js │ ├── create.js │ ├── cycleposts.js │ ├── deletebanners.js │ ├── deleteboard.js │ ├── deletenews.js │ ├── deletepost.js │ ├── deletepostsfiles.js │ ├── denybanappeals.js │ ├── dismissglobalreport.js │ ├── dismissreport.js │ ├── editaccounts.js │ ├── editpost.js │ ├── lockposts.js │ ├── login.js │ ├── logout.js │ ├── makepost.js │ ├── moveposts.js │ ├── newcaptcha.js │ ├── register.js │ ├── removebans.js │ ├── reportpost.js │ ├── spoilerpost.js │ ├── stickyposts.js │ ├── transferboard.js │ └── uploadbanners.js └── pages │ ├── account.js │ ├── banners.js │ ├── blockbypass.js │ ├── board.js │ ├── boardlist.js │ ├── captcha.js │ ├── captchapage.js │ ├── catalog.js │ ├── changepassword.js │ ├── create.js │ ├── globalmanage │ ├── accounts.js │ ├── bans.js │ ├── index.js │ ├── logs.js │ ├── news.js │ ├── recent.js │ ├── reports.js │ └── settings.js │ ├── home.js │ ├── index.js │ ├── login.js │ ├── manage │ ├── banners.js │ ├── bans.js │ ├── board.js │ ├── catalog.js │ ├── index.js │ ├── logs.js │ ├── recent.js │ ├── reports.js │ ├── settings.js │ └── thread.js │ ├── modlog.js │ ├── modloglist.js │ ├── news.js │ ├── randombanner.js │ ├── register.js │ └── thread.js ├── package-lock.json ├── package.json ├── queue.js ├── redis.js ├── redlock.js ├── remarkup.js ├── schedules ├── deletecaptchas.js ├── index.js ├── prune.js └── webring.js ├── server.js ├── socketio.js ├── style.css ├── views ├── custompages │ ├── faq.pug.example │ └── rules.pug.example ├── includes │ ├── 2charisocountries.pug │ ├── actionfooter.pug │ ├── actionfooter_globalmanage.pug │ ├── actionfooter_manage.pug │ ├── announcements.pug │ ├── banform.pug │ ├── bantable.pug │ ├── boardpages.pug │ ├── boardtable.pug │ ├── captcha.pug │ ├── favicon.pug │ ├── filelabel.pug │ ├── footer.pug │ ├── head.pug │ ├── managebanform.pug │ ├── modal.pug │ ├── navbar.pug │ ├── pages.pug │ ├── post.pug │ ├── postform.pug │ ├── posticons.pug │ ├── stickynav.pug │ ├── subjectfield.pug │ └── webringboardtable.pug ├── layout.pug ├── mixins │ ├── ban.pug │ ├── boardheader.pug │ ├── boardnav.pug │ ├── catalogtile.pug │ ├── globalmanagenav.pug │ ├── managenav.pug │ ├── modal.pug │ ├── newspost.pug │ ├── post.pug │ └── report.pug └── pages │ ├── 404.pug │ ├── 502.pug │ ├── account.pug │ ├── ban.pug │ ├── banners.pug │ ├── board.pug │ ├── boardlist.pug │ ├── bypass.pug │ ├── captcha.pug │ ├── catalog.pug │ ├── changepassword.pug │ ├── create.pug │ ├── editpost.pug │ ├── error.pug │ ├── globalmanageaccounts.pug │ ├── globalmanagebans.pug │ ├── globalmanagelogs.pug │ ├── globalmanagenews.pug │ ├── globalmanagerecent.pug │ ├── globalmanagereports.pug │ ├── globalmanagesettings.pug │ ├── home.pug │ ├── login.pug │ ├── managebanners.pug │ ├── managebans.pug │ ├── managelogs.pug │ ├── managerecent.pug │ ├── managereports.pug │ ├── managesettings.pug │ ├── message.pug │ ├── modlog.pug │ ├── modloglist.pug │ ├── news.pug │ ├── register.pug │ └── thread.pug └── worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | backup.sh 3 | configs/*.json 4 | configs/*.js 5 | static/* 6 | gulp/res/js/socket.io.js 7 | views/custompages/*.pug 8 | /gulp/res/css/codethemes 9 | gulp/res/js/timezone.js 10 | gulp/res/js/themelist.js 11 | gulp/res/js/post.js 12 | gulp/res/js/modal.js 13 | tmp/ 14 | -------------------------------------------------------------------------------- /backup.sh.example: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #change these 4 | APP_NAME="" 5 | MONGO_DATABASE="jschan" 6 | MONGO_HOST="" 7 | MONGO_PORT="" 8 | MONGO_USER="" 9 | MONGO_PASSWORD="" 10 | TIMESTAMP=`date +%F-%H%M` 11 | BACKUPS_DIR="/path/to/$APP_NAME" 12 | 13 | #probably dont change these 14 | DB_BACKUP_NAME="$APP_NAME-$TIMESTAMP.gz" 15 | FILE_BACKUP_NAME="$APP_NAME-$TIMESTAMP-files.zip" 16 | DB_ARCHIVE_PATH="$BACKUPS_DIR/$DB_BACKUP_NAME" 17 | FILE_ARCHIVE_PATH="$BACKUPS_DIR/$FILE_BACKUP_NAME" 18 | 19 | #make folder 20 | mkdir -p $BACKUPS_DIR 21 | 22 | #backups files 23 | zip -r -0 $FILE_ARCHIVE_PATH ./static 24 | 25 | #backup db 26 | mongodump --username $MONGO_USER --password $MONGO_PASSWORD --authenticationDatabase admin --db $MONGO_DATABASE --archive=$DB_ARCHIVE_PATH --gzip 27 | rm -rf dump 28 | 29 | #delete older than 7 days 30 | sudo find $ARCHIVE_PATH -type f -name "*.gz" -mtime +7 -exec rm -f {} \; 31 | 32 | exit 0 33 | -------------------------------------------------------------------------------- /configs/webring.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "following": [ 3 | "https://example.com/webring.json" 4 | ], 5 | "blacklist": [ 6 | "badwebsite.com" 7 | ], 8 | "logo": [ 9 | "https://yourdomain.com/favicon.ico" 10 | ], 11 | "proxy": { 12 | "enabled": false, 13 | "address": "socks5h://localhost:9050" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /controllers/forms/addnews.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const addNews = require(__dirname+'/../../models/forms/addnews.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const errors = []; 9 | 10 | if (!req.body.message || req.body.message.length === 0) { 11 | errors.push('Missing message'); 12 | } 13 | if (req.body.message.length > 10000) { 14 | errors.push('Message must be 10000 characters or less'); 15 | } 16 | if (!req.body.title || req.body.title.length === 0) { 17 | errors.push('Missing title'); 18 | } 19 | if (req.body.title.length > 50) { 20 | errors.push('Title must be 50 characters or less'); 21 | } 22 | 23 | if (errors.length > 0) { 24 | return dynamicResponse(req, res, 400, 'message', { 25 | 'title': 'Bad request', 26 | 'errors': errors, 27 | 'redirect': '/globalmanage/news.html' 28 | }); 29 | } 30 | 31 | try { 32 | await addNews(req, res, next); 33 | } catch (err) { 34 | return next(err); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /controllers/forms/appeal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const appealBans = require(__dirname+'/../../models/forms/appeal.js') 4 | , { globalLimits } = require(__dirname+'/../../configs/main.js') 5 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 6 | , { Bans } = require(__dirname+'/../../db'); 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | const errors = []; 11 | if (!req.body.checkedbans || req.body.checkedbans.length === 0 || req.body.checkedbans.length > 10) { 12 | errors.push('Must select 1-10 bans'); 13 | } 14 | if (!req.body.message || req.body.message.length === 0) { 15 | errors.push('Appeals must include a message'); 16 | } 17 | if (req.body.message.length > globalLimits.fieldLength.message) { 18 | errors.push('Appeal message must be 2000 characters or less'); 19 | } 20 | 21 | if (errors.length > 0) { 22 | return dynamicResponse(req, res, 400, 'message', { 23 | 'title': 'Bad request', 24 | 'errors': errors, 25 | 'redirect': '/' 26 | }); 27 | } 28 | 29 | let amount = 0; 30 | try { 31 | amount = await appealBans(req, res, next); 32 | } catch (err) { 33 | return next(err); 34 | } 35 | 36 | if (amount === 0) { 37 | /* 38 | this can occur if they selected invalid id, non-ip match, already appealed, or unappealable bans. prevented by databse filter, so we use 39 | use the updatedCount return value to check if any appeals were made successfully. if not, we end up here. 40 | */ 41 | return dynamicResponse(req, res, 400, 'message', { 42 | 'title': 'Bad request', 43 | 'error': 'Invalid bans selected', 44 | 'redirect': '/' 45 | }); 46 | } 47 | 48 | return dynamicResponse(req, res, 200, 'message', { 49 | 'title': 'Success', 50 | 'message': `Appealed ${amount} bans successfully`, 51 | 'redirect': '/' 52 | }); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /controllers/forms/changepassword.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const changePassword = require(__dirname+'/../../models/forms/changepassword.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const errors = []; 9 | 10 | //check exist 11 | if (!req.body.username || req.body.username.length <= 0) { 12 | errors.push('Missing username'); 13 | } 14 | if (!req.body.password || req.body.password.length <= 0) { 15 | errors.push('Missing password'); 16 | } 17 | if (!req.body.newpassword || req.body.newpassword.length <= 0) { 18 | errors.push('Missing new password'); 19 | } 20 | if (!req.body.newpasswordconfirm || req.body.newpasswordconfirm.length <= 0) { 21 | errors.push('Missing new password confirmation'); 22 | } 23 | 24 | //check too long 25 | if (req.body.username && req.body.username.length > 50) { 26 | errors.push('Username must be 50 characters or less'); 27 | } 28 | if (req.body.password && req.body.password.length > 100) { 29 | errors.push('Password must be 100 characters or less'); 30 | } 31 | if (req.body.newpassword && req.body.newpassword.length > 100) { 32 | errors.push('Password must be 100 characters or less'); 33 | } 34 | if (req.body.newpasswordconfirm && req.body.newpasswordconfirm.length > 100) { 35 | errors.push('Password confirmation must be 100 characters or less'); 36 | } 37 | if (req.body.newpassword != req.body.newpasswordconfirm) { 38 | errors.push('New password and password confirmation must match'); 39 | } 40 | 41 | if (errors.length > 0) { 42 | return dynamicResponse(req, res, 400, 'message', { 43 | 'title': 'Bad request', 44 | 'errors': errors, 45 | 'redirect': '/changepassword.html' 46 | }) 47 | } 48 | 49 | try { 50 | await changePassword(req, res, next); 51 | } catch (err) { 52 | return next(err); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /controllers/forms/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createBoard = require(__dirname+'/../../models/forms/create.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , { enableUserBoardCreation, globalLimits } = require(__dirname+'/../../configs/main.js') 6 | , alphaNumericRegex = require(__dirname+'/../../helpers/checks/alphanumregex.js') 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | if (enableUserBoardCreation === false && res.locals.permLevel > 1) { 11 | return dynamicResponse(req, res, 400, 'message', { 12 | 'title': 'Bad request', 13 | 'error': 'User board creation is currently disabled', 14 | 'redirect': '/create.html' 15 | }); 16 | } 17 | 18 | const errors = []; 19 | 20 | //check exist 21 | if (!req.body.uri || req.body.uri.length <= 0) { 22 | errors.push('Missing URI'); 23 | } 24 | if (!req.body.name || req.body.name.length <= 0) { 25 | errors.push('Missing name'); 26 | } 27 | if (!req.body.description || req.body.description.length <= 0) { 28 | errors.push('Missing description'); 29 | } 30 | 31 | //other validation 32 | if (req.body.uri) { 33 | if (req.body.uri.length > globalLimits.fieldLength.uri) { 34 | errors.push(`URI must be ${globalLimits.fieldLength.uri} characters or less`); 35 | } 36 | if (alphaNumericRegex.test(req.body.uri) !== true) { 37 | errors.push('URI must contain a-z 0-9 only'); 38 | } 39 | } 40 | if (req.body.name && req.body.name.length > globalLimits.fieldLength.boardname) { 41 | errors.push(`Name must be ${globalLimits.fieldLength.boardname} characters or less`); 42 | } 43 | if (req.body.description && req.body.description.length > globalLimits.fieldLength.description) { 44 | errors.push(`Description must be ${globalLimits.fieldLength.description} characters or less`); 45 | } 46 | 47 | if (errors.length > 0) { 48 | return dynamicResponse(req, res, 400, 'message', { 49 | 'title': 'Bad request', 50 | 'errors': errors, 51 | 'redirect': '/create.html' 52 | }); 53 | } 54 | 55 | try { 56 | await createBoard(req, res, next); 57 | } catch (err) { 58 | return next(err); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /controllers/forms/deletebanners.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const deleteBanners = require(__dirname+'/../../models/forms/deletebanners.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const errors = []; 9 | 10 | if (!req.body.checkedbanners || req.body.checkedbanners.length === 0 || req.body.checkedbanners.length > 10) { 11 | errors.push('Must select 1-10 banners to delete'); 12 | } 13 | 14 | if (errors.length > 0) { 15 | return dynamicResponse(req, res, 400, 'message', { 16 | 'title': 'Bad request', 17 | 'errors': errors, 18 | 'redirect': `/${req.params.board}/manage/banners.html` 19 | }) 20 | } 21 | 22 | for (let i = 0; i < req.body.checkedbanners.length; i++) { 23 | if (!res.locals.board.banners.includes(req.body.checkedbanners[i])) { 24 | return dynamicResponse(req, res, 400, 'message', { 25 | 'title': 'Bad request', 26 | 'message': 'Invalid banners selected', 27 | 'redirect': `/${req.params.board}/manage/banners.html` 28 | }) 29 | } 30 | } 31 | 32 | try { 33 | await deleteBanners(req, res, next); 34 | } catch (err) { 35 | console.error(err); 36 | return next(err); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /controllers/forms/deleteboard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Boards } = require(__dirname+'/../../db/') 4 | , deleteBoard = require(__dirname+'/../../models/forms/deleteboard.js') 5 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 6 | , alphaNumericRegex = require(__dirname+'/../../helpers/checks/alphanumregex.js') 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | const errors = []; 11 | 12 | if (!req.body.confirm) { 13 | errors.push('Missing confirmation'); 14 | } 15 | if (!req.body.uri) { 16 | errors.push('Missing URI'); 17 | } 18 | let board; 19 | if (alphaNumericRegex.test(req.body.uri) !== true) { 20 | errors.push('URI must contain a-z 0-9 only'); 21 | } else { 22 | //no need to check these if the board name is completely invalid 23 | if (req.params.board != null && req.params.board !== req.body.uri) { 24 | //board manage page to not be able to delete other boards; 25 | //req.params.board will be null on global delete, so this wont happen 26 | errors.push('URI does not match current board'); 27 | } 28 | try { 29 | board = await Boards.findOne(req.body.uri) 30 | } catch (err) { 31 | return next(err); 32 | } 33 | if (!board) { 34 | //global must check exists because the route skips Boards.exists middleware 35 | errors.push(`Board /${req.body.uri}/ does not exist`); 36 | } 37 | } 38 | 39 | if (errors.length > 0) { 40 | return dynamicResponse(req, res, 400, 'message', { 41 | 'title': 'Bad request', 42 | 'errors': errors, 43 | 'redirect': req.params.board ? `/${req.params.board}/manage/settings.html` : '/globalmanage/settings.html' 44 | }); 45 | } 46 | 47 | try { 48 | await deleteBoard(board._id, board); 49 | } catch (err) { 50 | return next(err); 51 | } 52 | 53 | return dynamicResponse(req, res, 200, 'message', { 54 | 'title': 'Success', 55 | 'message': 'Board deleted', 56 | 'redirect': req.params.board ? '/' : '/globalmanage/settings.html' 57 | }); 58 | 59 | } 60 | -------------------------------------------------------------------------------- /controllers/forms/deletenews.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const deleteNews = require(__dirname+'/../../models/forms/deletenews.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const errors = []; 9 | 10 | if (!req.body.checkednews || req.body.checkednews.length === 0 || req.body.checkednews.length > 10) { 11 | errors.push('Must select 1-10 newsposts delete'); 12 | } 13 | 14 | if (errors.length > 0) { 15 | return dynamicResponse(req, res, 400, 'message', { 16 | 'title': 'Bad request', 17 | 'errors': errors, 18 | 'redirect': '/globalmanage/news.html' 19 | }) 20 | } 21 | 22 | try { 23 | await deleteNews(req, res, next); 24 | } catch (err) { 25 | return next(err); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /controllers/forms/editaccounts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const editAccounts = require(__dirname+'/../../models/forms/editaccounts.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const errors = []; 9 | 10 | if (!req.body.checkedaccounts || req.body.checkedaccounts.length === 0 || req.body.checkedaccounts.length > 10) { 11 | errors.push('Must select 1-10 accounts'); 12 | } 13 | if (typeof req.body.auth_level !== 'number' && !req.body.delete_account) { 14 | errors.push('Missing auth level or delete action'); 15 | } 16 | if (typeof req.body.auth_level === 'number' && req.body.auth_level < 0 || req.body.auth_level > 4) { 17 | errors.push('Auth level must be 0-4'); 18 | } 19 | 20 | if (errors.length > 0) { 21 | return dynamicResponse(req, res, 400, 'message', { 22 | 'title': 'Bad request', 23 | 'errors': errors, 24 | 'redirect': '/globalmanage/accounts.html' 25 | }) 26 | } 27 | 28 | try { 29 | await editAccounts(req, res, next); 30 | } catch (err) { 31 | return next(err); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /controllers/forms/editbans.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const removeBans = require(__dirname+'/../../models/forms/removebans.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , denyAppeals = require(__dirname+'/../../models/forms/denybanappeals.js'); 6 | 7 | module.exports = async (req, res, next) => { 8 | 9 | const errors = []; 10 | 11 | if (!req.body.checkedbans || req.body.checkedbans.length === 0 || req.body.checkedbans.length > 10) { 12 | errors.push('Must select 1-10 bans') 13 | } 14 | if (!req.body.option || (req.body.option !== 'unban' && req.body.option !== 'deny_appeal')) { 15 | errors.push('Invalid ban action') 16 | } 17 | 18 | const redirect = req.params.board ? `/${req.params.board}/manage/bans.html` : '/globalmanage/bans.html'; 19 | 20 | if (errors.length > 0) { 21 | return dynamicResponse(req, res, 400, 'message', { 22 | 'title': 'Bad request', 23 | 'errors': errors, 24 | redirect 25 | }); 26 | } 27 | 28 | let amount = 0; 29 | let message; 30 | try { 31 | if (req.body.option === 'unban') { 32 | amount = await removeBans(req, res, next); 33 | message = `Removed ${amount} bans`; 34 | } else { 35 | amount = await denyAppeals(req, res, next); 36 | message = `Denied ${amount} appeals`; 37 | } 38 | } catch (err) { 39 | return next(err); 40 | } 41 | 42 | return dynamicResponse(req, res, 200, 'message', { 43 | 'title': 'Success', 44 | message, 45 | redirect 46 | }); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /controllers/forms/globalsettings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const changeGlobalSettings = require(__dirname+'/../../models/forms/changeglobalsettings.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const errors = []; 9 | 10 | if (req.body.filters && req.body.filters.length > 2000) { 11 | errors.push('Filters length must be 2000 characters or less'); 12 | } 13 | /* 14 | if (typeof req.body.captcha_mode === 'number' && (req.body.captcha_mode < 0 || req.body.captcha_mode > 2)) { 15 | errors.push('Invalid captcha mode.'); 16 | } 17 | */ 18 | if (typeof req.body.filter_mode === 'number' && (req.body.filter_mode < 0 || req.body.filter_mode > 2)) { 19 | errors.push('Invalid filter mode.'); 20 | } 21 | if (typeof req.body.ban_duration === 'number' && req.body.ban_duration <= 0) { 22 | errors.push('Invalid filter auto ban duration.'); 23 | } 24 | 25 | if (errors.length > 0) { 26 | return dynamicResponse(req, res, 400, 'message', { 27 | 'title': 'Bad request', 28 | 'errors': errors, 29 | 'redirect': '/globalmanage/settings.html' 30 | }); 31 | } 32 | 33 | try { 34 | await changeGlobalSettings(req, res, next); 35 | } catch (err) { 36 | return next(err); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /controllers/forms/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const loginAccount = require(__dirname+'/../../models/forms/login.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const errors = []; 9 | 10 | //check exist 11 | if (!req.body.username || req.body.username.length <= 0) { 12 | errors.push('Missing username'); 13 | } 14 | if (!req.body.password || req.body.password.length <= 0) { 15 | errors.push('Missing password'); 16 | } 17 | 18 | //check too long 19 | if (req.body.username && req.body.username.length > 50) { 20 | errors.push('Username must be 50 characters or less'); 21 | } 22 | if (req.body.password && req.body.password.length > 100) { 23 | errors.push('Password must be 100 characters or less'); 24 | } 25 | 26 | if (errors.length > 0) { 27 | return dynamicResponse(req, res, 400, 'message', { 28 | 'title': 'Bad request', 29 | 'errors': errors, 30 | 'redirect': '/login.html' 31 | }) 32 | } 33 | 34 | try { 35 | await loginAccount(req, res, next); 36 | } catch (err) { 37 | return next(err); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /controllers/forms/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const alphaNumericRegex = require(__dirname+'/../../helpers/checks/alphanumregex.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , { enableUserAccountCreation } = require(__dirname+'/../../configs/main.js') 6 | , registerAccount = require(__dirname+'/../../models/forms/register.js'); 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | if (enableUserAccountCreation === false && res.locals.permLevel > 1) { 11 | return dynamicResponse(req, res, 400, 'message', { 12 | 'title': 'Bad request', 13 | 'error': 'Acount creation is disabled', 14 | 'redirect': '/register.html' 15 | }); 16 | } 17 | 18 | const errors = []; 19 | 20 | //check exist 21 | if (!req.body.username || req.body.username.length <= 0) { 22 | errors.push('Missing username'); 23 | } 24 | if (!req.body.password || req.body.password.length <= 0) { 25 | errors.push('Missing password'); 26 | } 27 | if (!req.body.passwordconfirm || req.body.passwordconfirm.length <= 0) { 28 | errors.push('Missing password confirmation'); 29 | } 30 | 31 | //check 32 | if (req.body.username) { 33 | if (req.body.username.length > 50) { 34 | errors.push('Username must be 50 characters or less'); 35 | } 36 | if (alphaNumericRegex.test(req.body.username) !== true) { 37 | errors.push('Username must contain a-z 0-9 only'); 38 | } 39 | } 40 | if (req.body.password && req.body.password.length > 100) { 41 | errors.push('Password must be 100 characters or less'); 42 | } 43 | if (req.body.passwordconfirm && req.body.passwordconfirm.length > 100) { 44 | errors.push('Password confirmation must be 100 characters or less'); 45 | } 46 | if (req.body.password != req.body.passwordconfirm) { 47 | errors.push('Password and password confirmation must match'); 48 | } 49 | 50 | if (errors.length > 0) { 51 | return dynamicResponse(req, res, 400, 'message', { 52 | 'title': 'Bad request', 53 | 'errors': errors, 54 | 'redirect': '/register.html' 55 | }) 56 | } 57 | 58 | try { 59 | await registerAccount(req, res, next); 60 | } catch (err) { 61 | return next(err); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /controllers/forms/transfer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const transferBoard = require(__dirname+'/../../models/forms/transferboard.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , alphaNumericRegex = require(__dirname+'/../../helpers/checks/alphanumregex.js'); 6 | 7 | module.exports = async (req, res, next) => { 8 | 9 | const errors = []; 10 | 11 | if (!req.body.username || req.body.username.length === 0) { 12 | errors.push('Missing transfer username'); 13 | } 14 | if (req.body.username && req.body.username.length > 50) { 15 | errors.push('Transfer username must be 50 characters or less'); 16 | } 17 | if (req.body.username === res.locals.board.owner) { 18 | errors.push('New owner username must not be same as old owner'); 19 | } 20 | if (alphaNumericRegex.test(req.body.username) !== true) { 21 | errors.push('URI must contain a-z 0-9 only'); 22 | } 23 | 24 | if (errors.length > 0) { 25 | return dynamicResponse(req, res, 400, 'message', { 26 | 'title': 'Bad request', 27 | 'errors': errors, 28 | 'redirect': `/${req.params.board}/manage/settings.html` 29 | }) 30 | } 31 | 32 | try { 33 | await transferBoard(req, res, next); 34 | } catch (err) { 35 | return next(err); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /controllers/forms/uploadbanners.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const uploadBanners = require(__dirname+'/../../models/forms/uploadbanners.js') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js') 6 | , { globalLimits } = require(__dirname+'/../../configs/main.js'); 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | const errors = []; 11 | 12 | if (res.locals.numFiles === 0) { 13 | errors.push('Must provide a file'); 14 | } else if (res.locals.numFiles > globalLimits.bannerFiles.max) { 15 | errors.push(`Exceeded max banner uploads in one request of ${globalLimits.bannerFiles.max}`) 16 | } else if (res.locals.board.banners.length+res.locals.numFiles > 100) { 17 | errors.push('Number of uploads would exceed 100 banner limit'); 18 | } 19 | 20 | if (errors.length > 0) { 21 | await deleteTempFiles(req).catch(e => console.error); 22 | return dynamicResponse(req, res, 400, 'message', { 23 | 'title': 'Bad request', 24 | 'errors': errors, 25 | 'redirect': `/${req.params.board}/manage/banners.html` 26 | }) 27 | } 28 | 29 | try { 30 | await uploadBanners(req, res, next); 31 | } catch (err) { 32 | await deleteTempFiles(req).catch(e => console.error); 33 | return next(err); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /db/bypass.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/db.js') 4 | , { blockBypass } = require(__dirname+'/../configs/main.js') 5 | , db = Mongo.client.db('jschan').collection('bypass'); 6 | 7 | module.exports = { 8 | 9 | db, 10 | 11 | checkBypass: (id) => { 12 | return db.findOneAndUpdate({ 13 | '_id': id, 14 | 'uses': { 15 | '$lte': blockBypass.expireAfterUses 16 | } 17 | }, { 18 | '$inc': { 19 | 'uses': 1, 20 | } 21 | }).then(r => r.value); 22 | }, 23 | 24 | getBypass: () => { 25 | return db.insertOne({ 26 | 'uses': 0, 27 | 'expireAt': new Date(Date.now() + blockBypass.expireAfterTime) 28 | }); 29 | }, 30 | 31 | deleteAll: () => { 32 | return db.deleteMany({}); 33 | }, 34 | 35 | } 36 | -------------------------------------------------------------------------------- /db/captchas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/db.js') 4 | , db = Mongo.client.db('jschan').collection('captcha'); 5 | 6 | module.exports = { 7 | 8 | db, 9 | 10 | findOne: (id) => { 11 | return db.findOne({ '_id': id }); 12 | }, 13 | 14 | insertOne: (text) => { 15 | return db.insertOne({ 16 | 'text': text, 17 | 'expireAt': new Date() 18 | }); 19 | }, 20 | 21 | findOneAndDelete: (id, text) => { 22 | return db.findOneAndDelete({ 23 | '_id': id, 24 | 'text': text 25 | }); 26 | }, 27 | 28 | deleteAll: () => { 29 | return db.deleteMany({}); 30 | }, 31 | 32 | } 33 | -------------------------------------------------------------------------------- /db/db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { MongoClient, ObjectId, Int32 } = require('mongodb') 4 | , { migrateVersion } = require(__dirname+'/../package.json') 5 | , configs = require(__dirname+'/../configs/main.js'); 6 | 7 | module.exports = { 8 | 9 | connect: async () => { 10 | if (module.exports.client) { 11 | throw new Error('Mongo already connected'); 12 | } 13 | module.exports.client = await MongoClient.connect(configs.dbURL, { 14 | useNewUrlParser: true, 15 | useUnifiedTopology: true 16 | }); 17 | }, 18 | 19 | checkVersion: async() => { 20 | const currentVersion = await module.exports.client 21 | .db('jschan') 22 | .collection('version') 23 | .findOne({ '_id': 'version' }) 24 | .then(res => res.version); 25 | if (currentVersion < migrateVersion) { 26 | console.error('Your migration version is out-of-date. Run `gulp migrate` to update.'); 27 | process.exit(1); 28 | } 29 | }, 30 | 31 | ObjectId, 32 | 33 | NumberInt: Int32, 34 | 35 | } 36 | -------------------------------------------------------------------------------- /db/files.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/db.js') 4 | , Boards = require(__dirname+'/boards.js') 5 | , formatSize = require(__dirname+'/../helpers/files/formatsize.js') 6 | , db = Mongo.client.db('jschan').collection('files') 7 | 8 | module.exports = { 9 | 10 | db, 11 | 12 | increment: (file) => { 13 | return db.updateOne({ 14 | '_id': file.filename 15 | }, { 16 | '$inc': { 17 | 'count': 1 18 | }, 19 | '$addToSet': {//save list of thumb exts incase config is changed to track old exts 20 | 'exts': file.thumbextension, 21 | }, 22 | '$setOnInsert': { 23 | 'size': file.size 24 | } 25 | }, { 26 | 'upsert': true 27 | }); 28 | }, 29 | 30 | decrement: (fileNames) => { 31 | return db.updateMany({ 32 | '_id': { 33 | '$in': fileNames 34 | } 35 | }, { 36 | '$inc': { 37 | 'count': -1 38 | } 39 | }, { 40 | 'upsert': true //probably not necessary 41 | }); 42 | }, 43 | 44 | activeContent: () => { 45 | return db.aggregate([ 46 | { 47 | '$group': { 48 | '_id': null, 49 | 'count': { 50 | '$sum': 1 51 | }, 52 | 'size': { 53 | '$sum': '$size' 54 | } 55 | } 56 | } 57 | ]).toArray().then(res => { 58 | const stats = res[0]; 59 | if (stats) { 60 | return { 61 | count: stats.count, 62 | totalSize: stats.size, 63 | totalSizeString: formatSize(stats.size) 64 | } 65 | } else { 66 | return { 67 | count: 0, 68 | totalSize: 0, 69 | totalSizeString: '0B' 70 | } 71 | } 72 | }); 73 | }, 74 | 75 | deleteAll: () => { 76 | return db.deleteMany({}); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | Posts: require(__dirname+'/posts.js'), 6 | Boards: require(__dirname+'/boards.js'), 7 | Webring: require(__dirname+'/webring.js'), 8 | Stats: require(__dirname+'/stats.js'), 9 | Accounts: require(__dirname+'/accounts.js'), 10 | Bans: require(__dirname+'/bans.js'), 11 | Captchas: require(__dirname+'/captchas.js'), 12 | Files: require(__dirname+'/files.js'), 13 | News: require(__dirname+'/news.js'), 14 | Ratelimits: require(__dirname+'/ratelimits.js'), 15 | Modlogs: require(__dirname+'/modlogs.js'), 16 | Bypass: require(__dirname+'/bypass.js'), 17 | 18 | } 19 | -------------------------------------------------------------------------------- /db/news.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const Mongo = require(__dirname+'/db.js') 5 | , db = Mongo.client.db('jschan').collection('news'); 6 | 7 | module.exports = { 8 | 9 | db, 10 | 11 | find: (limit=0) => { 12 | return db.find({}).sort({ 13 | '_id': -1 14 | }) 15 | .limit(limit) 16 | .toArray(); 17 | }, 18 | 19 | insertOne: (news) => { 20 | return db.insertOne(news); 21 | }, 22 | 23 | deleteMany: (ids) => { 24 | return db.deleteMany({ 25 | '_id': { 26 | '$in': ids 27 | } 28 | }) 29 | }, 30 | 31 | deleteAll: () => { 32 | return db.deleteMany({}); 33 | }, 34 | 35 | } 36 | -------------------------------------------------------------------------------- /db/ratelimits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/db.js') 4 | , db = Mongo.client.db('jschan').collection('ratelimit'); 5 | 6 | module.exports = { 7 | 8 | db, 9 | 10 | resetQuota: (id, suffix) => { 11 | return db.deleteOne({ '_id': `${id}-${suffix}` }); 12 | }, 13 | 14 | incrmentQuota: (ip, suffix, amount) => { 15 | return db.findOneAndUpdate( 16 | { 17 | '_id': `${ip}-${suffix}` 18 | }, 19 | { 20 | '$inc': { 21 | 'sequence_value': amount 22 | }, 23 | '$setOnInsert': { 24 | 'expireAt': new Date() 25 | } 26 | }, 27 | { 28 | 'upsert': true 29 | } 30 | ).then(r => { return r.value ? r.value.sequence_value : 0 }); 31 | }, 32 | 33 | deleteAll: () => { 34 | return db.deleteMany({}); 35 | }, 36 | 37 | } 38 | -------------------------------------------------------------------------------- /db/webring.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/db.js') 4 | , db = Mongo.client.db('jschan').collection('webring'); 5 | 6 | module.exports = { 7 | 8 | db, 9 | 10 | boardSort: (skip=0, limit=50, sort={ ips:-1, pph:-1, sequence_value:-1 }, filter={}) => { 11 | const addedFilter = {}; 12 | if (filter.search) { 13 | addedFilter['$or'] = [ 14 | { uri: filter.search }, 15 | { tags: filter.search } 16 | ] 17 | } 18 | const addedSort = {}; 19 | if (sort.ips) { 20 | addedSort['uniqueUsers'] = sort.ips 21 | } 22 | if (sort.pph) { 23 | addedSort['postsPerHour'] = sort.pph 24 | } 25 | if (sort.sequence_value) { 26 | addedSort['totalPosts'] = sort.sequence_value 27 | } 28 | if (sort.lastPostTimestamp) { 29 | addedSort['lastPostTimestamp'] = sort.lastPostTimestamp 30 | } 31 | return db.find(addedFilter) 32 | .sort(addedSort) 33 | .skip(skip) 34 | .limit(limit) 35 | .toArray(); 36 | }, 37 | 38 | count: (filter) => { 39 | const addedFilter = {}; 40 | if (filter.search) { 41 | addedFilter['$or'] = [ 42 | { uri: filter.search }, 43 | { tags: filter.search } 44 | ] 45 | } 46 | return db.countDocuments(addedFilter); 47 | }, 48 | 49 | deleteAll: () => { 50 | return db.deleteMany({}); 51 | }, 52 | 53 | } 54 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | const numCpus = require('os').cpus().length; 2 | module.exports = { 3 | // Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/ 4 | apps : [{ 5 | name: 'build-worker', 6 | script: 'worker.js', 7 | instances: Math.floor(numCpus/2), //if you only have 1 core and floor to 0, 0 just means "all cores" which is correct in that case. 8 | autorestart: true, 9 | watch: false, 10 | max_memory_restart: '1G', 11 | log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS', 12 | env: { 13 | NODE_ENV: 'development' 14 | }, 15 | env_development: { 16 | NODE_ENV: 'development' 17 | }, 18 | env_production: { 19 | NODE_ENV: 'production' 20 | } 21 | }, { 22 | name: 'chan', 23 | script: 'server.js', 24 | instances: Math.floor(numCpus/2), 25 | autorestart: true, 26 | watch: false, 27 | max_memory_restart: '1G', 28 | log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS', 29 | wait_ready: true, 30 | kill_timeout: 5000, 31 | env: { 32 | NODE_ENV: 'development' 33 | }, 34 | env_production: { 35 | NODE_ENV: 'production' 36 | } 37 | }, { 38 | name: 'schedules', 39 | script: 'schedules/index.js', 40 | instances: 1, 41 | autorestart: true, 42 | watch: false, 43 | max_memory_restart: '1G', 44 | log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS', 45 | env: { 46 | NODE_ENV: 'development' 47 | }, 48 | env_development: { 49 | NODE_ENV: 'development' 50 | }, 51 | env_production: { 52 | NODE_ENV: 'production' 53 | } 54 | }] 55 | }; 56 | -------------------------------------------------------------------------------- /gulp/res/css/nscaptcha.css: -------------------------------------------------------------------------------- 1 | img { 2 | width: 200px; 3 | height: 80px; 4 | margin: 0 auto; 5 | } 6 | input { 7 | position: fixed; 8 | left: -3px; 9 | bottom: 0; 10 | opacity: 0.9; 11 | border: none; 12 | background: none; 13 | font-size: 18px; 14 | cursor: pointer; 15 | } 16 | body { 17 | font-family: arial, helvetica, sans-serif; 18 | font-size: 10pt; 19 | margin: 0; 20 | padding: 0; 21 | } 22 | -------------------------------------------------------------------------------- /gulp/res/css/themes/amoled.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 3 | --alt-label-color:#040404; 4 | --alt-font-color:#c5c8c6; 5 | --background-top:#000; 6 | --background-rest:#000; 7 | --navbar-color:#040404; 8 | --post-color:#000; 9 | --post-outline-color:#111; 10 | --label-color:#040404; 11 | --box-border-color:#111; 12 | --darken:#040404; 13 | --highlighted-post-color:#000; 14 | --highlighted-post-outline-color:#444; 15 | --board-title:#c5c8c6; 16 | --hr:#040404; 17 | --font-color:#c5c8c6; 18 | --name-color:#c5c8c6; 19 | --capcode-color:#f00; 20 | --subject-color:#b294bb; 21 | --link-color:#c5c8c6; 22 | --post-link-color:#5f89ac; 23 | --link-hover:#81a2be; 24 | --input-borders:#111; 25 | --input-color:#c5c8c6; 26 | --input-background:#040404; 27 | --dice-color:darkorange; 28 | --title-color:#d70000; 29 | --greentext-color:green; 30 | --pinktext-color:#E0727F; 31 | } 32 | .spoiler { 33 | background:gray; 34 | color:gray; 35 | } 36 | .spoiler:not(:hover) * { 37 | color: gray; 38 | background: gray!important; 39 | } 40 | .anchor:target + .post-container, 41 | .post-container.highlighted, 42 | .anchor:target + table tbody tr th, 43 | .anchor:target + table { 44 | /* border-style: dashed var(--highlighted-post-outline-color);*/ 45 | } 46 | .post-info, .post-container:target .post-info, .post-container.highlighted .post-info { 47 | background:none!important;border:none!important 48 | } 49 | -------------------------------------------------------------------------------- /gulp/res/css/themes/chaos.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 3 | --alt-label-color:#001010; 4 | --alt-font-color:#00CCCC; 5 | --background-top:#000505; 6 | --background-rest:#000505; 7 | --navbar-color:#001010; 8 | --post-color:#001010; 9 | --post-outline-color:#B7C5D9; 10 | --label-color:#001010; 11 | --box-border-color:#00AAAA; 12 | --darken:#00000010; 13 | --highlighted-post-color:#003030; 14 | --highlighted-post-outline-color:#FF0000; 15 | --board-title:#af0a0f; 16 | --hr:lightgray; 17 | --font-color:#00CCCC; 18 | --name-color:#FF0000; 19 | --capcode-color:#f00; 20 | --subject-color:#FF0000; 21 | --link-color:#FF0000; 22 | --post-link-color:#FF0000; 23 | --link-hover:#d00; 24 | --input-borders:#00AAAA; 25 | --input-color:#00CCCC; 26 | --input-background:#000505; 27 | --dice-color:darkorange; 28 | --title-color:#d70000; 29 | --greentext-color:#FF0000; 30 | --pinktext-color:#E0727F; 31 | } 32 | #livetext, 33 | #threadstats, 34 | #float .post-container, 35 | .catalog-tile, 36 | .live, 37 | .modal, 38 | .pages, 39 | .post-container, 40 | .stickynav, 41 | .toggle-summary, 42 | input[type=file], 43 | input[type=number], 44 | input[type=password], 45 | input[type=submit], 46 | input[type=text], 47 | select, 48 | textarea, 49 | .label, 50 | .postform-style, 51 | .close, 52 | table, 53 | .board-banner, 54 | #postform { 55 | border: 1px dotted; 56 | } 57 | hr { 58 | border-top: 1px dotted; 59 | } 60 | .navbar { 61 | border-bottom: 1px dotted; 62 | } 63 | a .post-name:hover, 64 | a:hover { 65 | background:var(--link-hover)!important; 66 | color:black!important; 67 | } 68 | .anchor:target + .post-container .post-info, .post-container.highlighted .post-info { 69 | border-bottom: none; 70 | } 71 | @media only screen and (max-width: 600px) { 72 | 73 | .post-info{ 74 | background: none; 75 | border-bottom: none; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /gulp/res/css/themes/choc.css: -------------------------------------------------------------------------------- 1 | /*tomorrow*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #42200f; 5 | --alt-font-color: #ff7e83; 6 | --background-top: #2f1903; 7 | --background-rest: #261602; 8 | --navbar-color: #302016; 9 | --post-color: #302016; 10 | --post-outline-color: #523830; 11 | --label-color: #42200f; 12 | --box-border-color: #4c332c; 13 | --darken: #ffffff10; 14 | --highlighted-post-color: #572a1b; 15 | --highlighted-post-outline-color: #784134; 16 | --board-title: #ff7e83; 17 | --hr: #4c332c; 18 | --font-color: #ff7e83; 19 | --name-color: #ff7e83; 20 | --capcode-color: #ff5252; 21 | --subject-color: #ff5252; 22 | --link-color: #a2a2ff; 23 | --post-link-color: #a2a2ff; 24 | --link-hover: #ff4d4b; 25 | --input-borders: #535353; 26 | --input-color: #fff; 27 | --input-background: #000; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: green; 31 | --pinktext-color:#E0727F; 32 | } 33 | -------------------------------------------------------------------------------- /gulp/res/css/themes/clear.css: -------------------------------------------------------------------------------- 1 | /*clear*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #f5f5f5; 5 | --alt-font-color: #333; 6 | --background-top: #EEE; 7 | --background-rest: #EEE; 8 | --navbar-color: #DDD; 9 | --post-color: #DDD; 10 | --post-outline-color: #d2d2d2; 11 | --label-color: #f5f5f5; 12 | --box-border-color: #e0e0e0; 13 | --darken: #0000000f; 14 | --highlighted-post-color: #EEDACB; 15 | --highlighted-post-outline-color: #d2d2d2; 16 | --board-title: maroon; 17 | --hr: #DDD; 18 | --font-color: #333; 19 | --name-color: #333; 20 | --capcode-color: #f00; 21 | --subject-color: #004A99; 22 | --link-color: black; 23 | --post-link-color: darkblue; 24 | --link-hover: #a74300; 25 | --input-borders: #d2d2d2; 26 | --input-color: #333; 27 | --input-background: #f5f5f5; 28 | --dice-color: maroon; 29 | --title-color: #d70000; 30 | --greentext-color: #789922; 31 | --pinktext-color:#E0727F; 32 | } 33 | .post-info, .post-container:target .post-info, .post-container.highlighted .post-info { 34 | background:none!important;border:none!important 35 | } 36 | -------------------------------------------------------------------------------- /gulp/res/css/themes/darkblue.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-top: rgb(40, 40, 46); 3 | --background-rest: rgb(40, 40, 46); 4 | --label-color: #1f1f1f; 5 | --box-border-color: #0099ff; 6 | --font-color: #eee; 7 | --alt-font-color: #fff; 8 | --alt-label-color: #800; 9 | --navbar-color: #3f3f3f; 10 | --darken: #00000010; 11 | --link-color: #0099ff; 12 | --post-link-color: #0099ff; 13 | --link-hover: #3333ff; 14 | --input-borders: #a9a9a9; 15 | --input-color: #fff; 16 | --input-background: black; 17 | --post-color: #1f1f1f; 18 | --post-outline-color: #0099ff; 19 | --highlighted-post-color: #2c2c3c; 20 | --highlighted-post-outline-color: #0555ff; 21 | --dice-color: darkorange; 22 | --title-color: #ff0000; 23 | --greentext-color: #55ff33; 24 | --pinktext-color:#E0727F; 25 | --board-title: #33ffff; 26 | --name-color: #0044ff; 27 | --capcode-color: #f00; 28 | --subject-color: #33ddff; 29 | --hr: #0099ff; 30 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 31 | } 32 | body { 33 | font-family: 'Hack', monospace, sans-serif; 34 | font-size: 9pt; 35 | color: #eee; 36 | } 37 | .bold { 38 | font-weight: bold; 39 | color: #fff; 40 | } 41 | .em { 42 | color: #FF6700; 43 | } 44 | .strikethrough { 45 | color: #e6e600; 46 | } 47 | a, a:visited, a .post-name { 48 | text-decoration: none; 49 | font-weight: bolder; 50 | font-size: 12px; 51 | color: var(--link-color); 52 | } 53 | /* prevent above rule making link in board titles small */ 54 | .board-title a { 55 | font-size: inherit; 56 | } 57 | .catalog-tile { 58 | padding: 10px; 59 | margin: 4px; 60 | height: 240px; 61 | border: 1px solid var(--post-outline-color); 62 | max-width: 250px; 63 | } 64 | .catalog-thumb { 65 | width: 80px; 66 | height: 80px; 67 | } 68 | .spoiler { 69 | cursor: help; 70 | } 71 | -------------------------------------------------------------------------------- /gulp/res/css/themes/gurochan.css: -------------------------------------------------------------------------------- 1 | /*gurochan*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #e6cbc0; 5 | --alt-font-color: #000; 6 | --background-top: #EDDAD2; 7 | --background-rest: #EDDAD2; 8 | --navbar-color: #D9AF9E; 9 | --post-color: #D9AF9E; 10 | --post-outline-color: #CA927B; 11 | --label-color: #E6CBC0; 12 | --box-border-color: #CA927B; 13 | --darken: #0000000f; 14 | --highlighted-post-color: #EEDACB; 15 | --highlighted-post-outline-color: #CA927B; 16 | --board-title: #FF6600; 17 | --hr: #CA927B; 18 | --font-color: #000; 19 | --name-color: #000; 20 | --capcode-color: #f00; 21 | --subject-color: #004A99; 22 | --link-color: #34345C; 23 | --post-link-color: #34345C; 24 | --link-hover: #34345C; 25 | --input-borders: #CA927B; 26 | --input-color: #000; 27 | --input-background: #E6CBC0; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: #789922; 31 | --pinktext-color:#E0727F; 32 | } 33 | -------------------------------------------------------------------------------- /gulp/res/css/themes/lain.css: -------------------------------------------------------------------------------- 1 | /*lainchan*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #333; 5 | --alt-font-color: #bbb; 6 | --background-top: #1E1E1E; 7 | --background-rest: #1E1E1E; 8 | --navbar-color: #333; 9 | --post-color: #333; 10 | --post-outline-color: #555; 11 | --label-color: #333; 12 | --box-border-color: #666; 13 | --darken: #ffffff0f; 14 | --highlighted-post-color: #32DD7220; 15 | --highlighted-post-outline-color: #32DD7250; 16 | --board-title: #32DD72; 17 | --hr: #333; 18 | --font-color: #bbb; 19 | --name-color: #32DD72; 20 | --capcode-color: #f00; 21 | --subject-color: #b294bb; 22 | --link-color: #fff; 23 | --post-link-color: #fff; 24 | --link-hover: #32DD72; 25 | --input-borders: #666; 26 | --input-color: #CCCCCC; 27 | --input-background: #333; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: #B8D962; 31 | --pinktext-color:#E0727F; 32 | } 33 | .post-container, #float .post-container, .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats { 34 | border-width: 1px; 35 | } 36 | .captcha { 37 | filter: invert(90%); 38 | } 39 | -------------------------------------------------------------------------------- /gulp/res/css/themes/miku.css: -------------------------------------------------------------------------------- 1 | /*miku*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #B6DDDE; 5 | --alt-font-color: black; 6 | --background-top: #2e99cc70; 7 | --background-rest: #D2FFEE; 8 | --navbar-color: #B6DDDE; 9 | --post-color: #B6DDDE; 10 | --post-outline-color: #8FCCCD; 11 | --label-color: #B6DDDE; 12 | --box-border-color: #8FCCCD; 13 | --darken: #00000010; 14 | --highlighted-post-color: #a9d8ff; 15 | --highlighted-post-outline-color: #8FCCCD; 16 | --board-title: #800000; 17 | --hr: #B7D9C5; 18 | --font-color: black; 19 | --name-color: #117743; 20 | --capcode-color: #f00; 21 | --subject-color: #117743; 22 | --link-color: #00637B; 23 | --post-link-color: #d00; 24 | --link-hover: #d00; 25 | --input-borders: #a9a9a9; 26 | --input-color: #000; 27 | --input-background: white; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: #789922; 31 | --pinktext-color:#E0727F; 32 | } 33 | .post-container, #float .post-container, .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats { 34 | border-width: 0 1px 1px 0; 35 | } 36 | -------------------------------------------------------------------------------- /gulp/res/css/themes/navy.css: -------------------------------------------------------------------------------- 1 | /*tomorrow*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #1a1e34; 5 | --alt-font-color: #bfbfbf; 6 | --background-top: #080c19; 7 | --background-rest: #080c19; 8 | --navbar-color: #1a1e34; 9 | --post-color: #1a1e34; 10 | --post-outline-color: #2c3a4e; 11 | --label-color: #1a1e34; 12 | --box-border-color: #2c3a4e; 13 | --darken: #ffffff08; 14 | --highlighted-post-color: #44283e; 15 | --highlighted-post-outline-color: #664e5c; 16 | --board-title: #ff858a; 17 | --hr: #303030; 18 | --font-color: #bfbfbf; 19 | --name-color: #67b38d; 20 | --capcode-color: #ff4141; 21 | --subject-color: #8f8dc9; 22 | --link-color: #8585a3; 23 | --post-link-color: #f65252; 24 | --link-hover: #f65252; 25 | --input-borders: #434343; 26 | --input-color: #bfbfbf; 27 | --input-background: #080c19; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: green; 31 | --pinktext-color:#E0727F; 32 | } 33 | -------------------------------------------------------------------------------- /gulp/res/css/themes/pink.css: -------------------------------------------------------------------------------- 1 | /*pink*/ 2 | :root { 3 | --icon-color: invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%); 4 | --alt-label-color: #ffcccb; 5 | --alt-font-color:#80002c; 6 | --background-top:#ffe4e6; 7 | --background-rest:#ffe4e6; 8 | --navbar-color:#ffcccb; 9 | --post-color:#ffcccb; 10 | --post-outline-color:#80002c; 11 | --label-color:#ffcccb; 12 | --box-border-color:#80002c; 13 | --darken:#80002c54; 14 | --highlighted-post-color:#eab5b4; 15 | --highlighted-post-outline-color:#80002c; 16 | --board-title:#80002c; 17 | --hr:#80002c; 18 | --font-color:#80002c; 19 | --name-color:#b3003e; 20 | --capcode-color:#f00; 21 | --subject-color:#e60050; 22 | --link-color:#80002c; 23 | --post-link-color:#80002c; 24 | --link-hover:#c48899; 25 | --input-borders:#80002c; 26 | --input-color:#80002c; 27 | --input-background:#ffe6ea; 28 | --dice-color:darkorange; 29 | --title-color:#d70000; 30 | --greentext-color:#789922; 31 | --pinktext-color:#FF00EF; 32 | } 33 | .post-container:not(.op), .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats,#float, table { 34 | box-shadow: 3px 3px 3px var(--darken); 35 | } 36 | hr { 37 | height: 0px; 38 | border-width: 2px medium medium; 39 | border-style: dashed none none; 40 | border-color: var(--post-outline-color); 41 | } 42 | .anchor:target + .post-container, 43 | .post-container.highlighted, 44 | .anchor:target + table tbody tr th, 45 | .anchor:target + table { 46 | border: 1px dashed; 47 | } 48 | 49 | @media only screen and (max-width: 600px) { 50 | 51 | .post-info{ 52 | background: none; 53 | border-bottom: none; 54 | } 55 | 56 | .anchor:target + .post-container .post-info, .post-container.highlighted .post-info { 57 | border-bottom: none; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /gulp/res/css/themes/rei-zero.css: -------------------------------------------------------------------------------- 1 | /*rei-zero*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #29373e; 5 | --alt-font-color: #d6d6d6; 6 | --background-top: #000E1C; 7 | --background-rest: #000E1C; 8 | --navbar-color: #29373E; 9 | --post-color: #29373E; 10 | --post-outline-color: #666666; 11 | --label-color: #29373E; 12 | --box-border-color: #000; 13 | --darken: #00000010; 14 | --highlighted-post-color: #193A3A; 15 | --highlighted-post-outline-color: #999999; 16 | --board-title: #193A3A; 17 | --hr: #474747; 18 | --font-color: #D6D6D6; 19 | --name-color: #117743; 20 | --capcode-color: #f00; 21 | --subject-color: #CCCCCC; 22 | --link-color: #5C99AD; 23 | --post-link-color: #00BfFF; 24 | --link-hover: #eeeeee; 25 | --input-borders: #a9a9a9; 26 | --input-color: lightgray; 27 | --input-background: #001229; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: #789922; 31 | --pinktext-color:#E0727F; 32 | } 33 | -------------------------------------------------------------------------------- /gulp/res/css/themes/robot.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 3 | --alt-label-color:#2d2d86; 4 | --alt-font-color:#f2f2f2; 5 | --background-top:#19194d; 6 | --background-rest:#19194d; 7 | --navbar-color:#2d2d86; 8 | --post-color:#262673; 9 | --post-outline-color:#262673; 10 | --label-color:#2d2d86; 11 | --box-border-color:#000000; 12 | --darken:#ffffff10; 13 | --highlighted-post-color:#3636a6; 14 | --highlighted-post-outline-color:#111; 15 | --board-title:#c5c8c6; 16 | --hr:#282a2e; 17 | --font-color:#f2f2f2; 18 | --name-color:#117743; 19 | --capcode-color:red; 20 | --subject-color:#6666CC; 21 | --link-color:white; 22 | --post-link-color:red; 23 | --link-hover:#f2f2f2; 24 | --input-borders:black; 25 | --input-color:black; 26 | --input-background:white; 27 | --dice-color:darkorange; 28 | --title-color:#d70000; 29 | --greentext-color:#789922; 30 | --pinktext-color:#E0727F; 31 | } 32 | .post-message a:hover { 33 | color:var(--post-link-color)!important; 34 | } 35 | .postform-style { 36 | color:black 37 | } 38 | .post-info, .post-container:target .post-info, .post-container.highlighted .post-info { 39 | background:none!important;border:none!important 40 | } 41 | 42 | -------------------------------------------------------------------------------- /gulp/res/css/themes/tomorrow.css: -------------------------------------------------------------------------------- 1 | /*tomorrow*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #282a2e; 5 | --alt-font-color: #c5c8c6; 6 | --background-top: #1d1f21; 7 | --background-rest: #1d1f21; 8 | --navbar-color: #282a2e; 9 | --post-color: #282a2e; 10 | --post-outline-color: #282a2e; 11 | --label-color: #282a2e; 12 | --box-border-color: #111; 13 | --darken: #ffffff10; 14 | --highlighted-post-color: #2b1d1f; 15 | --highlighted-post-outline-color: #111; 16 | --board-title: #c5c8c6; 17 | --hr: #282a2e; 18 | --font-color: #c5c8c6; 19 | --name-color: #c5c8c6; 20 | --capcode-color: #f00; 21 | --subject-color: #b294bb; 22 | --link-color: #c5c8c6; 23 | --post-link-color: #5f89ac; 24 | --link-hover: #81a2be; 25 | --input-borders: #111; 26 | --input-color: #c5c8c6; 27 | --input-background: #1d1d21; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: green; 31 | --pinktext-color:#E0727F; 32 | } 33 | 34 | table { 35 | border: none; 36 | border-spacing: 2px; 37 | } 38 | 39 | .captcha { 40 | filter: invert(90%); 41 | } 42 | 43 | @media only screen and (max-width: 600px) { 44 | 45 | .post-info{ 46 | border-bottom: none; 47 | } 48 | 49 | .anchor:target + .post-container .post-info, .post-container.highlighted .post-info { 50 | border-bottom: none; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /gulp/res/css/themes/tomorrow2.css: -------------------------------------------------------------------------------- 1 | /*tomorrow*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #282a2e; 5 | --alt-font-color: #c5c8c6; 6 | --background-top: #1d1f21; 7 | --background-rest: #1d1f21; 8 | --navbar-color: #282a2e; 9 | --post-color: #282a2e; 10 | --post-outline-color: #282a2e; 11 | --label-color: #282a2e; 12 | --box-border-color: #111; 13 | --darken: #ffffff10; 14 | --highlighted-post-color: #2b1d1f; 15 | --highlighted-post-outline-color: #111; 16 | --board-title: #c5c8c6; 17 | --hr: #282a2e; 18 | --font-color: #c5c8c6; 19 | --name-color: #c5c8c6; 20 | --capcode-color: #f00; 21 | --subject-color: #b294bb; 22 | --link-color: #c5c8c6; 23 | --post-link-color: #5f89ac; 24 | --link-hover: #81a2be; 25 | --input-borders: #111; 26 | --input-color: #c5c8c6; 27 | --input-background: #1d1d21; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: green; 31 | --pinktext-color:#E0727F; 32 | } 33 | 34 | .anchor:target + .post-container, 35 | .post-container.highlighted { 36 | border: 1px solid var(--highlighted-post-outline-color) !important; 37 | } 38 | 39 | .post-container, #float .post-container, .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats { 40 | border-bottom: 1px solid var(--post-outline-color); 41 | border-right: 1px solid var(--post-outline-color); 42 | border-top: 1px solid var(--post-color); 43 | border-left: 1px solid var(--post-color); 44 | } 45 | 46 | table { 47 | border: none; 48 | border-spacing: 2px; 49 | } 50 | 51 | .captcha { 52 | filter: invert(90%); 53 | } 54 | 55 | @media only screen and (max-width: 600px) { 56 | 57 | .post-info{ 58 | background: none; 59 | border-bottom: none; 60 | } 61 | 62 | .anchor:target + .post-container .post-info, .post-container.highlighted .post-info { 63 | border-bottom: none; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /gulp/res/css/themes/yotsuba b.css: -------------------------------------------------------------------------------- 1 | /*yotsuba b*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #98E; 5 | --alt-font-color: black; 6 | --background-top: #d6daf0; 7 | --background-rest: #eef2ff; 8 | --navbar-color: #D6DAF0; 9 | --post-color: #D6DAF0; 10 | --post-outline-color: #B7C5D9; 11 | --label-color: #98E; 12 | --box-border-color: #000; 13 | --darken: #00000010; 14 | --highlighted-post-color: #d6bad0; 15 | --highlighted-post-outline-color: #ba9dbf; 16 | --board-title: #af0a0f; 17 | --hr: lightgray; 18 | --font-color: black; 19 | --name-color: #117743; 20 | --capcode-color: #f00; 21 | --subject-color: #0F0C5D; 22 | --link-color: #34345C; 23 | --post-link-color: #d00; 24 | --link-hover: #d00; 25 | --input-borders: #a9a9a9; 26 | --input-color: #000; 27 | --input-background: white; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: green; 31 | --pinktext-color:#E0727F; 32 | } 33 | 34 | .post-container, #float .post-container, .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats { 35 | border-width: 0 1px 1px 0; 36 | } 37 | -------------------------------------------------------------------------------- /gulp/res/css/themes/yotsuba.css: -------------------------------------------------------------------------------- 1 | /*yotsuba b*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #800; 5 | --alt-font-color: #fff; 6 | --background-top: #fed6af90; 7 | --background-rest: #ffe; 8 | --navbar-color: #f0e0d6; 9 | --post-color: #f0e0d6; 10 | --post-outline-color: #d9bfb7; 11 | --label-color: #EA8; 12 | --box-border-color: #000; 13 | --darken: #00000010; 14 | --highlighted-post-color: #f0c0b0; 15 | --highlighted-post-outline-color: #d99f91; 16 | --board-title: #af0a0f; 17 | --hr: #D9BFB7; 18 | --font-color: #800000; 19 | --name-color: #800000; 20 | --capcode-color: #f00; 21 | --subject-color: #f00; 22 | --link-color: #800; 23 | --post-link-color: navy; 24 | --link-hover: #f00; 25 | --input-borders: #a9a9a9; 26 | --input-color: #000; 27 | --input-background: white; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: #789922; 31 | --pinktext-color:#E0727F; 32 | } 33 | 34 | .post-container, #float .post-container, .stickynav, .pages, .toggle-summary, .catalog-tile, #livetext, #threadstats { 35 | border-width: 0 1px 1px 0; 36 | } 37 | -------------------------------------------------------------------------------- /gulp/res/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /gulp/res/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #00aba9 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /gulp/res/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/icons/favicon.ico -------------------------------------------------------------------------------- /gulp/res/icons/favicon2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/icons/favicon2.ico -------------------------------------------------------------------------------- /gulp/res/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/icons/mstile-150x150.png -------------------------------------------------------------------------------- /gulp/res/icons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/file/android-chrome-144x144.png", 7 | "sizes": "144x144", 8 | "type": "image/png" 9 | } 10 | ], 11 | "theme_color": "", 12 | "background_color": "" 13 | } 14 | -------------------------------------------------------------------------------- /gulp/res/img/attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/attachment.png -------------------------------------------------------------------------------- /gulp/res/img/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/audio.png -------------------------------------------------------------------------------- /gulp/res/img/bumplock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/bumplock.png -------------------------------------------------------------------------------- /gulp/res/img/cyclic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/cyclic.png -------------------------------------------------------------------------------- /gulp/res/img/defaultbanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/defaultbanner.png -------------------------------------------------------------------------------- /gulp/res/img/deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/deleted.png -------------------------------------------------------------------------------- /gulp/res/img/dice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/dice.png -------------------------------------------------------------------------------- /gulp/res/img/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/flags.png -------------------------------------------------------------------------------- /gulp/res/img/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/lock.png -------------------------------------------------------------------------------- /gulp/res/img/ratelimit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/ratelimit.png -------------------------------------------------------------------------------- /gulp/res/img/spoiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/spoiler.png -------------------------------------------------------------------------------- /gulp/res/img/sticky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/sticky.png -------------------------------------------------------------------------------- /gulp/res/img/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/ba30cc1cae42cc2963ae0177081a63c7d2d166b9/gulp/res/img/video.png -------------------------------------------------------------------------------- /gulp/res/js/captcha.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', (event) => { 2 | 3 | const captchaFields = document.getElementsByClassName('captchafield'); 4 | let refreshing = false; 5 | 6 | const updateCaptchaImages = (url) => { 7 | for (let i = 0; i < captchaFields.length; i++) { 8 | if (captchaFields[i].previousSibling.children.length > 0) { 9 | captchaFields[i].previousSibling.children[0].src = url; 10 | } 11 | } 12 | }; 13 | 14 | const refreshCaptchas = function(e) { 15 | if (refreshing) { 16 | return; 17 | } 18 | refreshing = true; 19 | document.cookie = 'captchaid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; 20 | const captchaImg = this; 21 | const xhr = new XMLHttpRequest(); 22 | xhr.onload = () => { 23 | refreshing = false; 24 | updateCaptchaImages(xhr.responseURL); 25 | } 26 | xhr.onerror = () => { 27 | refreshing = false; 28 | } 29 | xhr.open('GET', '/captcha', true); 30 | xhr.send(null); 31 | }; 32 | 33 | const loadCaptcha = function(e) { 34 | const field = e.target; 35 | const captchaDiv = field.previousSibling; 36 | const captchaImg = document.createElement('img'); 37 | const refreshDiv = document.createElement('div'); 38 | refreshDiv.classList.add('captcharefresh', 'noselect'); 39 | refreshDiv.addEventListener('click', refreshCaptchas, true); 40 | refreshDiv.textContent = '↻'; 41 | field.placeholder = 'loading'; 42 | captchaImg.src = '/captcha'; 43 | captchaImg.onload = function() { 44 | field.placeholder = ''; 45 | captchaDiv.appendChild(captchaImg); 46 | captchaDiv.appendChild(refreshDiv); 47 | captchaDiv.style.display = ''; 48 | } 49 | }; 50 | 51 | for (let i = 0; i < captchaFields.length; i++) { 52 | const field = captchaFields[i]; 53 | if (field.form.action.endsWith('/forms/blockbypass')) { 54 | return loadCaptcha({target: field }) 55 | } 56 | field.placeholder = 'focus to load captcha'; 57 | field.addEventListener('focus', loadCaptcha, { once: true }); 58 | } 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /gulp/res/js/catalog.js: -------------------------------------------------------------------------------- 1 | if (isCatalog) { 2 | window.addEventListener('DOMContentLoaded', (event) => { 3 | 4 | const getFilterSelector = (filter) => { 5 | filter = filter.replace(/["\\]/g, '\\$&').toLowerCase(); 6 | return `.catalog-tile:not([data-filter*="${filter}"])`; 7 | } 8 | const catalogFilter = document.getElementById('catalogfilter'); 9 | catalogFilter.value = ''; 10 | const filterCatalog = () => { 11 | let existingRule; 12 | let i = 0; 13 | for (; i < renderSheet[rulesKey].length; i++) { 14 | if (renderSheet[rulesKey][i].selectorText.startsWith('.catalog-tile:not([data-filter*=')) { 15 | existingRule = renderSheet[rulesKey][i]; 16 | break; 17 | } 18 | } 19 | if (catalogFilter.value.length > 0) { 20 | if (existingRule) { 21 | existingRule.selectorText = getFilterSelector(catalogFilter.value); 22 | } else { 23 | renderSheet.insertRule(getFilterSelector(catalogFilter.value) + ' { display: none; }'); 24 | } 25 | } else { 26 | renderSheet.deleteRule(i); 27 | } 28 | } 29 | catalogFilter.addEventListener('input', filterCatalog, false); 30 | 31 | const sorts = { 32 | date: (a, b) => { 33 | //date newest first 34 | return new Date(b.dataset.date) - new Date(a.dataset.date); 35 | }, 36 | bump: (a, b) => { 37 | //bump date most recent first 38 | return a.dataset.bump - b.dataset.bump; 39 | }, 40 | replies: (a, b) => { 41 | //replies most first 42 | return b.dataset.replies - a.dataset.replies; 43 | }, 44 | } 45 | const tiles = document.getElementsByClassName('catalog-tile'); 46 | const catalogSort = document.getElementById('catalogsort'); 47 | catalogSort.value = ''; 48 | const sortCatalog = (mode) => { 49 | console.log('sorting catalog', mode); 50 | const tiles = document.getElementsByClassName('catalog-tile'); 51 | const tilesArray = Array.from(tiles); 52 | tilesArray 53 | .sort(sorts[mode] || (() => 1)) 54 | .forEach((tile, index) => { 55 | tile.style.order = index; 56 | }); 57 | } 58 | catalogSort.addEventListener('change', (e) => { sortCatalog(e.target.value) }, false); 59 | 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /gulp/res/js/counter.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', (event) => { 2 | 3 | const messageBox = document.getElementById('message'); 4 | 5 | if (messageBox) { 6 | const messageBoxLabel = messageBox.previousSibling; 7 | const maxLength = messageBox.getAttribute('maxlength'); 8 | const minLength = messageBox.getAttribute('minlength'); 9 | let currentLength = messageBox.value.length; 10 | const counter = document.createElement('small'); 11 | messageBoxLabel.appendChild(counter); 12 | 13 | const updateCounter = () => { 14 | counter.innerText = `(${currentLength}/${maxLength})`; 15 | if (currentLength >= maxLength || currentLength < minLength) { 16 | counter.style.color = 'red'; 17 | } else { 18 | counter.removeAttribute('style'); 19 | } 20 | }; 21 | 22 | const updateLength = function(e) { 23 | if (messageBox.value.length > maxLength) { 24 | messageBox.value = messageBox.value.substring(0,maxLength); 25 | } 26 | currentLength = messageBox.value.length; 27 | updateCounter(); 28 | }; 29 | 30 | updateCounter(); 31 | 32 | messageBox.addEventListener('input', updateLength); 33 | 34 | } 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /gulp/res/js/localstorage.js: -------------------------------------------------------------------------------- 1 | const isCatalog = window.location.pathname.endsWith('catalog.html'); 2 | const isThread = /\/\w+\/thread\/\d+.html/.test(window.location.pathname); 3 | const isModView = /\/\w+\/manage\/(thread\/)?(index|\d+).html/.test(window.location.pathname); 4 | 5 | function setLocalStorage(key, value) { 6 | try { 7 | localStorage.setItem(key, value); 8 | } catch (e) { 9 | clearLocalStorageJunk(); 10 | } finally { 11 | localStorage.setItem(key, value); 12 | } 13 | } 14 | 15 | function clearLocalStorageJunk() { 16 | //clears hover cache when localstorage gets full 17 | const hoverCaches = Object.keys(localStorage).filter(k => k.startsWith('hovercache')); 18 | for(let i = 0; i < hoverCaches.length; i++) { 19 | localStorage.removeItem(hoverCaches[i]); 20 | } 21 | } 22 | 23 | function setDefaultLocalStorage(key, value) { 24 | if (!localStorage.getItem(key)) { 25 | setLocalStorage(key, value); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gulp/res/js/settings.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', (event) => { 2 | 3 | let settingsModal; 4 | let settingsBg; 5 | 6 | const hideSettings = () => { 7 | settingsModal.style.display = 'none'; 8 | settingsBg.style.display = 'none'; 9 | } 10 | 11 | const openSettings = (data) => { 12 | settingsModal.style.display = 'unset'; 13 | settingsBg.style.display = 'unset'; 14 | } 15 | 16 | const modalHtml = modal({ 17 | modal: { 18 | title: 'Settings', 19 | settings: { 20 | themes, 21 | codeThemes, 22 | }, 23 | hidden: true, 24 | } 25 | }); 26 | 27 | const inserted = document.body.insertAdjacentHTML('afterbegin', modalHtml); 28 | settingsBg = document.getElementsByClassName('modal-bg')[0]; 29 | settingsModal = document.getElementsByClassName('modal')[0]; 30 | 31 | settingsBg.onclick = hideSettings; 32 | settingsModal.getElementsByClassName('close')[0].onclick = hideSettings; 33 | 34 | const settings = document.getElementById('settings'); 35 | if (settings) { //can be false if we are in minimal view 36 | settings.onclick = openSettings; 37 | } 38 | 39 | window.dispatchEvent(new CustomEvent('settingsReady')); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /gulp/res/js/titlescroll.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', (event) => { 2 | 3 | let focused = document.hasFocus(); 4 | let unread = []; 5 | const originalTitle = document.title; 6 | 7 | const changeFavicon = (href) => { 8 | const currentFav = document.head.querySelector('link[type="image/x-icon"]'); 9 | const newFav = document.createElement('link'); 10 | newFav.type = 'image/x-icon'; 11 | newFav.rel = 'shortcut icon'; 12 | newFav.href = href; 13 | currentFav.remove(); 14 | document.head.appendChild(newFav); 15 | } 16 | 17 | const isVisible = (e) => { 18 | const top = e.getBoundingClientRect().top; 19 | const bottom = e.getBoundingClientRect().bottom; 20 | const height = window.innerHeight; 21 | return (top >= 0 || bottom-top > height) && bottom <= height; 22 | } 23 | 24 | const updateTitle = () => { 25 | if (unread.length === 0) { 26 | document.title = originalTitle; 27 | changeFavicon('/favicon.ico'); 28 | } else { 29 | document.title = `(${unread.length}) ${originalTitle}`; 30 | changeFavicon('/file/favicon2.ico'); 31 | } 32 | } 33 | 34 | const focusChange = () => { 35 | focused = !focused; 36 | } 37 | 38 | const updateVisible = () => { 39 | const unreadBefore = unread.length; 40 | unread = unread.filter(p => { 41 | if (isVisible(p)) { 42 | p.classList.remove('highlighted'); 43 | return false; 44 | } 45 | return true; 46 | }); 47 | if (unreadBefore !== unread.length) { 48 | updateTitle(); 49 | } 50 | } 51 | 52 | window.onfocus = focusChange; 53 | window.onblur = focusChange; 54 | window.addEventListener('scroll', updateVisible); 55 | 56 | window.addEventListener('addPost', function(e) { 57 | if (e.detail.hover) { 58 | return; //dont need to handle hovered posts for this 59 | } 60 | const post = e.detail.post; 61 | //if browsing another tab or the post is out of scroll view 62 | if (!focused || !isVisible(post)) { 63 | post.classList.add('highlighted'); 64 | unread.push(post); 65 | updateTitle(); 66 | } 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /helpers/affectedboards.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Posts } = require(__dirname+'/../db/'); 4 | 5 | module.exports = async (posts, deleting) => { 6 | 7 | //get a map of boards to threads affected 8 | const boardThreadMap = {}; 9 | for (let i = 0; i < posts.length; i++) { 10 | const post = posts[i]; 11 | if (!boardThreadMap[post.board]) { 12 | boardThreadMap[post.board] = { 13 | 'directThreads': new Set(), 14 | 'threads': new Set() 15 | }; 16 | } 17 | if (!post.thread) { 18 | //a thread was directly selected on this board, not just posts. so we handle deletes differently 19 | boardThreadMap[post.board].directThreads.add(post.postId); 20 | } 21 | const threadId = post.thread || post.postId; 22 | boardThreadMap[post.board].threads.add(threadId); 23 | } 24 | 25 | const beforePages = {}; 26 | const threadBoards = Object.keys(boardThreadMap); 27 | //get number of pages for each before actions for deleting old pages and changing page nav numbers incase number of pages changes 28 | if (deleting) { 29 | await Promise.all(threadBoards.map(async board => { 30 | beforePages[board] = Math.ceil((await Posts.getPages(board)) / 10); 31 | })); 32 | } 33 | 34 | return { boardThreadMap, beforePages, threadBoards }; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /helpers/checks/alphanumregex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //literally just alphanumeric ¯\_(ツ)_/¯ 4 | module.exports = /^[a-zA-Z0-9]+$/ 5 | -------------------------------------------------------------------------------- /helpers/checks/bancheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/') 4 | , hasPerms = require(__dirname+'/hasperms.js') 5 | , dynamicResponse = require(__dirname+'/../dynamic.js'); 6 | 7 | module.exports = async (req, res, next) => { 8 | 9 | if (res.locals.permLevel > 1) {//global staff or admin bypass 10 | const bans = await Bans.find(res.locals.ip, res.locals.board ? res.locals.board._id : null); 11 | if (bans && bans.length > 0) { 12 | const globalBans = bans.filter(ban => { return ban.board === null }); 13 | if (globalBans.length > 0 || (res.locals.permLevel >= 4 && globalBans.length !== bans.length)) { 14 | //board staff bypass bans on their own board, but not global bans 15 | const unseenBans = bans.filter(b => !b.seen).map(b => b._id); 16 | await Bans.markSeen(unseenBans); //mark bans as seen 17 | bans.forEach(ban => ban.seen = true); //mark seen as true in memory for user viewed ban page 18 | return res.status(403).render('ban', { 19 | bans: bans, 20 | }); 21 | } 22 | } 23 | } 24 | next(); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /helpers/checks/calcpermsmiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const hasPerms = require(__dirname+'/hasperms.js'); 4 | 5 | module.exports = (req, res, next) => { 6 | res.locals.permLevel = hasPerms(req, res); 7 | next(); 8 | } 9 | -------------------------------------------------------------------------------- /helpers/checks/csrfmiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const csrf = require('csurf')(); 4 | 5 | module.exports = csrf; 6 | -------------------------------------------------------------------------------- /helpers/checks/dnsbl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cache = require(__dirname+'/../../redis.js') 4 | , dynamicResponse = require(__dirname+'/../dynamic.js') 5 | , deleteTempFiles = require(__dirname+'/../files/deletetempfiles.js') 6 | , { dnsbl, blockBypass } = require(__dirname+'/../../configs/main.js') 7 | , { batch } = require('dnsbl'); 8 | 9 | module.exports = async (req, res, next) => { 10 | 11 | if (dnsbl.enabled && dnsbl.blacklists.length > 0 //if dnsbl enabled and has more than 0 blacklists 12 | && (!res.locals.blockBypass || !blockBypass.bypassDnsbl)) { //and there is no valid block bypass, or they do not bypass dnsbl 13 | const ip = req.headers['x-real-ip'] || req.connection.remoteAddress; 14 | let isBlacklisted = await cache.get(`blacklisted:${ip}`); 15 | if (isBlacklisted === null) { //not cached 16 | const dnsblResp = await batch(ip, dnsbl.blacklists); 17 | isBlacklisted = dnsblResp.some(r => r.listed === true); 18 | await cache.set(`blacklisted:${ip}`, isBlacklisted, dnsbl.cacheTime); 19 | } 20 | if (isBlacklisted) { 21 | deleteTempFiles(req).catch(e => console.error); 22 | return dynamicResponse(req, res, 403, 'message', { 23 | 'title': 'Forbidden', 24 | 'message': 'Your IP address is listed on a blacklist', 25 | 'redirect': req.headers.referer || '/' 26 | }); 27 | } 28 | } 29 | return next(); 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /helpers/checks/hasperms.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res) => { 4 | if (req.session) { 5 | const { authenticated, user } = req.session; 6 | if (authenticated === true && user != null) { 7 | if (user.authLevel < 4) { //assigned levels 8 | return user.authLevel; 9 | } 10 | if (res.locals.board != null) { 11 | if (res.locals.board.owner === user.username) { 12 | return 2; //board owner 2 13 | } else if (res.locals.board.settings.moderators.includes(user.username) === true) { 14 | return 3; //board staff 3 15 | } 16 | } 17 | } 18 | } 19 | return 4; //not logged in, not staff or moderator 20 | } 21 | -------------------------------------------------------------------------------- /helpers/checks/haspermsmiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cache = {}; 4 | 5 | module.exports = (requiredLevel) => { 6 | 7 | return cache[requiredLevel] || (function(req, res, next) { 8 | if (res.locals.permLevel > requiredLevel) { 9 | return res.status(403).render('message', { 10 | 'title': 'Forbidden', 11 | 'message': 'No Permission', 12 | 'redirect': req.headers.referer || '/' 13 | }); 14 | } 15 | next(); 16 | }); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /helpers/checks/isloggedin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res, next) => { 4 | if (req.session && req.session.authenticated === true) { 5 | return next(); 6 | } 7 | let goto; 8 | if (req.method === 'GET' && req.path) { 9 | //coming from a GET page isLoggedIn middleware check 10 | goto = req.path; 11 | } 12 | return res.redirect(`/login.html${goto ? '?goto='+goto : ''}`); 13 | } 14 | -------------------------------------------------------------------------------- /helpers/checks/spamcheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/../../db/db.js') 4 | , { Posts } = require(__dirname+'/../../db/') 5 | , timeUtils = require(__dirname+'/../timeutils.js') 6 | 7 | module.exports = async (req, res) => { 8 | 9 | if (res.locals.permLevel <= 1) { //global staff bypass spam check 10 | return false; 11 | } 12 | 13 | const now = Date.now(); 14 | const last120id = Mongo.ObjectId.createFromTime(Math.floor((now - (timeUtils.MINUTE*2))/1000)); 15 | const last30id = Mongo.ObjectId.createFromTime(Math.floor((now - (timeUtils.MINUTE*0.5))/1000)); 16 | const last5id = Mongo.ObjectId.createFromTime(Math.floor((now - 5000)/1000)); 17 | const ors = []; 18 | const contentOr = []; 19 | if (res.locals.numFiles > 0) { 20 | contentOr.push({ 21 | 'files': { 22 | '$elemMatch': { 23 | 'hash': { //any file hash will match, doesnt need to be all 24 | '$in': req.files.file.map(f => f.sha256) 25 | } 26 | } 27 | } 28 | }); 29 | } 30 | if (req.body.message) { 31 | contentOr.push({ 32 | 'nomarkup': req.body.message 33 | }) 34 | } 35 | //matching content from any IP in the past 30 seconds 36 | ors.push({ 37 | '_id': { 38 | '$gt': last30id 39 | }, 40 | '$or': contentOr 41 | }); 42 | //matching content from same IP in last 2 minutes 43 | ors.push({ 44 | '_id': { 45 | '$gt': last120id 46 | }, 47 | 'ip.single': res.locals.ip.single, 48 | '$or': contentOr 49 | }); 50 | //any posts from same IP in past 5 seconds TODO: make this just use a redis key of IP and expire after 5 seconds 51 | ors.push({ 52 | '_id': { 53 | '$gt': last5id 54 | }, 55 | 'ip.single': res.locals.ip.single 56 | }) 57 | 58 | let flood = await Posts.db.find({ 59 | '$or': ors 60 | }).toArray(); 61 | 62 | return flood.length > 0; 63 | 64 | } 65 | -------------------------------------------------------------------------------- /helpers/commit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('child_process') 4 | .execSync('git rev-parse --short HEAD') 5 | .toString() 6 | .trim(); 7 | -------------------------------------------------------------------------------- /helpers/datearray.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //https://stackoverflow.com/a/4413721 4 | module.exports = (startDate, stopDate) => { 5 | const dateArray = new Array(); 6 | let currentDate = startDate; 7 | while (currentDate <= stopDate) { 8 | dateArray.push(new Date (currentDate.valueOf())); 9 | currentDate.setDate(currentDate.getDate() + 1); 10 | } 11 | return dateArray; 12 | } 13 | -------------------------------------------------------------------------------- /helpers/decodequeryip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const escapeRegExp = require(__dirname+'/escaperegexp.js') 4 | , { isIP } = require('net') 5 | , { ipHashPermLevel } = require(__dirname+'/../configs/main.js') 6 | 7 | module.exports = (query, permLevel) => { 8 | if (query.ip && typeof query.ip === 'string') { 9 | const decoded = decodeURIComponent(query.ip); 10 | if (permLevel <= ipHashPermLevel && isIP(decoded)) { //if perms to view raw ip, allow querying 11 | return decoded; 12 | } else if (decoded.length === 10) { //otherwise, only allow last 10 char substring 13 | return new RegExp(`${escapeRegExp(decoded)}$`); 14 | } 15 | } 16 | return null; //else, no ip filter 17 | } 18 | -------------------------------------------------------------------------------- /helpers/dnsbl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cache = require(__dirname+'/../redis.js') 4 | , dynamicResponse = require(__dirname+'/dynamic.js') 5 | , { dnsbl } = require(__dirname+'/../configs/main.js') 6 | , { batch } = require('dnsbl'); 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | if (dnsbl.enabled) { 11 | const ip = req.headers['x-real-ip'] || req.connection.remoteAddress; 12 | let isBlacklisted = await cache.get(`blacklisted:${ip}`); 13 | if (isBlacklisted === null) { //not cached 14 | const dnsblResp = await batch(ip, dnsbl.blacklists); 15 | isBlacklisted = dnsblResp.some(r => r.listed === true); 16 | await cache.set(`blacklisted:${ip}`, isBlacklisted, dnsbl.cacheTime); 17 | } 18 | if (isBlacklisted) { 19 | return dynamicResponse(req, res, 403, 'message', { 20 | 'title': 'Forbidden', 21 | 'message': 'Your IP address is listed on a blacklist', 22 | 'redirect': req.headers.referer || '/' 23 | }); 24 | } 25 | } 26 | return next(); 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /helpers/dointerval.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (func, interval, runFirst) => { 4 | if (runFirst) { 5 | func(); 6 | } 7 | setInterval(func, interval); 8 | } 9 | -------------------------------------------------------------------------------- /helpers/dynamic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res, code, page, data) => { 4 | if (req.body.minimal) { 5 | data.minimal = true; 6 | } 7 | res.status(code); 8 | if (req.headers['x-using-xhr'] != null) { 9 | return res.json(data); 10 | } else { 11 | return res.render(page, data); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /helpers/escaperegexp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (string) => { 4 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 5 | } 6 | -------------------------------------------------------------------------------- /helpers/files/deletefailed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { remove } = require('fs-extra') 4 | , uploadDirectory = require(__dirname+'/uploadDirectory.js'); 5 | 6 | module.exports = async (filenames, folder) => { 7 | 8 | await Promise.all(filenames.map(async filename => { 9 | remove(`${uploadDirectory}/${folder}/${filename}`) 10 | })); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /helpers/files/deleteold.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { stat, remove, readdir } = require('fs-extra') 4 | , uploadDirectory = require(__dirname+'/uploadDirectory.js'); 5 | 6 | //takes directory name and timestamp to delete files older than 7 | module.exports = async (directory, olderThan) => { 8 | const dir = `${uploadDirectory}/${directory}` 9 | const files = await readdir(dir); 10 | if (files.length > 0) { 11 | return Promise.all(files.map(async file => { 12 | const filePath = `${dir}/${file}`; 13 | try { 14 | const stats = await stat(filePath); 15 | const expiry = new Date(olderThan).getTime(); 16 | if (stats.ctime.getTime() < expiry) { 17 | await remove(filePath); 18 | } 19 | } catch (e) { 20 | console.error(e); 21 | } 22 | })); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /helpers/files/deletepostfiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { remove } = require('fs-extra') 4 | , uploadDirectory = require(__dirname+'/uploadDirectory.js') 5 | 6 | module.exports = (files) => { 7 | 8 | //delete all the files and thumbs 9 | return Promise.all(files.map(async file => { 10 | return Promise.all([ 11 | remove(`${uploadDirectory}/file/${file.filename}`), 12 | file.hasThumb ? remove(`${uploadDirectory}/file/thumb-${file.hash}${file.thumbextension}`) : void 0, 13 | ]).catch(e => console.error); 14 | })); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /helpers/files/deletetempfiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { remove } = require('fs-extra') 4 | 5 | module.exports = async (req) => { 6 | 7 | if (req.files != null) { 8 | let files = []; 9 | const keys = Object.keys(req.files); 10 | for (let i = 0; i < keys.length; i++) { 11 | const val = req.files[keys[i]]; 12 | if (Array.isArray(val)) { 13 | files = files.concat(val); 14 | } else { 15 | files.push(val); 16 | } 17 | } 18 | return Promise.all(files.map(async file => { 19 | remove(file.tempFilePath); 20 | })); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /helpers/files/ffprobe.js: -------------------------------------------------------------------------------- 1 | const ffmpeg = require('fluent-ffmpeg') 2 | , configs = require(__dirname+'/../../configs/main.js') 3 | , uploadDirectory = require(__dirname+'/uploadDirectory.js'); 4 | 5 | module.exports = (filename, folder, temp) => { 6 | 7 | return new Promise((resolve, reject) => { 8 | ffmpeg.ffprobe(temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`, (err, metadata) => { 9 | if (err) { 10 | return reject(err) 11 | } 12 | return resolve(metadata); 13 | }); 14 | }); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /helpers/files/fixgifs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (file) => { 4 | //handle gifs with multiple geometry and size 5 | if (Array.isArray(file.geometry)) { 6 | file.geometry = file.geometry[0]; 7 | } 8 | if (Array.isArray(file.sizeString)) { 9 | file.sizeString = file.sizeString[0]; 10 | } 11 | if (Array.isArray(file.geometryString)) { 12 | file.geometryString = file.geometryString[0]; 13 | } 14 | return file; 15 | } 16 | -------------------------------------------------------------------------------- /helpers/files/formatsize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] 4 | , k = 1024; 5 | 6 | module.exports = (bytes) => { 7 | if (bytes === 0) { 8 | return '0B'; 9 | } 10 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 11 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`; 12 | }; 13 | -------------------------------------------------------------------------------- /helpers/files/imageidentify.js: -------------------------------------------------------------------------------- 1 | const gm = require('gm') 2 | , configs = require(__dirname+'/../../configs/main.js') 3 | , uploadDirectory = require(__dirname+'/uploadDirectory.js'); 4 | 5 | module.exports = (filename, folder, temp) => { 6 | 7 | return new Promise((resolve, reject) => { 8 | const filePath = temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`; 9 | gm(`${filePath}[0]`) //0 for first frame of gifs, much faster 10 | .identify(function (err, data) { 11 | if (err) { 12 | return reject(err); 13 | } 14 | return resolve(data) 15 | }); 16 | }); 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /helpers/files/imagethumbnail.js: -------------------------------------------------------------------------------- 1 | const gm = require('gm') 2 | , { thumbSize } = require(__dirname+'/../../configs/main.js') 3 | , uploadDirectory = require(__dirname+'/uploadDirectory.js'); 4 | 5 | module.exports = (file) => { 6 | 7 | return new Promise((resolve, reject) => { 8 | gm(`${uploadDirectory}/file/${file.filename}[0]`) //0 for first gif frame 9 | .resize(Math.min(thumbSize, file.geometry.width), Math.min(thumbSize, file.geometry.height)) 10 | .write(`${uploadDirectory}/file/thumb-${file.hash}${file.thumbextension}`, function (err) { 11 | if (err) { 12 | return reject(err); 13 | } 14 | return resolve(); 15 | }); 16 | }); 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /helpers/files/mimetypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const image = new Set([ 4 | 'image/jpeg', 5 | 'image/pjpeg', 6 | 'image/png', 7 | 'image/bmp', 8 | ]); 9 | 10 | const animatedImage = new Set([ 11 | 'image/gif', 12 | 'image/webp', 13 | 'image/apng', 14 | ]); 15 | 16 | const video = new Set([ 17 | 'video/mpeg', 18 | 'video/quicktime', 19 | 'video/mp4', 20 | 'video/webm', 21 | 'video/x-matroska', 22 | ]); 23 | 24 | const audio = new Set([ 25 | 'audio/mp3', 26 | 'audio/mpeg', 27 | 'audio/ogg', 28 | 'audio/wave', 29 | 'audio/wav', 30 | ]); 31 | 32 | const other = new Set(require(__dirname+'/../../configs/main.js').otherMimeTypes); 33 | 34 | module.exports = { 35 | 36 | allowed: (mimetype, options) => { 37 | return (options.image && image.has(mimetype)) || 38 | (options.animatedImage && animatedImage.has(mimetype)) || 39 | (options.video && video.has(mimetype)) || 40 | (options.audio && audio.has(mimetype)) || 41 | (options.other && other.has(mimetype)); 42 | }, 43 | 44 | image, animatedImage, video, audio, other 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /helpers/files/moveupload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const uploadDirectory = require(__dirname+'/uploadDirectory.js'); 4 | 5 | module.exports = (file, filename, folder) => { 6 | 7 | return new Promise((resolve, reject) => { 8 | file.mv(`${uploadDirectory}/${folder}/${filename}`, function (err) { 9 | if (err) { 10 | return reject(err); 11 | } 12 | return resolve(); 13 | }); 14 | }); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /helpers/files/uploadDirectory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path') 4 | , directory = path.join(__dirname+'/../../static') 5 | 6 | module.exports = directory; 7 | -------------------------------------------------------------------------------- /helpers/files/videothumbnail.js: -------------------------------------------------------------------------------- 1 | const ffmpeg = require('fluent-ffmpeg') 2 | , { thumbSize } = require(__dirname+'/../../configs/main.js') 3 | , uploadDirectory = require(__dirname+'/uploadDirectory.js'); 4 | 5 | module.exports = (file, geometry, frames) => { 6 | 7 | return new Promise((resolve, reject) => { 8 | ffmpeg(`${uploadDirectory}/file/${file.filename}`) 9 | .on('end', () => { 10 | return resolve(); 11 | }) 12 | .on('error', function(err, stdout, stderr) { 13 | return reject(err); 14 | }) 15 | .screenshots({ 16 | timestamps: [(frames === 'N/A' ? 0 : '1%')],//1% should remedy black first frames or fade-ins 17 | count: 1, 18 | filename: `thumb-${file.hash}${file.thumbextension}`, 19 | folder: `${uploadDirectory}/file/`, 20 | size: geometry.width > geometry.height ? `${thumbSize}x?` : `?x${thumbSize}` 21 | //keep aspect ratio, but also making sure taller/wider thumbs dont exceed thumbSize in either dimension 22 | }); 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /helpers/haship.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { ipHashSecret } = require(__dirname+'/../configs/main.js') 4 | , { createHash } = require('crypto'); 5 | 6 | module.exports = (ip) => createHash('sha256').update(ipHashSecret + ip).digest('base64'); 7 | -------------------------------------------------------------------------------- /helpers/numfiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res, next) => { 4 | res.locals.numFiles = 0; 5 | if (req.files && req.files.file) { 6 | if (Array.isArray(req.files.file)) { 7 | res.locals.numFiles = req.files.file.filter(file => file.size > 0).length; 8 | } else { 9 | res.locals.numFiles = req.files.file.size > 0 ? 1 : 0; 10 | req.files.file = [req.files.file]; 11 | } 12 | } 13 | next(); 14 | } 15 | -------------------------------------------------------------------------------- /helpers/pagequeryconverter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (query, limit) => { 4 | const nopage = { ...query }; 5 | delete nopage.page; 6 | const queryString = new URLSearchParams(nopage).toString(); 7 | let page; 8 | if (query.page && Number.isSafeInteger(parseInt(query.page))) { 9 | page = parseInt(query.page); 10 | if (page <= 0) { 11 | page = 1; 12 | } 13 | } else { 14 | page = 1; 15 | } 16 | const offset = (page-1) * limit; 17 | return { queryString, page, offset }; 18 | } 19 | -------------------------------------------------------------------------------- /helpers/posting/diceroll.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (match, numdice, numsides, operator, modifier) => { 4 | numdice = parseInt(numdice); 5 | numsides = parseInt(numsides); 6 | let sum = (Math.floor(Math.random() * numsides) + 1) * numdice; 7 | if (modifier && operator) { 8 | modifier = parseInt(modifier); 9 | //do i need to make sure it doesnt go negative or maybe give absolute value? 10 | if (operator === '+') { 11 | sum += modifier; 12 | } else { 13 | sum -= modifier; 14 | } 15 | } 16 | return `(${match}) Rolled ${numdice} dice with ${numsides} sides${modifier ? ' and modifier '+operator+modifier : '' } = ${sum}`; 17 | } 18 | -------------------------------------------------------------------------------- /helpers/posting/escape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const entities = { 4 | "'": ''', 5 | '/': '/', 6 | '`': '`', 7 | '=': '=', 8 | '&': '&', 9 | '<': '<', 10 | '>': '>', 11 | '"': '"' 12 | } 13 | 14 | module.exports = (string) => { 15 | return string.replace(/[&<>"'`=\/]/g, s => entities[s]); 16 | } 17 | -------------------------------------------------------------------------------- /helpers/posting/linkmatch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const parenPairRegex = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]\))*\))*\)/g 4 | 5 | module.exports = (match) => { 6 | let trimmedMatch; 7 | let excess = ''; 8 | const parensPairs = match.match(parenPairRegex); 9 | //naive solution 10 | if (parensPairs) { 11 | const lastMatch = parensPairs[parensPairs.length-1]; 12 | const lastIndex = match.lastIndexOf(lastMatch); 13 | trimmedMatch = match.substring(0, lastIndex+lastMatch.length); 14 | excess = match.substring(lastIndex+lastMatch.length); 15 | } else if (match.indexOf(')') !== -1){ 16 | trimmedMatch = match.substring(0, match.indexOf(')')); 17 | excess = match.substring(match.indexOf(')')); 18 | } else { 19 | trimmedMatch = match; 20 | } 21 | trimmedMatch = trimmedMatch 22 | .replace(/\(/g, '%28') 23 | .replace(/\)/g, '%29'); 24 | return `${trimmedMatch}${excess}`; 25 | }; 26 | -------------------------------------------------------------------------------- /helpers/posting/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const linkQuotes = require(__dirname+'/quotes.js') 4 | , { markdown } = require(__dirname+'/markdown.js') 5 | , sanitizeOptions = require(__dirname+'/sanitizeoptions.js') 6 | , sanitize = require('sanitize-html'); 7 | 8 | module.exports = async (inputMessage, boardName, threadId=null) => { 9 | 10 | let message = inputMessage; 11 | let quotes = []; 12 | let crossquotes = []; 13 | 14 | //markdown a post, link the quotes, sanitize and return message and quote arrays 15 | if (message && message.length > 0) { 16 | message = markdown(message); 17 | const { quotedMessage, threadQuotes, crossQuotes } = await linkQuotes(boardName, message, threadId); 18 | message = quotedMessage; 19 | quotes = threadQuotes; 20 | crossquotes = crossQuotes; 21 | message = sanitize(message, sanitizeOptions.after); 22 | } 23 | 24 | return { message, quotes, crossquotes }; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /helpers/posting/name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getTripCode = require(__dirname+'/tripcode.js') 4 | , nameRegex = /^(?(?!##).*?)?(?:##(?[^ ]{1}.*?))?(?##(? .*?)?)?$/ 5 | , capcodeLevels = ['Admin', 'Global Staff', 'Board Owner', 'Board Mod']; 6 | 7 | module.exports = async (inputName, permLevel, boardSettings) => { 8 | 9 | const { forceAnon, defaultName } = boardSettings; 10 | 11 | let name = defaultName; 12 | let tripcode = null; 13 | let capcode = null; 14 | if ((permLevel < 4 || !forceAnon) && inputName && inputName.length > 0) { 15 | // get matches with named groups for name, trip and capcode in 1 regex 16 | const matches = inputName.match(nameRegex); 17 | if (matches && matches.groups) { 18 | const groups = matches.groups; 19 | //name 20 | if (groups.name) { 21 | name = groups.name; 22 | } 23 | //tripcode 24 | if (groups.tripcode) { 25 | tripcode = `!!${(await getTripCode(groups.tripcode))}`; 26 | } 27 | //capcode 28 | if (permLevel < 4 && groups.capcode) { 29 | let type = capcodeLevels[permLevel]; 30 | capcode = groups.capcodetext ? groups.capcodetext.trim() : type; 31 | if (type.toLowerCase() === capcode.toLowerCase()) { 32 | capcode = type; 33 | } else { 34 | capcode = `${type} ${capcode}`; 35 | } 36 | capcode = `## ${capcode}`; 37 | } 38 | } 39 | } 40 | 41 | return { name, tripcode, capcode }; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /helpers/posting/sanitizeoptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | before: { 6 | allowedTags: [], 7 | allowedAttributes: {} 8 | }, 9 | 10 | after: { 11 | allowedTags: [ 'span', 'a', 'img', 'small'], 12 | allowedAttributes: { 13 | 'a': [ 'href', 'rel', 'class', 'referrerpolicy', 'target' ], 14 | 'span': [ 'class' ], 15 | 'img': ['src', 'height', 'width'] 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /helpers/posting/tripcode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { tripcodeSecret } = require(__dirname+'/../../configs/main.js') 4 | , { createHash } = require('crypto') 5 | 6 | module.exports = async (password) => { 7 | 8 | const tripcodeHash = createHash('sha256').update(password + tripcodeSecret).digest('base64'); 9 | const tripcode = tripcodeHash.substring(tripcodeHash.length-10); 10 | return tripcode; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /helpers/processip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { ipHashPermLevel } = require(__dirname+'/../configs/main.js') 4 | , { isIP } = require('net') 5 | , hashIp = require(__dirname+'/haship.js'); 6 | 7 | module.exports = (req, res, next) => { 8 | const ip = req.headers['x-real-ip'] || req.connection.remoteAddress; //need to consider forwarded-for, etc here and in nginx 9 | const ipVersion = isIP(ip); 10 | if (ipVersion) { 11 | const delimiter = ipVersion === 4 ? '.' : ':'; 12 | let split = ip.split(delimiter); 13 | const qrange = split.slice(0,Math.floor(split.length*0.75)).join(delimiter); 14 | const hrange = split.slice(0,Math.floor(split.length*0.5)).join(delimiter); 15 | res.locals.ip = { 16 | raw: ipHashPermLevel === -1 ? hashIp(ip) : ip, 17 | single: hashIp(ip), 18 | qrange: hashIp(qrange), 19 | hrange: hashIp(hrange), 20 | } 21 | next(); 22 | } else { 23 | return res.status(400).render('message', { 24 | 'title': 'Bad request', 25 | 'message': 'Malformed IP' //should never get here 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /helpers/referrercheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { refererCheck, allowedHosts } = require(__dirname+'/../configs/main.js') 4 | , dynamicResponse = require(__dirname+'/dynamic.js') 5 | , allowedHostSet = new Set(allowedHosts); 6 | 7 | module.exports = (req, res, next) => { 8 | if (req.method !== 'POST') { 9 | return next(); 10 | } 11 | let validReferer = false; 12 | try { 13 | const url = new URL(req.headers.referer); 14 | validReferer = allowedHostSet.has(url.hostname); 15 | } catch(e) { 16 | //referrer is invalid url 17 | } 18 | if (refererCheck === true && (!req.headers.referer || !validReferer)) { 19 | return dynamicResponse(req, res, 403, 'message', { 20 | 'title': 'Forbidden', 21 | 'message': 'Invalid or missing "Referer" header. Are you posting from the correct URL?' 22 | }); 23 | } 24 | next(); 25 | } 26 | -------------------------------------------------------------------------------- /helpers/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { enableUserBoardCreation, enableUserAccountCreation, 4 | lockWait, globalLimits, boardDefaults, cacheTemplates, 5 | meta, enableWebring } = require(__dirname+'/../configs/main.js') 6 | , { outputFile } = require('fs-extra') 7 | , formatSize = require(__dirname+'/files/formatsize.js') 8 | , pug = require('pug') 9 | , path = require('path') 10 | , commit = require(__dirname+'/commit.js') 11 | , uploadDirectory = require(__dirname+'/files/uploadDirectory.js') 12 | , redlock = require(__dirname+'/../redlock.js') 13 | , templateDirectory = path.join(__dirname+'/../views/pages/') 14 | 15 | module.exports = async (htmlName, templateName, options, json=null) => { 16 | const html = pug.renderFile(`${templateDirectory}${templateName}`, { 17 | ...options, 18 | cache: cacheTemplates, 19 | meta, 20 | commit, 21 | defaultTheme: boardDefaults.theme, 22 | defaultCodeTheme: boardDefaults.codeTheme, 23 | postFilesSize: formatSize(globalLimits.postFilesSize.max), 24 | enableUserAccountCreation, 25 | enableUserBoardCreation, 26 | globalLimits, 27 | enableWebring, 28 | }); 29 | const lock = await redlock.lock(`locks:${htmlName}`, lockWait); 30 | const htmlPromise = outputFile(`${uploadDirectory}/html/${htmlName}`, html); 31 | let jsonPromise; 32 | if (json !== null) { 33 | jsonPromise = outputFile(`${uploadDirectory}/json/${json.name}`, JSON.stringify(json.data)); 34 | } 35 | await Promise.all([htmlPromise, jsonPromise]); 36 | await lock.unlock(); 37 | return html; 38 | }; 39 | -------------------------------------------------------------------------------- /helpers/sessionrefresh.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Accounts } = require(__dirname+'/../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | if (req.session && req.session.authenticated === true) { 7 | // keeping session updated incase user updated on global manage 8 | const account = await Accounts.findOne(req.session.user.username); 9 | if (!account) { 10 | req.session.destroy(); 11 | } else { 12 | req.session.user = { 13 | 'username': account._id, 14 | 'authLevel': account.authLevel, 15 | 'modBoards': account.modBoards, 16 | 'ownedBoards': account.ownedBoards, 17 | }; 18 | } 19 | } 20 | next(); 21 | } 22 | -------------------------------------------------------------------------------- /helpers/setminimal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res, next) => { 4 | res.locals.minimal = true; 5 | next(); 6 | } 7 | -------------------------------------------------------------------------------- /helpers/themes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { readdirSync } = require('fs') 4 | , { themes, codeThemes } = require(__dirname+'/../configs/main.js'); 5 | 6 | module.exports = { 7 | 8 | themes: themes.length > 0 ? themes : readdirSync(__dirname+'/../gulp/res/css/themes/').map(x => x.substring(0,x.length-4)), 9 | 10 | codeThemes: codeThemes.length > 0 ? codeThemes : readdirSync(__dirname+'/../node_modules/highlight.js/styles/').map(x => x.substring(0,x.length-4)), 11 | 12 | } 13 | -------------------------------------------------------------------------------- /helpers/timediffstring.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (label, end) => { 4 | return (`${end[0] > 0 ? end[0]+'s' : ''}${Math.trunc(end[1]/1000000)}ms `).padStart(9) + label; 5 | } 6 | -------------------------------------------------------------------------------- /migrations/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | '0.0.1': require(__dirname+'/migration-0.0.1.js'), //add bypasses to database 5 | '0.0.2': require(__dirname+'/migration-0.0.2.js'), //rename ip field in posts 6 | '0.0.3': require(__dirname+'/migration-0.0.3.js'), //move files from /img to /file/ 7 | '0.0.4': require(__dirname+'/migration-0.0.4.js'), //rename some fields for board lock mode and unlisting 8 | '0.0.5': require(__dirname+'/migration-0.0.5.js'), //add bumplimit to board settings 9 | '0.0.6': require(__dirname+'/migration-0.0.6.js'), //add blocked countries to board settings 10 | '0.0.7': require(__dirname+'/migration-0.0.7.js'), //sage only email without force anon for some reason 11 | '0.0.8': require(__dirname+'/migration-0.0.8.js'), //option to auto reset triggers after hour is over 12 | '0.0.9': require(__dirname+'/migration-0.0.9.js'), //ip changes 13 | } 14 | -------------------------------------------------------------------------------- /migrations/migration-0.0.1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Creating bypass collection'); 5 | await db.createCollection('bypass'); 6 | console.log('Creating bypass collection index'); 7 | await db.collection('bypass').createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }); 8 | }; 9 | -------------------------------------------------------------------------------- /migrations/migration-0.0.2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Renaming IP fields on posts'); 5 | await db.collection('posts').updateMany({}, { 6 | '$rename': { 7 | 'ip.hash': 'ip.single' 8 | } 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /migrations/migration-0.0.3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Moving user uploads from /img/ to /file/'); 5 | require('fs').renameSync(__dirname+'/../static/img/', __dirname+'/../static/file/') 6 | }; 7 | -------------------------------------------------------------------------------- /migrations/migration-0.0.4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Renaming some settings fields on boards'); 5 | await db.collection('boards').updateMany({}, { 6 | '$rename': { 7 | 'settings.locked': 'settings.lockMode', 8 | 'settings.unlisted': 'settings.unlistedLocal', 9 | 'settings.webring': 'settings.unlistedWebring' 10 | } 11 | }); 12 | console.log('upadting renamed fields to proper values') 13 | await db.collection('boards').updateMany({ 14 | 'settings.lockMode': true, 15 | }, { 16 | '$set': { 17 | 'settings.lockMode': 2, 18 | } 19 | }); 20 | await db.collection('boards').updateMany({ 21 | 'settings.lockMode': false, 22 | }, { 23 | '$set': { 24 | 'settings.lockMode': 0, 25 | } 26 | }); 27 | await db.collection('boards').updateMany({ 28 | 'settings.triggerAction': 3, 29 | }, { 30 | '$set': { 31 | 'settings.triggerAction': 4, 32 | } 33 | }); 34 | console.log('clearing boards cache'); 35 | await redis.deletePattern('board:*') 36 | }; 37 | -------------------------------------------------------------------------------- /migrations/migration-0.0.5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { globalLimits } = require(__dirname+'/../configs/main.js'); 4 | 5 | module.exports = async(db, redis) => { 6 | console.log('Adding bumplimit field to boards on posts'); 7 | await db.collection('boards').updateMany({}, { 8 | '$set': { 9 | 'settings.bumpLimit': globalLimits.bumpLimit.max, 10 | } 11 | }); 12 | console.log('Cleared boards cache'); 13 | await redis.deletePattern('board:*'); 14 | console.log('Cleared globalsettings cache'); 15 | await redis.deletePattern('globalsettings'); 16 | }; 17 | -------------------------------------------------------------------------------- /migrations/migration-0.0.6.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding blockedCountries field to boards'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'settings.blockedCountries': [], 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /migrations/migration-0.0.7.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('add sageOnlyEmail option to boards'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'settings.sageOnlyEmail': false, 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /migrations/migration-0.0.8.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('add resetTrigger option to boards'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'settings.resetTrigger': false, 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /migrations/migration-0.0.9.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const hashIp = require(__dirname+'/../helpers/haship.js'); 4 | 5 | module.exports = async(db, redis) => { 6 | console.log('change bans index'); 7 | await db.collection('bans').dropIndex("ip_1_board_1"); 8 | await db.collection('bans').createIndex({ 'ip.single': 1 , 'board': 1 }); 9 | console.log('adjusting ip on posts and clearing reports'); 10 | const promises = [] 11 | await db.collection('posts').find().forEach(doc => { 12 | promises.push(db.collection('posts').updateOne({ 13 | '_id':doc._id 14 | }, { 15 | '$set':{ 16 | 'ip.raw': doc.ip.single, 17 | 'ip.single': hashIp(doc.ip.single), 18 | 'ip.qrange': hashIp(doc.ip.qrange), 19 | 'ip.hrange': hashIp(doc.ip.hrange), 20 | 'reports': [], //easier than fixing reports 21 | 'globalreports': [], //easier than fixing reports 22 | } 23 | })) 24 | }); 25 | console.log('adjusting ip in modlogs') 26 | await db.collection('modlog').find().forEach(doc => { 27 | promises.push(db.collection('modlog').updateOne({ 28 | '_id':doc._id 29 | }, { 30 | '$set':{ 31 | 'ip': { 32 | 'raw': doc.ip, 33 | 'single': hashIp(doc.ip) 34 | } 35 | } 36 | })) 37 | }); 38 | console.log('adjust ip in bans, set null type and remove saved posts') 39 | await db.collection('bans').find().forEach(doc => { 40 | promises.push(db.collection('bans').updateOne({ 41 | '_id':doc._id 42 | }, { 43 | '$set':{ 44 | 'ip': { 45 | 'raw': doc.ip, 46 | 'single': hashIp(doc.ip) 47 | }, 48 | 'type': null, 49 | 'posts': null //easier than fixing all saved posts 50 | } 51 | })) 52 | }); 53 | await Promise.all(promises); 54 | console.log('Cleared boards cache'); 55 | await redis.deletePattern('board:*'); 56 | }; 57 | -------------------------------------------------------------------------------- /models/forms/addnews.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { News } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js') 6 | , buildQueue = require(__dirname+'/../../queue.js') 7 | , messageHandler = require(__dirname+'/../../helpers/posting/message.js'); 8 | 9 | module.exports = async (req, res, next) => { 10 | 11 | const { message: markdownNews } = await messageHandler(req.body.message, null, null); 12 | 13 | const post = { 14 | 'title': req.body.title, 15 | 'message': { 16 | 'raw': req.body.message, 17 | 'markdown': markdownNews 18 | }, 19 | 'date': new Date(), 20 | }; 21 | 22 | await News.insertOne(post); 23 | 24 | buildQueue.push({ 25 | 'task': 'buildNews', 26 | 'options': {} 27 | }); 28 | 29 | return dynamicResponse(req, res, 200, 'message', { 30 | 'title': 'Success', 31 | 'message': 'Added newspost', 32 | 'redirect': '/globalmanage/news.html' 33 | }); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /models/forms/appeal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | return Bans.appeal(res.locals.ip.single, req.body.checkedbans, req.body.message).then(r => r.modifiedCount); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /models/forms/blockbypass.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bypass } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , { secureCookies, blockBypass } = require(__dirname+'/../../configs/main.js') 6 | , production = process.env.NODE_ENV === 'production'; 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | const bypass = await Bypass.getBypass(); 11 | const bypassId = bypass.insertedId; 12 | res.locals.blockBypass = bypass.ops[0]; 13 | 14 | return res 15 | .cookie('bypassid', bypassId.toString(), { 16 | 'maxAge': blockBypass.expireAfterTime, 17 | 'secure': production && secureCookies, 18 | 'sameSite': 'strict' 19 | }) 20 | .render('message', { 21 | 'minimal': req.body.minimal, //todo: make use x- header for ajax once implm. 22 | 'title': 'Success', 23 | 'message': 'Completed block bypass, you may go back and make your post.', 24 | }); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /models/forms/bumplockposts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { NumberInt } = require(__dirname+'/../../db/db.js') 4 | 5 | module.exports = (posts) => { 6 | 7 | const filteredposts = posts.filter(post => { 8 | return !post.thread 9 | }) 10 | 11 | if (filteredposts.length === 0) { 12 | return { 13 | message: 'No thread(s) to bumplock', 14 | }; 15 | } 16 | 17 | return { 18 | message: `Toggled bumplock for ${filteredposts.length} thread(s)`, 19 | action: '$bit', 20 | query: { 21 | 'bumplocked': { 22 | 'xor': NumberInt(1) 23 | }, 24 | } 25 | }; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /models/forms/changepassword.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bcrypt = require('bcrypt') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , { Accounts } = require(__dirname+'/../../db/'); 6 | 7 | module.exports = async (req, res, next) => { 8 | 9 | const username = req.body.username.toLowerCase(); 10 | const password = req.body.password; 11 | const newPassword = req.body.newpassword; 12 | 13 | //fetch an account 14 | const account = await Accounts.findOne(username); 15 | 16 | //if the account doesnt exist, reject 17 | if (!account) { 18 | return dynamicResponse(req, res, 403, 'message', { 19 | 'title': 'Forbidden', 20 | 'message': 'Incorrect username or password', 21 | 'redirect': '/changepassword.html' 22 | }); 23 | } 24 | 25 | // bcrypt compare input to saved hash 26 | const passwordMatch = await bcrypt.compare(password, account.passwordHash); 27 | 28 | //if hashes matched 29 | if (passwordMatch === false) { 30 | return dynamicResponse(req, res, 403, 'message', { 31 | 'title': 'Forbidden', 32 | 'message': 'Incorrect username or password', 33 | 'redirect': '/changepassword.html' 34 | }); 35 | } 36 | 37 | //change the password 38 | await Accounts.changePassword(username, newPassword); 39 | 40 | return dynamicResponse(req, res, 200, 'message', { 41 | 'title': 'Success', 42 | 'message': 'Changed password', 43 | 'redirect': '/login.html' 44 | }); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /models/forms/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Boards, Accounts } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js') 6 | , restrictedURIs = new Set(['captcha', 'forms', 'randombanner']) 7 | , { ensureDir } = require('fs-extra') 8 | , { boardDefaults } = require(__dirname+'/../../configs/main.js'); 9 | 10 | module.exports = async (req, res, next) => { 11 | 12 | const { name, description } = req.body 13 | , uri = req.body.uri.toLowerCase() 14 | , tags = req.body.tags.split('\n').filter(n => n) 15 | , owner = req.session.user.username; 16 | 17 | if (restrictedURIs.has(uri)) { 18 | return dynamicResponse(req, res, 400, 'message', { 19 | 'title': 'Bad Request', 20 | 'message': 'That URI is not available for board creation', 21 | 'redirect': '/create.html' 22 | }); 23 | } 24 | 25 | const board = await Boards.findOne(uri); 26 | 27 | // if board exists reject 28 | if (board != null) { 29 | return dynamicResponse(req, res, 409, 'message', { 30 | 'title': 'Conflict', 31 | 'message': 'Board with this URI already exists', 32 | 'redirect': '/create.html' 33 | }); 34 | } 35 | 36 | 37 | //todo: add a settings for defaults 38 | const newBoard = { 39 | '_id': uri, 40 | owner, 41 | 'banners': [], 42 | 'sequence_value': 1, 43 | 'pph': 0, 44 | 'ips': 0, 45 | 'lastPostTimestamp': null, 46 | 'settings': { 47 | name, 48 | description, 49 | tags, 50 | 'moderators': [], 51 | ...boardDefaults 52 | } 53 | } 54 | 55 | await Promise.all([ 56 | Boards.insertOne(newBoard), 57 | Accounts.addOwnedBoard(owner, uri), 58 | ensureDir(`${uploadDirectory}/html/${uri}`), 59 | ensureDir(`${uploadDirectory}/json/${uri}`), 60 | ensureDir(`${uploadDirectory}/banners/${uri}`) 61 | ]); 62 | 63 | return res.redirect(`/${uri}/index.html`); 64 | 65 | } 66 | 67 | -------------------------------------------------------------------------------- /models/forms/cycleposts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { NumberInt } = require(__dirname+'/../../db/db.js') 4 | 5 | module.exports = (posts) => { 6 | 7 | const filteredposts = posts.filter(post => { 8 | return !post.thread 9 | }) 10 | 11 | if (filteredposts.length === 0) { 12 | return { 13 | message: 'No thread(s) to cycle', 14 | }; 15 | } 16 | 17 | return { 18 | message: `Toggled Cyclical mode for ${filteredposts.length} thread(s)`, 19 | action: '$bit', 20 | query: { 21 | 'cyclic': { 22 | 'xor': NumberInt(1) 23 | }, 24 | } 25 | }; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /models/forms/deletebanners.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const { remove } = require('fs-extra') 5 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 6 | , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js') 7 | , { Boards } = require(__dirname+'/../../db/') 8 | , buildQueue = require(__dirname+'/../../queue.js'); 9 | 10 | module.exports = async (req, res, next) => { 11 | 12 | const redirect = `/${req.params.board}/manage/banners.html`; 13 | 14 | //delete file of all selected banners 15 | await Promise.all(req.body.checkedbanners.map(async filename => { 16 | remove(`${uploadDirectory}/banner/${req.params.board}/${filename}`); 17 | })); 18 | 19 | //remove from db 20 | const amount = await Boards.removeBanners(req.params.board, req.body.checkedbanners); 21 | 22 | //update res locals banners in memory 23 | res.locals.board.banners = res.locals.board.banners.filter(banner => { 24 | return !req.body.checkedbanners.includes(banner); 25 | }); 26 | 27 | //rebuild public banners page 28 | buildQueue.push({ 29 | 'task': 'buildBanners', 30 | 'options': { 31 | 'board': res.locals.board, 32 | } 33 | }); 34 | 35 | return dynamicResponse(req, res, 200, 'message', { 36 | 'title': 'Success', 37 | 'message': `Deleted banners.`, 38 | 'redirect': redirect 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /models/forms/deleteboard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Accounts, Boards, Stats, Posts, Bans, Modlogs } = require(__dirname+'/../../db/') 4 | , cache = require(__dirname+'/../../redis.js') 5 | , deletePosts = require(__dirname+'/deletepost.js') 6 | , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js') 7 | , { remove } = require('fs-extra'); 8 | 9 | module.exports = async (uri, board) => { 10 | 11 | //delete board 12 | await Boards.deleteOne(uri); 13 | //get all posts (should probably project to get files for deletin and anything else necessary) 14 | const allPosts = await Posts.allBoardPosts(uri); 15 | if (allPosts.length > 0) { 16 | //delete posts and decrement images 17 | await deletePosts(allPosts, uri, true); 18 | } 19 | await Promise.all([ 20 | Accounts.removeOwnedBoard(board.owner, uri), //remove board from owner account 21 | board.settings.moderators.length > 0 ? Accounts.removeModBoard(board.settings.moderators) : void 0, //remove board from mods accounts 22 | Modlogs.deleteBoard(uri), //modlogs for the board 23 | Bans.deleteBoard(uri), //bans for the board 24 | Stats.deleteBoard(uri), //stats for the board 25 | remove(`${uploadDirectory}/html/${uri}/`), //html 26 | remove(`${uploadDirectory}/json/${uri}/`), //json 27 | remove(`${uploadDirectory}/banners/${uri}/`) //banners 28 | ]); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /models/forms/deletenews.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { News } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , buildQueue = require(__dirname+'/../../queue.js') 6 | 7 | module.exports = async (req, res, next) => { 8 | 9 | await News.deleteMany(req.body.checkednews); 10 | 11 | buildQueue.push({ 12 | 'task': 'buildNews', 13 | 'options': {} 14 | }); 15 | 16 | return dynamicResponse(req, res, 200, 'message', { 17 | 'title': 'Success', 18 | 'message': 'Deleted news', 19 | 'redirect': '/globalmanage/news.html' 20 | }); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /models/forms/deletepostsfiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Files } = require(__dirname+'/../../db/') 4 | , { pruneImmediately } = require(__dirname+'/../../configs/main.js') 5 | , pruneFiles = require(__dirname+'/../../schedules/prune.js') 6 | , deletePostFiles = require(__dirname+'/../../helpers/files/deletepostfiles.js'); 7 | 8 | module.exports = async (posts, unlinkOnly) => { 9 | 10 | //get filenames from all the posts 11 | let files = []; 12 | for (let i = 0; i < posts.length; i++) { 13 | const post = posts[i]; 14 | if (post.files.length > 0) { 15 | files = files.concat(post.files.map(file => { 16 | return { 17 | filename: file.filename, 18 | hash: file.hash, 19 | thumbextension: file.thumbextension 20 | }; 21 | })); 22 | } 23 | } 24 | files = [...new Set(files)]; 25 | 26 | if (files.length == 0) { 27 | return { 28 | message: 'No files found' 29 | }; 30 | } 31 | 32 | if (files.length > 0) { 33 | const fileNames = files.map(x => x.filename); 34 | await Files.decrement(fileNames); 35 | if (pruneImmediately) { 36 | await pruneFiles(fileNames); 37 | } 38 | } 39 | 40 | if (unlinkOnly) { 41 | return { 42 | message:`Unlinked ${files.length} file(s) across ${posts.length} post(s)`, 43 | action:'$set', 44 | query: { 45 | 'files': [] 46 | } 47 | }; 48 | } else { 49 | //delete all the files 50 | await deletePostFiles(files); 51 | return { 52 | message:`Deleted ${files.length} file(s) from server`, 53 | //NOTE: only deletes from selected posts. other posts with same image will 404 54 | action:'$set', 55 | query: { 56 | 'files': [] 57 | } 58 | }; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /models/forms/denybanappeals.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | return Bans.denyAppeal(req.params.board, req.body.checkedbans).then(result => result.modifiedCount); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /models/forms/dismissglobalreport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (posts) => { 4 | 5 | const filteredposts = posts.filter(post => { 6 | return post.globalreports.length > 0 7 | }) 8 | 9 | if (filteredposts.length === 0) { 10 | return { 11 | message: 'No global report(s) to dismiss' 12 | } 13 | } 14 | 15 | return { 16 | message: 'Dismissed global report(s)', 17 | action: '$set', 18 | query: { 19 | 'globalreports': [] 20 | } 21 | }; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /models/forms/dismissreport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res) => { 4 | 5 | const filteredposts = res.locals.posts.filter(post => { 6 | return (req.body.global_dismiss && post.globalreports.length > 0) 7 | || (req.body.dismiss && post.reports.length > 0) 8 | }); 9 | 10 | if (filteredposts.length === 0) { 11 | return { 12 | message: 'No report(s) to dismiss' 13 | } 14 | } 15 | 16 | const ret = { 17 | message: 'Dismissed reports', 18 | action: '$set', 19 | query: {} 20 | }; 21 | ret.query[`${req.body.global_dismiss ? 'global' : ''}reports`] = []; 22 | 23 | return ret; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /models/forms/lockposts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { NumberInt } = require(__dirname+'/../../db/db.js') 4 | 5 | module.exports = (posts) => { 6 | 7 | const filteredposts = posts.filter(post => { 8 | return !post.thread 9 | }) 10 | 11 | if (filteredposts.length === 0) { 12 | return { 13 | message: 'No thread(s) to lock', 14 | }; 15 | } 16 | 17 | return { 18 | message: `Toggled Lock for ${filteredposts.length} thread(s)`, 19 | action: '$bit', 20 | query: { 21 | 'locked': { 22 | 'xor': NumberInt(1) 23 | }, 24 | } 25 | }; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /models/forms/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bcrypt = require('bcrypt') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') 5 | , { Accounts } = require(__dirname+'/../../db/'); 6 | 7 | module.exports = async (req, res, next) => { 8 | 9 | const username = req.body.username.toLowerCase(); 10 | const password = req.body.password; 11 | const goto = req.body.goto || '/account.html'; 12 | const failRedirect = `/login.html${goto ? '?goto='+goto : ''}` 13 | 14 | //fetch an account 15 | const account = await Accounts.findOne(username); 16 | 17 | //if the account doesnt exist, reject 18 | if (!account) { 19 | return dynamicResponse(req, res, 403, 'message', { 20 | 'title': 'Forbidden', 21 | 'message': 'Incorrect username or password', 22 | 'redirect': failRedirect 23 | }); 24 | } 25 | 26 | // bcrypt compare input to saved hash 27 | const passwordMatch = await bcrypt.compare(password, account.passwordHash); 28 | 29 | //if hashes matched 30 | if (passwordMatch === true) { 31 | 32 | // add the account to the session and authenticate if password was correct 33 | req.session.user = { 34 | 'username': account._id, 35 | 'authLevel': account.authLevel, 36 | 'ownedBoards': account.ownedBoards, 37 | 'modBoards': account.modBoards, 38 | }; 39 | req.session.authenticated = true; 40 | 41 | //successful login 42 | return res.redirect(goto); 43 | 44 | } 45 | 46 | return dynamicResponse(req, res, 403, 'message', { 47 | 'title': 'Forbidden', 48 | 'message': 'Incorrect username or password', 49 | 'redirect': failRedirect 50 | }); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /models/forms/logout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res, next) => { 4 | 5 | //remove session 6 | req.session.destroy(); 7 | return res.redirect('/'); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /models/forms/newcaptcha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res, next) => { 4 | 5 | res.clearCookie('captchaid'); 6 | return res.redirect('/captcha.html'); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /models/forms/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Accounts } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const original = req.body.username; //stored but not used yet 9 | const username = original.toLowerCase(); //lowercase to prevent duplicates with mixed case 10 | const password = req.body.password; 11 | 12 | const account = await Accounts.findOne(username); 13 | 14 | // if the account exists reject 15 | if (account != null) { 16 | return dynamicResponse(req, res, 409, 'message', { 17 | 'title': 'Conflict', 18 | 'message': 'Account with this username already exists', 19 | 'redirect': '/register.html' 20 | }); 21 | } 22 | 23 | // add account to db. password is hashed in db model func for easier tests 24 | await Accounts.insertOne(original, username, password, 4); 25 | 26 | return res.redirect('/login.html'); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /models/forms/removebans.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | return Bans.removeMany(req.params.board, req.body.checkedbans).then(result => result.deletedCount); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /models/forms/reportpost.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { ObjectId } = require(__dirname+'/../../db/db.js'); 4 | 5 | module.exports = (req, res) => { 6 | 7 | const report = { 8 | 'id': ObjectId(), 9 | 'reason': req.body.report_reason, 10 | 'date': new Date(), 11 | 'ip': { 12 | 'single': res.locals.ip.single, 13 | 'raw': res.locals.ip.raw 14 | } 15 | } 16 | 17 | const ret = { 18 | message: `Reported ${res.locals.posts.length} post(s)`, 19 | action: '$push', 20 | query: {} 21 | }; 22 | const query = { 23 | '$each': [report], 24 | '$slice': -5 //limit number of reports 25 | } 26 | if (req.body.global_report) { 27 | ret.query['globalreports'] = query; 28 | } 29 | if (req.body.report) { 30 | ret.query['reports'] = query; 31 | } 32 | 33 | return ret; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /models/forms/spoilerpost.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (posts) => { 4 | 5 | // filter to ones not spoilered 6 | const filteredPosts = posts.filter(post => { 7 | return !post.spoiler && post.files.length > 0; 8 | }); 9 | 10 | if (filteredPosts.length === 0) { 11 | return { 12 | message:'No post(s) to spoiler' 13 | }; 14 | } 15 | 16 | return { 17 | message: `Spoilered ${filteredPosts.length} post(s)`, 18 | action: '$set', 19 | query: { 20 | 'spoiler': true 21 | } 22 | }; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /models/forms/stickyposts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { NumberInt } = require(__dirname+'/../../db/db.js') 4 | 5 | module.exports = (posts) => { 6 | 7 | const filteredposts = posts.filter(post => { 8 | return !post.thread 9 | }) 10 | 11 | if (filteredposts.length === 0) { 12 | return { 13 | message: 'No thread(s) to sticky', 14 | }; 15 | } 16 | 17 | return { 18 | message: `Toggled sticky for ${filteredposts.length} thread(s)`, 19 | action: '$bit', 20 | query: { 21 | 'sticky': { 22 | 'xor': NumberInt(1) 23 | }, 24 | } 25 | }; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /models/forms/transferboard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Boards, Accounts } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const newOwner = await Accounts.findOne(req.body.username.toLowerCase()); 9 | 10 | if (!newOwner) { 11 | return dynamicResponse(req, res, 400, 'message', { 12 | 'title': 'Bad request', 13 | 'message': 'Cannot transfer to account that does not exist', 14 | 'redirect': `/${req.params.board}/manage/settings.html` 15 | }); 16 | } 17 | 18 | //modify accounts with new board ownership 19 | await Accounts.removeOwnedBoard(res.locals.board.owner, req.params.board) 20 | await Accounts.addOwnedBoard(newOwner._id, req.params.board); 21 | 22 | //set owner in memory and in db 23 | res.locals.board.owner = newOwner._id; 24 | await Boards.setOwner(req.params.board, res.locals.board.owner); 25 | 26 | return dynamicResponse(req, res, 200, 'message', { 27 | 'title': 'Success', 28 | 'message': 'Transferred ownership', 29 | 'redirect': `/${req.params.board}/index.html` 30 | }); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /models/pages/account.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res, next) => { 4 | 5 | res 6 | .set('Cache-Control', 'private, max-age=5') 7 | .render('account', { 8 | user: req.session.user, 9 | }); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /models/pages/banners.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildBanners } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildBanners({ board: res.locals.board }); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.send(html); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/pages/blockbypass.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildBypass } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildBypass(res.locals.minimal); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.send(html); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/pages/board.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../db/posts.js') 4 | , { buildBoard } = require(__dirname+'/../../helpers/tasks.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const page = req.params.page === 'index' ? 1 : Number(req.params.page); 9 | let html; 10 | try { 11 | const maxPage = Math.min(Math.ceil((await Posts.getPages(req.params.board)) / 10), Math.ceil(res.locals.board.settings.threadLimit/10)) || 1; 12 | if (page > maxPage) { 13 | return next(); 14 | } 15 | html = await buildBoard({ 16 | board: res.locals.board, 17 | page, 18 | maxPage 19 | }); 20 | } catch (err) { 21 | return next(err); 22 | } 23 | 24 | return res.send(html); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /models/pages/captcha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Ratelimits } = require(__dirname+'/../../db/') 4 | , generateCaptcha = require(__dirname+'/../../helpers/captcha/captchagenerate.js') 5 | , { secureCookies, rateLimitCost } = require(__dirname+'/../../configs/main.js') 6 | , production = process.env.NODE_ENV === 'production'; 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | if (!production && req.cookies['captchaid'] != null) { 11 | return res.redirect(`/captcha/${req.cookies['captchaid']}.jpg`); 12 | } 13 | 14 | let captchaId; 15 | try { 16 | const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.single, 'captcha', rateLimitCost.captcha); 17 | if (ratelimit > 100) { 18 | return res.status(429).redirect('/file/ratelimit.png'); 19 | } 20 | const { id, text } = await generateCaptcha(); 21 | captchaId = id; 22 | } catch (err) { 23 | return next(err); 24 | } 25 | 26 | return res 27 | .cookie('captchaid', captchaId.toString(), { 28 | 'maxAge': 5*60*1000, //5 minute cookie 29 | 'secure': production && secureCookies, 30 | 'sameSite': 'strict' 31 | }) 32 | .redirect(`/captcha/${captchaId}.jpg`); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /models/pages/captchapage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildCaptcha } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildCaptcha(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.send(html); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/pages/catalog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildCatalog } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildCatalog({ board: res.locals.board }); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.send(html); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/pages/changepassword.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildChangePassword } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildChangePassword(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.send(html); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/pages/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildCreate } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | return res.render('create'); 8 | 9 | /* 10 | let html; 11 | try { 12 | html = await buildCreate(); 13 | } catch (err) { 14 | return next(err); 15 | } 16 | 17 | return res.send(html); 18 | */ 19 | 20 | } 21 | -------------------------------------------------------------------------------- /models/pages/globalmanage/accounts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Accounts } = require(__dirname+'/../../../db/') 4 | , pageQueryConverter = require(__dirname+'/../../../helpers/pagequeryconverter.js') 5 | , limit = 20; 6 | 7 | module.exports = async (req, res, next) => { 8 | 9 | const { page, offset, queryString } = pageQueryConverter(req.query, limit); 10 | 11 | let filter = {}; 12 | const username = (typeof req.query.username === 'string' ? req.query.username : null); 13 | if (username) { 14 | filter['_id'] = username; 15 | } 16 | const uri = (typeof req.query.uri === 'string' ? req.queru.uri : null); 17 | if (uri) { 18 | filter['$or'] = [ 19 | { 20 | 'ownedBoards': uri 21 | }, 22 | { 23 | 'modBoards': uri 24 | }, 25 | ]; 26 | } 27 | 28 | let accounts, maxPage; 29 | try { 30 | [accounts, maxPage] = await Promise.all([ 31 | Accounts.find(filter, offset, limit), 32 | Accounts.count(filter), 33 | ]); 34 | maxPage = Math.ceil(maxPage/limit); 35 | } catch (err) { 36 | return next(err) 37 | } 38 | 39 | res 40 | .set('Cache-Control', 'private, max-age=5') 41 | .render('globalmanageaccounts', { 42 | csrf: req.csrfToken(), 43 | queryString, 44 | username, 45 | uri, 46 | accounts, 47 | page, 48 | maxPage, 49 | }); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /models/pages/globalmanage/bans.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let bans; 8 | try { 9 | bans = await Bans.getGlobalBans(); 10 | } catch (err) { 11 | return next(err) 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('globalmanagebans', { 17 | csrf: req.csrfToken(), 18 | bans, 19 | }); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /models/pages/globalmanage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | globalManageReports: require(__dirname+'/reports.js'), 5 | globalManageBans: require(__dirname+'/bans.js'), 6 | globalManageLogs: require(__dirname+'/logs.js'), 7 | globalManageRecent: require(__dirname+'/recent.js'), 8 | globalManageNews: require(__dirname+'/news.js'), 9 | globalManageAccounts: require(__dirname+'/accounts.js'), 10 | globalManageSettings: require(__dirname+'/settings.js'), 11 | } 12 | -------------------------------------------------------------------------------- /models/pages/globalmanage/logs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Modlogs } = require(__dirname+'/../../../db/') 4 | , pageQueryConverter = require(__dirname+'/../../../helpers/pagequeryconverter.js') 5 | , decodeQueryIP = require(__dirname+'/../../../helpers/decodequeryip.js') 6 | , limit = 50; 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | const { page, offset, queryString } = pageQueryConverter(req.query, limit); 11 | 12 | let filter = {}; 13 | const username = (typeof req.query.username === 'string' ? req.query.username : null); 14 | if (username && !Array.isArray(username)) { 15 | filter.user = username; 16 | } 17 | const uri = (typeof req.query.uri === 'string' ? req.query.uri : null); 18 | if (uri && !Array.isArray(uri)) { 19 | filter.board = uri; 20 | } 21 | const ipMatch = decodeQueryIP(req.query, res.locals.permLevel); 22 | if (ipMatch instanceof RegExp) { 23 | filter['ip.single'] = ipMatch; 24 | } else if (typeof ipMatch === 'string') { 25 | filter['ip.raw'] = ipMatch; 26 | } 27 | 28 | let logs, maxPage; 29 | try { 30 | [logs, maxPage] = await Promise.all([ 31 | Modlogs.find(filter, offset, limit), 32 | Modlogs.count(filter), 33 | ]); 34 | maxPage = Math.ceil(maxPage/limit); 35 | } catch (err) { 36 | return next(err) 37 | } 38 | 39 | res 40 | .set('Cache-Control', 'private, max-age=5') 41 | .render('globalmanagelogs', { 42 | csrf: req.csrfToken(), 43 | queryString, 44 | username, 45 | uri, 46 | ip: ipMatch ? req.query.ip : null, 47 | logs, 48 | page, 49 | maxPage, 50 | }); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /models/pages/globalmanage/news.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { News } = require(__dirname+'/../../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let news; 8 | try { 9 | news = await News.find(); 10 | } catch (err) { 11 | return next(err) 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('globalmanagenews', { 17 | csrf: req.csrfToken(), 18 | news, 19 | }); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /models/pages/globalmanage/recent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Posts } = require(__dirname+'/../../../db/') 4 | , pageQueryConverter = require(__dirname+'/../../../helpers/pagequeryconverter.js') 5 | , decodeQueryIP = require(__dirname+'/../../../helpers/decodequeryip.js') 6 | , limit = 20; 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | const { page, offset, queryString } = pageQueryConverter(req.query, limit); 11 | let ipMatch = decodeQueryIP(req.query, res.locals.permLevel); 12 | 13 | let posts; 14 | try { 15 | posts = await Posts.getGlobalRecent(offset, limit, ipMatch); 16 | } catch (err) { 17 | return next(err) 18 | } 19 | 20 | res 21 | .set('Cache-Control', 'private, max-age=5') 22 | .render('globalmanagerecent', { 23 | csrf: req.csrfToken(), 24 | posts, 25 | page, 26 | ip: ipMatch ? req.query.ip : null, 27 | queryString, 28 | }); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /models/pages/globalmanage/reports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Posts } = require(__dirname+'/../../../db/') 4 | , pageQueryConverter = require(__dirname+'/../../../helpers/pagequeryconverter.js') 5 | , decodeQueryIP = require(__dirname+'/../../../helpers/decodequeryip.js') 6 | , limit = 20; 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | const { page, offset, queryString } = pageQueryConverter(req.query, limit); 11 | let ipMatch = decodeQueryIP(req.query, res.locals.permLevel); 12 | 13 | let reports; 14 | try { 15 | reports = await Posts.getGlobalReports(offset, limit, ipMatch); 16 | } catch (err) { 17 | return next(err) 18 | } 19 | 20 | res 21 | .set('Cache-Control', 'private, max-age=5') 22 | .render('globalmanagereports', { 23 | csrf: req.csrfToken(), 24 | reports, 25 | page, 26 | ip: ipMatch ? req.query.ip : null, 27 | queryString, 28 | }); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /models/pages/globalmanage/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cache = require(__dirname+'/../../../redis.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let settings = await cache.get('globalsettings'); 8 | if (!settings) { 9 | settings = { 10 | captchaMode: 0, 11 | filters: [], 12 | filterMode: 0, 13 | filterBanDuration: 0, 14 | } 15 | cache.set('globalsettings', settings); 16 | } 17 | 18 | res 19 | .set('Cache-Control', 'private, max-age=5') 20 | .render('globalmanagesettings', { 21 | csrf: req.csrfToken(), 22 | settings, 23 | }); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /models/pages/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildHomepage } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildHomepage(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.send(html); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/pages/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | changePassword: require(__dirname+'/changepassword.js'), 5 | blockBypass: require(__dirname+'/blockbypass.js'), 6 | register: require(__dirname+'/register.js'), 7 | account: require(__dirname+'/account.js'), 8 | home: require(__dirname+'/home.js'), 9 | login: require(__dirname+'/login.js'), 10 | create: require(__dirname+'/create.js'), 11 | board: require(__dirname+'/board.js'), 12 | catalog: require(__dirname+'/catalog.js'), 13 | banners: require(__dirname+'/banners.js'), 14 | randombanner: require(__dirname+'/randombanner.js'), 15 | news: require(__dirname+'/news.js'), 16 | captchaPage: require(__dirname+'/captchapage.js'), 17 | captcha: require(__dirname+'/captcha.js'), 18 | thread: require(__dirname+'/thread.js'), 19 | modlog: require(__dirname+'/modlog.js'), 20 | modloglist: require(__dirname+'/modloglist.js'), 21 | boardlist: require(__dirname+'/boardlist.js'), 22 | } 23 | -------------------------------------------------------------------------------- /models/pages/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildLogin } = require(__dirname+'/../../helpers/tasks.js') 4 | , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | res.render('login', { 9 | 'goto': (typeof req.query.goto === 'string' ? req.query.goto : null) 10 | }); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /models/pages/manage/banners.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res, next) => { 4 | 5 | res 6 | .set('Cache-Control', 'private, max-age=5') 7 | .render('managebanners', { 8 | csrf: req.csrfToken(), 9 | }); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /models/pages/manage/bans.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Bans = require(__dirname+'/../../../db/bans.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let bans; 8 | try { 9 | bans = await Bans.getBoardBans(req.params.board); 10 | } catch (err) { 11 | return next(err) 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('managebans', { 17 | csrf: req.csrfToken(), 18 | bans, 19 | }); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /models/pages/manage/board.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../../db/posts.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | const page = req.params.page === 'index' ? 1 : Number(req.params.page); 8 | let maxPage; 9 | let threads; 10 | try { 11 | maxPage = Math.min(Math.ceil((await Posts.getPages(req.params.board)) / 10), Math.ceil(res.locals.board.settings.threadLimit/10)) || 1; 12 | if (page > maxPage) { 13 | return next(); 14 | } 15 | threads = await Posts.getRecent(req.params.board, page, 10, true); 16 | } catch (err) { 17 | return next(err); 18 | } 19 | 20 | res 21 | .set('Cache-Control', 'private, max-age=5') 22 | .render('board', { 23 | modview: true, 24 | page, 25 | maxPage, 26 | threads, 27 | board: res.locals.board, 28 | csrf: req.csrfToken(), 29 | }); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /models/pages/manage/catalog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../../db/posts.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let threads; 8 | try { 9 | threads = await Posts.getCatalog(req.params.board); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('catalog', { 17 | modview: true, 18 | threads, 19 | board: res.locals.board, 20 | csrf: req.csrfToken(), 21 | }); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /models/pages/manage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | manageReports: require(__dirname+'/reports.js'), 5 | manageRecent: require(__dirname+'/recent.js'), 6 | manageSettings: require(__dirname+'/settings.js'), 7 | manageBans: require(__dirname+'/bans.js'), 8 | manageLogs: require(__dirname+'/logs.js'), 9 | manageBanners: require(__dirname+'/banners.js'), 10 | manageBoard: require(__dirname+'/board.js'), 11 | manageCatalog: require(__dirname+'/catalog.js'), 12 | manageThread: require(__dirname+'/thread.js'), 13 | } 14 | -------------------------------------------------------------------------------- /models/pages/manage/logs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Modlogs } = require(__dirname+'/../../../db/') 4 | , pageQueryConverter = require(__dirname+'/../../../helpers/pagequeryconverter.js') 5 | // , decodeQueryIP = require(__dirname+'/../../../helpers/decodequeryip.js') 6 | , limit = 50; 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | const { page, offset, queryString } = pageQueryConverter(req.query, limit); 11 | 12 | let filter = { 13 | board: req.params.board 14 | }; 15 | const username = typeof req.query.username === 'string' ? req.query.username : null; 16 | if (username) { 17 | filter.user = username; 18 | } 19 | const uri = typeof req.query.uri === 'string' ? req.query.uri : null; 20 | if (uri) { 21 | filter.board = uri; 22 | } 23 | //todo fetch log entry by id and then get ip and hash 24 | 25 | let logs, maxPage; 26 | try { 27 | [logs, maxPage] = await Promise.all([ 28 | Modlogs.find(filter, offset, limit), 29 | Modlogs.count(filter), 30 | ]); 31 | maxPage = Math.ceil(maxPage/limit); 32 | } catch (err) { 33 | return next(err) 34 | } 35 | 36 | res 37 | .set('Cache-Control', 'private, max-age=5') 38 | .render('managelogs', { 39 | csrf: req.csrfToken(), 40 | queryString, 41 | username, 42 | uri, 43 | //posterid here 44 | logs, 45 | page, 46 | maxPage, 47 | }); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /models/pages/manage/recent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Posts } = require(__dirname+'/../../../db/') 4 | , decodeQueryIP = require(__dirname+'/../../../helpers/decodequeryip.js') 5 | , pageQueryConverter = require(__dirname+'/../../../helpers/pagequeryconverter.js') 6 | , limit = 20; 7 | 8 | module.exports = async (req, res, next) => { 9 | 10 | const { page, offset, queryString } = pageQueryConverter(req.query, limit); 11 | let ip = decodeQueryIP(req.query, res.locals.permLevel); 12 | const postId = typeof req.query.postid === 'string' ? req.query.postid : null; 13 | if (postId && +postId === parseInt(postId) && Number.isSafeInteger(+postId)) { 14 | const fetchedPost = await Posts.getPost(req.params.board, +postId, true); 15 | if (fetchedPost) { 16 | ip = decodeQueryIP({ ip: fetchedPost.ip.single.slice(-10) }, res.locals.permlevel); 17 | } 18 | } 19 | 20 | let posts; 21 | try { 22 | posts = await Posts.getBoardRecent(offset, limit, ip, req.params.board); 23 | } catch (err) { 24 | return next(err) 25 | } 26 | 27 | res 28 | .set('Cache-Control', 'private, max-age=5') 29 | .render('managerecent', { 30 | csrf: req.csrfToken(), 31 | posts, 32 | page, 33 | postId, 34 | queryIp: ip ? req.query.ip : null, 35 | queryString, 36 | }); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /models/pages/manage/reports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../../db/posts.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let reports; 8 | try { 9 | reports = await Posts.getReports(req.params.board); 10 | } catch (err) { 11 | return next(err) 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('managereports', { 17 | csrf: req.csrfToken(), 18 | reports, 19 | }); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /models/pages/manage/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { themes, codeThemes } = require(__dirname+'/../../../helpers/themes.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | res 8 | .set('Cache-Control', 'private, max-age=5') 9 | .render('managesettings', { 10 | csrf: req.csrfToken(), 11 | themes, 12 | codeThemes, 13 | }); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /models/pages/manage/thread.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../../db/posts.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let thread; 8 | try { 9 | thread = await Posts.getThread(res.locals.board._id, res.locals.thread.postId, true); 10 | if (!thread) { 11 | return next(); //deleted between exists 12 | } 13 | } catch (err) { 14 | return next(err); 15 | } 16 | 17 | res 18 | .set('Cache-Control', 'private, max-age=5') 19 | .render('thread', { 20 | modview: true, 21 | upLevel: true, 22 | board: res.locals.board, 23 | thread, 24 | csrf: req.csrfToken(), 25 | }); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /models/pages/modlog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Modlogs } = require(__dirname+'/../../db/') 4 | , { buildModLog } = require(__dirname+'/../../helpers/tasks.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | if (!res.locals.date) { 9 | return next(); 10 | } 11 | 12 | const startDate = res.locals.date.date; 13 | const { year, month, day } = res.locals.date; 14 | const endDate = new Date(Date.UTC(year, month, day, 23, 59, 59, 999)); 15 | 16 | let html; 17 | try { 18 | const logs = await Modlogs.findBetweenDate(res.locals.board, startDate, endDate); 19 | if (!logs || logs.length === 0) { 20 | return next(); 21 | } 22 | html = await buildModLog({ 23 | board: res.locals.board, 24 | startDate, 25 | endDate, 26 | logs 27 | }); 28 | } catch (err) { 29 | return next(err); 30 | } 31 | 32 | return res.send(html); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /models/pages/modloglist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildModLogList } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildModLogList({ board: res.locals.board }); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.send(html); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/pages/news.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildNews } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildNews(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.send(html); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/pages/randombanner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Boards = require(__dirname+'/../../db/boards.js') 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | if (!req.query.board || typeof req.query.board !== 'string') { 8 | return next(); 9 | } 10 | 11 | let banner; 12 | try { 13 | banner = await Boards.randomBanner(req.query.board); 14 | } catch (err) { 15 | return next(err); 16 | } 17 | 18 | if (!banner) { 19 | //non existing boards will show default banner, but it doesnt really matter. 20 | return res.redirect('/file/defaultbanner.png'); 21 | } 22 | 23 | return res.redirect(`/banner/${req.query.board}/${banner}`); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /models/pages/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildRegister } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildRegister(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.send(html); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /models/pages/thread.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildThread } = require(__dirname+'/../../helpers/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildThread({ 10 | threadId: res.locals.thread.postId, 11 | board: res.locals.board 12 | }); 13 | } catch (err) { 14 | return next(err); 15 | } 16 | 17 | return res.send(html); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jschan", 3 | "version": "0.0.1", 4 | "migrateVersion": "0.0.9", 5 | "description": "", 6 | "main": "server.js", 7 | "dependencies": { 8 | "bcrypt": "^5.0.0", 9 | "bull": "^3.12.1", 10 | "cache-pug-templates": "^2.0.1", 11 | "connect-redis": "^4.0.4", 12 | "cookie-parser": "^1.4.5", 13 | "csurf": "^1.11.0", 14 | "del": "^5.1.0", 15 | "dnsbl": "^3.2.0", 16 | "express": "^4.17.1", 17 | "express-fileupload": "github:fatchan/express-fileupload", 18 | "express-session": "^1.17.0", 19 | "fluent-ffmpeg": "^2.1.2", 20 | "fs": "0.0.1-security", 21 | "fs-extra": "^9.0.0", 22 | "gm": "github:fatchan/gm", 23 | "gulp": "^4.0.2", 24 | "gulp-clean-css": "^4.3.0", 25 | "gulp-concat": "^2.6.1", 26 | "gulp-less": "^4.0.1", 27 | "gulp-pug": "^4.0.1", 28 | "gulp-uglify-es": "^2.0.0", 29 | "highlight.js": "^10.0.0", 30 | "ioredis": "^4.14.1", 31 | "mongodb": "^3.5.0", 32 | "node-fetch": "^2.6.0", 33 | "path": "^0.12.7", 34 | "pm2": "^4.3.0", 35 | "pug": "^3.0.0", 36 | "redlock": "^4.1.0", 37 | "sanitize-html": "^1.21.1", 38 | "saslprep": "^1.0.3", 39 | "socket.io": "^2.3.0", 40 | "socket.io-redis": "^5.2.0", 41 | "socks-proxy-agent": "^5.0.0" 42 | }, 43 | "scripts": { 44 | "test": "echo \"Error: no test specified\" && exit 1", 45 | "setup": "npm i -g pm2 gulp && gulp", 46 | "start": "pm2 start ecosystem.config.js --env production", 47 | "start-dev": "pm2 start ecosystem.config.js --env development" 48 | }, 49 | "author": "fatchan", 50 | "license": "AGPL-3.0-or-later" 51 | } 52 | -------------------------------------------------------------------------------- /queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Queue = require('bull') 4 | , { redis } = require(__dirname+'/configs/main.js') 5 | , taskQueue = new Queue('task', { redis }); 6 | 7 | module.exports = { 8 | 9 | queue: taskQueue, 10 | 11 | push: (data, options) => { 12 | taskQueue.add(data, { ...options, removeOnComplete: true}); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Redis = require('ioredis') 4 | , configs = require(__dirname+'/configs/main.js') 5 | , client = new Redis(configs.redis); 6 | 7 | module.exports = { 8 | 9 | redisClient: client, 10 | 11 | //get a value with key 12 | get: (key) => { 13 | return client.get(key).then(res => { return JSON.parse(res) }); 14 | }, 15 | 16 | //set a value on key 17 | set: (key, value, ttl) => { 18 | if (ttl) { 19 | client.set(key, JSON.stringify(value), 'EX', ttl); 20 | } else { 21 | client.set(key, JSON.stringify(value)); 22 | } 23 | }, 24 | 25 | //add items to a set 26 | sadd: (key, value) => { 27 | return client.sadd(key, value); 28 | }, 29 | 30 | //get all members of a set 31 | sgetall: (key) => { 32 | return client.smembers(key); 33 | }, 34 | 35 | //remove an item from a set 36 | srem: (key, value) => { 37 | return client.srem(key, value); 38 | }, 39 | 40 | //get random item from set 41 | srand: (key) => { 42 | return client.srandmember(key); 43 | }, 44 | 45 | //delete value with key 46 | del: (keyOrKeys) => { 47 | if (Array.isArray(keyOrKeys)) { 48 | return client.del(...keyOrKeys); 49 | } else { 50 | return client.del(keyOrKeys); 51 | } 52 | }, 53 | 54 | deletePattern: (pattern) => { 55 | return new Promise((resolve, reject) => { 56 | const stream = client.scanStream({ 57 | match: pattern 58 | }); 59 | stream.on('data', (keys) => { 60 | if (keys.length > 0) { 61 | const pipeline = client.pipeline(); 62 | for (let i = 0; i < keys.length; i++) { 63 | pipeline.del(keys[i]); 64 | } 65 | pipeline.exec(); 66 | } 67 | }); 68 | stream.on('end', () => { 69 | resolve(); 70 | }); 71 | stream.on('error', (err) => { 72 | reject(err); 73 | }); 74 | }); 75 | }, 76 | 77 | } 78 | -------------------------------------------------------------------------------- /redlock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Redlock = require('redlock') 4 | , { redisClient } = require(__dirname+'/redis.js') 5 | , redlock = new Redlock([redisClient]); 6 | 7 | redlock.on('clientError', console.error); 8 | 9 | module.exports = redlock; 10 | -------------------------------------------------------------------------------- /remarkup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process 4 | .on('uncaughtException', console.error) 5 | .on('unhandledRejection', console.error); 6 | 7 | const Mongo = require(__dirname+'/db/db.js'); 8 | 9 | (async () => { 10 | 11 | await Mongo.connect(); 12 | const { Posts } = require(__dirname+'/db/') 13 | , linkQuotes = require(__dirname+'/helpers/posting/quotes.js') 14 | , { markdown } = require(__dirname+'/helpers/posting/markdown.js') 15 | , sanitizeOptions = require(__dirname+'/helpers/posting/sanitizeoptions.js') 16 | , sanitize = require('sanitize-html'); 17 | 18 | const posts = await Posts.db.find({/*query here*/}).toArray(); 19 | await Promise.all(posts.map(async (post) => { 20 | let message = markdown(post.nomarkup); 21 | const { quotedMessage, threadQuotes, crossQuotes } = await linkQuotes(post.board, message, null); 22 | message = sanitize(quotedMessage, sanitizeOptions.after); 23 | console.log(post.postId, message.substring(0,10)+'...'); 24 | return Posts.db.updateOne({board:post.board, postId:post.postId}, {$set:{message:message}}); 25 | })); 26 | 27 | })(); 28 | 29 | 30 | -------------------------------------------------------------------------------- /schedules/deletecaptchas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const deleteOld = require(__dirname+'/../helpers/files/deleteold.js') 4 | , timeUtils = require(__dirname+'/../helpers/timeutils.js') 5 | 6 | module.exports = () => { 7 | return deleteOld('captcha', Date.now()-(timeUtils.MINUTE*5)); 8 | } 9 | -------------------------------------------------------------------------------- /schedules/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process 4 | .on('uncaughtException', console.error) 5 | .on('unhandledRejection', console.error); 6 | 7 | const timeUtils = require(__dirname+'/../helpers/timeutils.js') 8 | , Mongo = require(__dirname+'/../db/db.js') 9 | , { pruneImmediately, debugLogs, enableWebring } = require(__dirname+'/../configs/main.js') 10 | , doInterval = require(__dirname+'/../helpers/dointerval.js'); 11 | 12 | (async () => { 13 | 14 | debugLogs && console.log('CONNECTING TO MONGODB'); 15 | await Mongo.connect(); 16 | await Mongo.checkVersion(); 17 | debugLogs && console.log('STARTING SCHEDULES'); 18 | 19 | //update board stats and homepage 20 | const taskQueue = require(__dirname+'/../queue.js'); 21 | taskQueue.push({ 22 | 'task': 'updateStats', 23 | 'options': {} 24 | }, { 25 | 'repeat': { 26 | 'cron': '0 * * * *' 27 | } 28 | }); 29 | 30 | //delete files for expired captchas 31 | const deleteCaptchas = require(__dirname+'/deletecaptchas.js'); 32 | doInterval(deleteCaptchas, timeUtils.MINUTE*5, true); 33 | 34 | //file pruning 35 | if (!pruneImmediately) { 36 | const pruneFiles = require(__dirname+'/prune.js'); 37 | doInterval(pruneFiles, timeUtils.DAY, true); 38 | } 39 | 40 | //update the webring 41 | if (enableWebring) { 42 | const updateWebring = require(__dirname+'/webring.js'); 43 | doInterval(updateWebring, timeUtils.MINUTE*15, true); 44 | } 45 | 46 | })(); 47 | -------------------------------------------------------------------------------- /schedules/prune.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Files = require(__dirname+'/../db/files.js') 4 | , { debugLogs } = require(__dirname+'/../configs/main.js') 5 | , { remove } = require('fs-extra') 6 | , uploadDirectory = require(__dirname+'/../helpers/files/uploadDirectory.js'); 7 | 8 | module.exports = async(fileNames) => { 9 | const query = { 10 | 'count': { 11 | '$lte': 0 12 | } 13 | } 14 | if (fileNames) { 15 | query['_id'] = { 16 | '$in': fileNames 17 | }; 18 | } 19 | const unreferenced = await Files.db.find(query, { 20 | 'projection': { 21 | 'count': 0, 22 | 'size': 0 23 | } 24 | }).toArray(); 25 | await Files.db.removeMany(query); 26 | await Promise.all(unreferenced.map(async file => { 27 | debugLogs && console.log('Pruning', file._id); 28 | return Promise.all( 29 | [remove(`${uploadDirectory}/file/${file._id}`)] 30 | .concat(file.exts ? file.exts.filter(ext => ext).map(ext => { 31 | remove(`${uploadDirectory}/file/thumb-${file._id.split('.')[0]}${ext}`) 32 | }) : []) 33 | ) 34 | })); 35 | } 36 | -------------------------------------------------------------------------------- /socketio.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const configs = require(__dirname+'/configs/main.js'); 4 | 5 | module.exports = { 6 | 7 | io: null, //null to begin with 8 | 9 | connect: (server) => { 10 | const io = require('socket.io')(server); 11 | const redisAdapter = require('socket.io-redis'); 12 | io.adapter(redisAdapter({ ...configs.redis })); 13 | module.exports.io = io; 14 | module.exports.startRooms(); 15 | }, 16 | 17 | startRooms: () => { 18 | module.exports.io.on('connection', socket => { 19 | socket.on('room', room => { 20 | //TODO: add some validation here that rooms exist or AT LEAST a regex for valid thread rooms 21 | socket.join(room); 22 | socket.send('joined'); 23 | }); 24 | }); 25 | }, 26 | 27 | emitRoom: (room, event, message) => { 28 | module.exports.io.to(room).emit(event, message); 29 | }, 30 | 31 | } 32 | -------------------------------------------------------------------------------- /views/custompages/rules.pug.example: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title Rules 5 | 6 | block content 7 | h1.board-title Rules 8 | .table-container.flex-center.mv-10 9 | table.table-body 10 | tr.table-row 11 | td 12 | p 1. Put rules here 13 | -------------------------------------------------------------------------------- /views/includes/actionfooter.pug: -------------------------------------------------------------------------------- 1 | details.toggle-label#actionform 2 | summary.toggle-summary Show Post Actions 3 | .actions 4 | h4.no-m-p Actions: 5 | label 6 | input.post-check(type='checkbox', name='delete' value='1') 7 | | Delete Posts 8 | label 9 | input.post-check(type='checkbox', name='unlink_file' value='1') 10 | | Unlink Files 11 | label 12 | input.post-check(type='checkbox', name='spoiler' value='1') 13 | | Spoiler Files 14 | label 15 | input#password(type='password', name='postpassword', placeholder='post password' autocomplete='off') 16 | label 17 | input.post-check(type='checkbox', name='report' value='1') 18 | | Report 19 | label 20 | input.post-check(type='checkbox', name='global_report' value='1') 21 | | Global Report 22 | label 23 | input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off') 24 | .actions 25 | h4.no-m-p Captcha: 26 | include ./captcha.pug 27 | input(type='submit', value='submit') 28 | -------------------------------------------------------------------------------- /views/includes/actionfooter_globalmanage.pug: -------------------------------------------------------------------------------- 1 | details.toggle-label#actionform 2 | summary.toggle-summary Show Post Actions 3 | .actions 4 | h4.no-m-p Actions: 5 | label 6 | input.post-check(type='checkbox', name='delete' value='1') 7 | | Delete Posts 8 | label 9 | input.post-check(type='checkbox', name='delete_file' value='1') 10 | | Delete Files 11 | label 12 | input.post-check(type='checkbox', name='spoiler' value='1') 13 | | Spoiler Files 14 | label 15 | input.post-check(type='checkbox', name='edit' value='1') 16 | | Edit Post 17 | label 18 | input.post-check(type='checkbox', name='delete_ip_global' value='1') 19 | | Delete from IP globally 20 | label 21 | input.post-check(type='checkbox', name='global_dismiss' value='1') 22 | | Dismiss Global Reports 23 | label 24 | input.post-check(type='checkbox', name='global_report_ban' value='1') 25 | | Global Ban Reporters 26 | label 27 | input.post-check(type='checkbox', name='global_ban' value='1') 28 | | Global Ban Poster 29 | label 30 | input.post-check(type='checkbox', name='ban_q' value='1') 31 | | 1/4 Range 32 | label 33 | input.post-check(type='checkbox', name='ban_h' value='1') 34 | | 1/2 Range 35 | label 36 | input.post-check(type='checkbox', name='no_appeal' value='1') 37 | | Non-appealable Ban 38 | label 39 | input.post-check(type='checkbox', name='preserve_post' value='1') 40 | | Show Post In Ban 41 | label 42 | input.post-check(type='checkbox', name='hide_name' value='1') 43 | | Hide Username In Modlog 44 | label 45 | input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') 46 | label 47 | input(type='text', name='ban_duration', placeholder='ban duration e.g. 7d' autocomplete='off') 48 | label 49 | input(type='text', name='log_message', placeholder='modlog message' autocomplete='off') 50 | input(type='submit', value='submit') 51 | -------------------------------------------------------------------------------- /views/includes/announcements.pug: -------------------------------------------------------------------------------- 1 | if board.settings.announcement.markdown 2 | hr(size=1) 3 | pre.post-message.no-m-p.text-center !{board.settings.announcement.markdown} 4 | hr(size=1) 5 | -------------------------------------------------------------------------------- /views/includes/banform.pug: -------------------------------------------------------------------------------- 1 | form.form-post(action=`/forms/appeal`, enctype='application/x-www-form-urlencoded', method='POST') 2 | include ./bantable.pug 3 | for ban in bans 4 | +ban(ban, true) 5 | - const allowAppeal = bans.filter(ban => ban.allowAppeal === true && !ban.appeal).length > 0; 6 | if allowAppeal === true 7 | h4.no-m-p Appeal bans: 8 | .form-wrapper.flexleft.mt-10 9 | input(type='hidden' name='_csrf' value=csrf) 10 | .row 11 | .label Message 12 | textarea(rows='5' name='message' required) 13 | .row 14 | .label Captcha 15 | span.col 16 | include ./captcha.pug 17 | input(type='submit', value='submit') 18 | -------------------------------------------------------------------------------- /views/includes/bantable.pug: -------------------------------------------------------------------------------- 1 | .table-container.mv-10.text-center.horscroll 2 | table.fw 3 | tr 4 | th 5 | th Board 6 | th Reason 7 | th IP 8 | th Type 9 | th Issuer 10 | th Issue Date 11 | th Expiry 12 | th Post(s) 13 | th Seen? 14 | th Appealable? 15 | th Appeal 16 | -------------------------------------------------------------------------------- /views/includes/boardpages.pug: -------------------------------------------------------------------------------- 1 | | Page: 2 | if maxPage === 0 3 | a.bold(href=`index.html`) [1] 4 | - for(let i = 1; i <= maxPage; i++) 5 | if page === i 6 | a.bold(href=`${i === 1 ? 'index' : i}.html`) [#{i}] 7 | else 8 | a(href=`${i === 1 ? 'index' : i}.html`) [#{i}] 9 | | 10 | | | 11 | -------------------------------------------------------------------------------- /views/includes/boardtable.pug: -------------------------------------------------------------------------------- 1 | .table-container.flex-center.mv-10.text-center 2 | table.boardtable 3 | tr 4 | th Board 5 | th Description 6 | th PPH 7 | th Users 8 | th Posts 9 | -------------------------------------------------------------------------------- /views/includes/captcha.pug: -------------------------------------------------------------------------------- 1 | noscript.no-m-p 2 | iframe.captcha(src='/captcha.html' 'width=210' height='80' scrolling='no' loading='lazy') 3 | .jsonly.captcha(style='display:none;') 4 | input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='captcha text' pattern=".{6}" required title='6 characters') 5 | -------------------------------------------------------------------------------- /views/includes/favicon.pug: -------------------------------------------------------------------------------- 1 | link(rel='shortcut icon' href='/favicon.ico' type='image/x-icon') 2 | link(rel='apple-touch-icon' sizes='144x144' href='/file/apple-touch-icon.png') 3 | link(rel='manifest' href='/site.webmanifest') 4 | link(rel='mask-icon' href='/file/safari-pinned-tab.svg' color='#5bbad5') 5 | meta(name='msapplication-TileColor' content='#00aba9') 6 | -------------------------------------------------------------------------------- /views/includes/filelabel.pug: -------------------------------------------------------------------------------- 1 | label.jsonly.postform-style.filelabel(for='file') 2 | | Select/Drop/Paste file#{maxFiles > 1 ? 's' : ''} 3 | -------------------------------------------------------------------------------- /views/includes/footer.pug: -------------------------------------------------------------------------------- 1 | unless minimal 2 | small.footer#bottom 3 | | - 4 | a(href='/rules.html') rules 5 | | - 6 | a(href='/faq.html') faq 7 | | - 8 | a(href='https://github.com/fatchan/jschan/') source code 9 | | - 10 | script(src=`/js/render.js?v=${commit}`) 11 | -------------------------------------------------------------------------------- /views/includes/head.pug: -------------------------------------------------------------------------------- 1 | meta(charset='utf-8') 2 | meta(name='viewport' content='width=device-width initial-scale=1') 3 | - const isBoard = board != null; 4 | if isBoard 5 | if board.settings.description 6 | meta(name='description' content=board.settings.description) 7 | if board.settings.tags 8 | meta(name='keywords' content=board.settings.tags.join(',')) 9 | noscript 10 | style .jsonly { display: none!important; } 11 | link(rel='stylesheet' href=`/css/style.css?v=${commit}`) 12 | - const theme = isBoard ? board.settings.theme : defaultTheme; 13 | - const codeTheme = isBoard ? board.settings.codeTheme : defaultCodeTheme; 14 | link#theme(rel='stylesheet' data-theme=theme href=`/css/themes/${theme}.css`) 15 | if isBoard && board.settings.customCss 16 | style #{board.settings.customCss} 17 | link#codetheme(rel='stylesheet' data-theme=codeTheme href=`/css/codethemes/${codeTheme}.css`) 18 | include ./favicon.pug 19 | script(src=`/js/all.js?v=${commit}`) 20 | -------------------------------------------------------------------------------- /views/includes/managebanform.pug: -------------------------------------------------------------------------------- 1 | if bans.length === 0 2 | p No bans. 3 | else 4 | input(type='hidden' name='_csrf' value=csrf) 5 | include ../includes/bantable.pug 6 | for ban in bans 7 | +ban(ban) 8 | .action-wrapper.mv-10 9 | .row 10 | label 11 | input(type='radio' name='option' value='unban' checked='checked') 12 | | Unban 13 | .row 14 | label 15 | input(type='radio' name='option' value='deny_appeal') 16 | | Deny Appeal 17 | input(type='submit' value='submit') 18 | 19 | -------------------------------------------------------------------------------- /views/includes/modal.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/modal.pug 2 | +modal(modal) 3 | -------------------------------------------------------------------------------- /views/includes/navbar.pug: -------------------------------------------------------------------------------- 1 | unless minimal 2 | nav.navbar 3 | a.nav-item(href='/index.html') Home 4 | a.nav-item(href='/news.html') News 5 | a.nav-item(href='/boards.html' style=(enableWebring ? 'line-height: 1.5em' : null)) 6 | | Boards 7 | if enableWebring 8 | .rainbow +Webring 9 | a.nav-item(href='/account.html') Account 10 | if board 11 | a.nav-item(href=`/${board._id}/manage/reports.html`) Manage 12 | a.jsonly.nav-item.right#settings 13 | -------------------------------------------------------------------------------- /views/includes/pages.pug: -------------------------------------------------------------------------------- 1 | | Page: 2 | - const qs = queryString ? queryString+'&' : '' 3 | if maxPage === 0 4 | a.bold(href=`?${qs}page=1`) [1] 5 | else if maxPage > 0 6 | - for(let i = 1; i <= maxPage; i++) 7 | if page === i 8 | a.bold(href=`?${qs}page=${i}`) [#{i}] 9 | else 10 | a(href=`?${qs}page=${i}`) [#{i}] 11 | | 12 | else 13 | if page > 1 14 | if page > 2 15 | a(href=`?${qs}page=1`) [<<] 16 | | 17 | a(href=`?${qs}page=${page-1}`) [<] 18 | | 19 | a.bold(href=`?${qs}page=${page}`) [#{page}] 20 | | 21 | a(href=`?${qs}page=${page+1}`) [>] 22 | -------------------------------------------------------------------------------- /views/includes/post.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/post.pug 2 | +post(post) 3 | -------------------------------------------------------------------------------- /views/includes/posticons.pug: -------------------------------------------------------------------------------- 1 | if post.sticky || post.bumplocked || post.locked || post.cyclic 2 | span.post-icons 3 | if post.sticky 4 | img(src='/file/sticky.png' height='14' width='14' title='Sticky') 5 | | 6 | if post.bumplocked 7 | img(src='/file/bumplock.png' height='14' width='14' title='Bumplocked') 8 | | 9 | if post.locked 10 | img(src='/file/lock.png' height='14' width='14' title='Locked') 11 | | 12 | if post.cyclic 13 | img(src='/file/cyclic.png' height='14' width='14' title='Cyclic') 14 | | 15 | 16 | -------------------------------------------------------------------------------- /views/includes/stickynav.pug: -------------------------------------------------------------------------------- 1 | nav.stickynav 2 | a.nav-item(href='#bottom') [▼] 3 | | 4 | a.nav-item(href='#top') [▲] 5 | -------------------------------------------------------------------------------- /views/includes/subjectfield.pug: -------------------------------------------------------------------------------- 1 | if !isThread || (!board.settings.disableReplySubject && !board.settings.forceAnon) 2 | section.row 3 | .label 4 | span Subject 5 | if subjectRequired 6 | span.required * 7 | input(type='text', name='subject', autocomplete='off' maxlength=globalLimits.fieldLength.subject required=subjectRequired) 8 | -------------------------------------------------------------------------------- /views/includes/webringboardtable.pug: -------------------------------------------------------------------------------- 1 | .table-container.flex-center.mv-10.text-center 2 | table.boardtable.w900 3 | tr 4 | th Board 5 | th Description 6 | th PPH 7 | th Users 8 | th Posts 9 | th Last Activity 10 | -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | include includes/head.pug 5 | block head 6 | body#top 7 | include includes/navbar.pug 8 | main(class=(minimal?'minimal':'')) 9 | .container 10 | block content 11 | include includes/footer.pug 12 | 13 | -------------------------------------------------------------------------------- /views/mixins/ban.pug: -------------------------------------------------------------------------------- 1 | include ./post.pug 2 | mixin ban(ban, banpage) 3 | tr 4 | td 5 | if !banpage || (ban.appeal == null && ban.allowAppeal === true) 6 | input.post-check(type='checkbox', name='checkedbans' value=ban._id) 7 | td 8 | if ban.board 9 | a(href=`/${ban.board}/`) /#{ban.board}/ 10 | else 11 | | Global 12 | td= ban.reason 13 | - const ip = permLevel > ipHashPermLevel ? ban.ip.single.slice(-10) : ban.ip.raw; 14 | td #{ip} 15 | td #{ban.type} 16 | td #{ban.issuer} 17 | - const banDate = new Date(ban.date); 18 | td: time.right.reltime(datetime=banDate.toISOString()) #{banDate.toLocaleString(undefined, {hour12:false})} 19 | - const expireDate = new Date(ban.expireAt); 20 | td: time.right.reltime(datetime=expireDate.toISOString()) #{expireDate.toLocaleString(undefined, {hour12:false})} 21 | td.banposts 22 | if ban.posts && ban.posts.length > 0 23 | | Hover to view 24 | .thread 25 | each p in ban.posts 26 | +post(p, false, false, false, true) 27 | else 28 | Posts not shown 29 | td 30 | if ban.seen 31 | | ✓ 32 | else 33 | | ⨯ 34 | td 35 | if ban.allowAppeal 36 | | ✓ 37 | else 38 | | ⨯ 39 | td 40 | if ban.appeal 41 | textarea(rows=1 disabled='true') #{ban.appeal} 42 | else if ban.allowAppeal 43 | | No appeal submitted 44 | else 45 | | - 46 | -------------------------------------------------------------------------------- /views/mixins/boardheader.pug: -------------------------------------------------------------------------------- 1 | mixin boardheader(pagename) 2 | .board-header 3 | img.board-banner(src=`/randombanner?board=${board._id}` width='300' height='100' loading='lazy') 4 | br 5 | if pagename 6 | h1.board-title #{pagename}(#[a.no-decoration(href=`/${board._id}/index.html`) /#{board._id}/]) 7 | else 8 | a.no-decoration(href=`/${board._id}/index.html`) 9 | h1.board-title /#{board._id}/ - #{board.settings.name} 10 | h4.board-description #{board.settings.description} 11 | -------------------------------------------------------------------------------- /views/mixins/boardnav.pug: -------------------------------------------------------------------------------- 1 | mixin boardnav(selected, showIndex, upLevel) 2 | if showIndex 3 | | 4 | a(href=`${upLevel ? '../' : ''}index.html` class=(selected === 'index' ? 'bold' : '')) [Index] 5 | | 6 | a(href=`${upLevel ? '../' : ''}catalog.html` class=(selected === 'catalog' ? 'bold' : '')) [Catalog] 7 | | 8 | a(href=`${upLevel ? '../' : ''}banners.html` class=(selected === 'banners' ? 'bold' : '')) [Banners] 9 | | 10 | a(href=`${upLevel ? '../' : ''}logs.html` class=(selected === 'logs' ? 'bold' : '')) [Logs] 11 | 12 | -------------------------------------------------------------------------------- /views/mixins/catalogtile.pug: -------------------------------------------------------------------------------- 1 | mixin catalogtile(board, post, index) 2 | .catalog-tile(data-board=post.board 3 | data-post-id=post.postId 4 | data-filter=((post.subject+post.nomarkup).toLowerCase() || '') 5 | data-date=post.date 6 | data-replies=post.replyposts 7 | data-bump=post.bumped) 8 | - const postURL = `/${board._id}/${modview ? 'manage/' : ''}thread/${post.postId}.html#${post.postId}` 9 | .post-info 10 | input.left.post-check(type='checkbox', name='checkedposts' value=post.postId) 11 | if modview 12 | a.left.ml-5.bold(href=`recent.html?postid=${post.postId}`) [+] 13 | include ../includes/posticons.pug 14 | a.no-decoration.post-subject(href=postURL) #{post.subject || 'No subject'} 15 | br 16 | span(title='Replies') R: #{post.replyposts} 17 | | / 18 | span(title='Files') F: #{post.replyfiles} 19 | | / 20 | span(title='Page') P: #{Math.ceil(index/10)} 21 | if post.files.length > 0 22 | .post-file-src 23 | a(href=postURL) 24 | - const file = post.files[0] 25 | if post.spoiler 26 | div.spoilerimg.catalog-thumb 27 | else if file.attachment 28 | div.attachmentimg.catalog-thumb 29 | else if file.mimetype.startsWith('audio') 30 | div.audioimg.catalog-thumb 31 | else if file.hasThumb 32 | img.catalog-thumb(src=`/file/thumb-${file.hash}${file.thumbextension}` width=file.geometry.thumbwidth height=file.geometry.thumbheight loading='lazy') 33 | else 34 | img.catalog-thumb(src=`/file/${file.filename}` width=file.geometry.width height=file.geometry.height loading='lazy') 35 | if post.message 36 | pre.no-m-p.post-message !{post.message} 37 | -------------------------------------------------------------------------------- /views/mixins/globalmanagenav.pug: -------------------------------------------------------------------------------- 1 | mixin globalmanagenav(selected) 2 | nav.pages 3 | a(href='reports.html' class=(selected === 'reports' ? 'bold' : '')) [Reports] 4 | | 5 | a(href='bans.html' class=(selected === 'bans' ? 'bold' : '')) [Bans] 6 | | 7 | a(href='recent.html' class=(selected === 'recent' ? 'bold' : '')) [Recent] 8 | | 9 | a(href='globallogs.html' class=(selected === 'logs' ? 'bold' : '')) [Logs] 10 | if permLevel === 0 11 | | 12 | a(href='accounts.html' class=(selected === 'accounts' ? 'bold' : '')) [Accounts] 13 | | 14 | a(href='news.html' class=(selected === 'news' ? 'bold' : '')) [News] 15 | | 16 | a(href='settings.html' class=(selected === 'settings' ? 'bold' : '')) [Settings] 17 | 18 | -------------------------------------------------------------------------------- /views/mixins/managenav.pug: -------------------------------------------------------------------------------- 1 | mixin managenav(selected, upLevel) 2 | nav.pages 3 | if selected === 'index' 4 | include ../includes/boardpages.pug 5 | | 6 | else 7 | a(href=`${upLevel ? '../' : ''}index.html` class=(selected === 'index' ? 'bold' : '')) [Mod Index] 8 | | 9 | a(href=`${upLevel ? '../' : ''}catalog.html` class=(selected === 'catalog' ? 'bold' : '')) [Mod Catalog] 10 | | 11 | a(href=`${upLevel ? '../' : ''}recent.html` class=(selected === 'recent' ? 'bold' : '')) [Recent] 12 | | 13 | a(href=`${upLevel ? '../' : ''}reports.html` class=(selected === 'reports' ? 'bold' : '')) [Reports] 14 | | 15 | a(href=`${upLevel ? '../' : ''}bans.html` class=(selected === 'bans' ? 'bold' : '')) [Bans] 16 | | 17 | a(href=`${upLevel ? '../' : ''}logs.html` class=(selected === 'logs' ? 'bold' : '')) [Logs] 18 | | 19 | if permLevel < 3 20 | a(href=`${upLevel ? '../' : ''}settings.html` class=(selected === 'settings' ? 'bold' : '')) [Settings] 21 | | 22 | a(href=`${upLevel ? '../' : ''}banners.html` class=(selected === 'banners' ? 'bold' : '')) [Banners] 23 | -------------------------------------------------------------------------------- /views/mixins/newspost.pug: -------------------------------------------------------------------------------- 1 | mixin newspost(post, globalmanage=false) 2 | .table-container.flex-center.mv-5 3 | .anchor(id=post._id) 4 | table 5 | tr 6 | th 7 | if globalmanage === true 8 | input.left.post-check(type='checkbox', name='checkednews' value=post._id) 9 | a.left(href=`#${post._id}`) #{post.title} 10 | - const newsDate = new Date(post.date); 11 | time.right.reltime(datetime=newsDate.toISOString()) #{newsDate.toLocaleString(undefined, {hour12:false})} 12 | tr 13 | td 14 | if globalmanage === true 15 | p.no-m-p #{`${post.message.raw.substring(0,50)}...`} 16 | else 17 | pre.post-message.no-m-p !{post.message.markdown} 18 | -------------------------------------------------------------------------------- /views/mixins/report.pug: -------------------------------------------------------------------------------- 1 | mixin report(r, manage=false) 2 | .reports.post-container 3 | input.post-check(type='checkbox', name='checkedreports' value=r.id) 4 | | 5 | - const ip = permLevel > ipHashPermLevel ? r.ip.single.slice(-10) : r.ip.raw; 6 | a.bold(href=`${manage ? 'recent.html' : ''}?ip=${encodeURIComponent(ip)}`) [#{ip}] 7 | | 8 | - const reportDate = new Date(r.date); 9 | time.reltime(datetime=reportDate.toISOString()) #{reportDate.toLocaleString(undefined, { hour12:false })} 10 | | | Reason: #{r.reason} 11 | -------------------------------------------------------------------------------- /views/pages/404.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block content 4 | h1.board-title 404 Not Found 5 | -------------------------------------------------------------------------------- /views/pages/502.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block content 4 | h1.board-title 502 Bad Gateway 5 | .col.flex-center.mv-10 6 | p The server encountered a temporary error and could not complete your request. 7 | -------------------------------------------------------------------------------- /views/pages/account.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title Account 5 | 6 | block content 7 | .board-header 8 | h1.board-title Welcome, #{user.username} 9 | h4.board-description Auth level: #{user.authLevel} 10 | br 11 | hr(size=1) 12 | h4.no-m-p General: 13 | ul 14 | if user.authLevel <= 1 15 | li: a(href='/globalmanage/recent.html') Global management 16 | if enableUserBoardCreation || user.authLevel <= 1 17 | li: a(href='/create.html') Create a board 18 | if !enableUserAccountCreation && user.authLevel <= 1 19 | li: a(href='/register.html') Register an account 20 | li: a(href='/changepassword.html') Change password 21 | form(action='/forms/logout' method='post') 22 | input(type='submit' value='Log out') 23 | hr(size=1) 24 | h4.no-m-p Boards you own: 25 | if user.ownedBoards && user.ownedBoards.length > 0 26 | ul 27 | for b in user.ownedBoards 28 | li 29 | a(href=`/${b}/index.html`) /#{b}/ 30 | | - 31 | a(href=`/${b}/manage/index.html`) Mod Index 32 | | , 33 | a(href=`/${b}/manage/catalog.html`) Mod Catalog 34 | | , 35 | a(href=`/${b}/manage/recent.html`) Recent 36 | | , 37 | a(href=`/${b}/manage/reports.html`) Reports 38 | | , 39 | a(href=`/${b}/manage/bans.html`) Bans 40 | | , 41 | a(href=`/${b}/manage/logs.html`) Logs 42 | | , 43 | a(href=`/${b}/manage/settings.html`) Settings 44 | | , 45 | a(href=`/${b}/manage/banners.html`) Banners 46 | else 47 | p None 48 | hr(size=1) 49 | h4.no-m-p Boards you moderate: 50 | if user.modBoards && user.modBoards.length > 0 51 | ul 52 | for b in user.modBoards 53 | li 54 | a(href=`/${b}/index.html`) /#{b}/ 55 | | - 56 | a(href=`/${b}/manage/index.html`) Mod Index 57 | | , 58 | a(href=`/${b}/manage/catalog.html`) Mod Catalog 59 | | , 60 | a(href=`/${b}/manage/recent.html`) Recent 61 | | , 62 | a(href=`/${b}/manage/reports.html`) Reports 63 | | , 64 | a(href=`/${b}/manage/bans.html`) Bans 65 | | , 66 | a(href=`/${b}/manage/logs.html`) Logs 67 | else 68 | p None 69 | -------------------------------------------------------------------------------- /views/pages/ban.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/ban.pug 3 | 4 | block head 5 | title Banned! 6 | 7 | block content 8 | h1.board-title Banned! 9 | h4.board-description Bans currently in place against your IP: 10 | include ../includes/banform.pug 11 | -------------------------------------------------------------------------------- /views/pages/banners.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/boardheader.pug 3 | include ../mixins/boardnav.pug 4 | 5 | block head 6 | title /#{board._id}/ - Banners 7 | 8 | block content 9 | +boardheader('Banners') 10 | br 11 | include ../includes/stickynav.pug 12 | .pages 13 | +boardnav('banners', true, false) 14 | hr(size=1) 15 | if board.banners.length > 0 16 | .catalog 17 | each banner in board.banners 18 | img.board-banner(src=`/banner/${board._id}/${banner}` width='300' height='100' loading='lazy') 19 | else 20 | p Board has no custom banners. 21 | include ../includes/stickynav.pug 22 | hr(size=1) 23 | .pages 24 | +boardnav('banners', true, false) 25 | -------------------------------------------------------------------------------- /views/pages/board.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/post.pug 3 | include ../mixins/boardnav.pug 4 | include ../mixins/managenav.pug 5 | include ../mixins/boardheader.pug 6 | 7 | block head 8 | title /#{board._id}/ - #{board.settings.name} - page #{page} 9 | 10 | block content 11 | +boardheader(modview ? 'Mod View' : null) 12 | br 13 | include ../includes/postform.pug 14 | br 15 | include ../includes/announcements.pug 16 | include ../includes/stickynav.pug 17 | if modview 18 | +managenav('index') 19 | else 20 | .pages 21 | include ../includes/boardpages.pug 22 | +boardnav(null, false, false) 23 | form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}actions` method='POST' enctype='application/x-www-form-urlencoded') 24 | if modview 25 | input(type='hidden' name='_csrf' value=csrf) 26 | hr(size=1) 27 | if threads.length === 0 28 | p No posts. 29 | hr(size=1) 30 | for thread in threads 31 | .thread 32 | +post(thread, true) 33 | for post in thread.replies 34 | +post(post, true) 35 | hr(size=1) 36 | if modview 37 | +managenav('index') 38 | else 39 | .pages 40 | include ../includes/boardpages.pug 41 | +boardnav(null, false, false) 42 | if modview 43 | include ../includes/actionfooter_manage.pug 44 | else 45 | include ../includes/actionfooter.pug 46 | -------------------------------------------------------------------------------- /views/pages/bypass.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title Block Bypass 5 | 6 | block content 7 | h1.board-title Block Bypass 8 | .form-wrapper.flex-center.mv-10 9 | if message 10 | p.title #{message} 11 | form.form-post(action='/forms/blockbypass' method='POST') 12 | .row 13 | .label Captcha 14 | span.col 15 | include ../includes/captcha.pug 16 | if minimal 17 | input(type='hidden' name='minimal' value='1') 18 | input(type='submit', value='Submit') 19 | -------------------------------------------------------------------------------- /views/pages/captcha.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf-8') 5 | link(rel='stylesheet', href='/css/nscaptcha.css') 6 | body 7 | img(src='/captcha') 8 | form(action='/forms/newcaptcha', method='POST') 9 | input(type='submit' value='↻') 10 | -------------------------------------------------------------------------------- /views/pages/catalog.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/catalogtile.pug 3 | include ../mixins/boardnav.pug 4 | include ../mixins/managenav.pug 5 | include ../mixins/boardheader.pug 6 | 7 | block head 8 | title /#{board._id}/ - Catalog 9 | 10 | block content 11 | +boardheader('Catalog') 12 | br 13 | include ../includes/postform.pug 14 | br 15 | include ../includes/announcements.pug 16 | include ../includes/stickynav.pug 17 | .wrapbar 18 | if modview 19 | +managenav('catalog') 20 | else 21 | .pages 22 | +boardnav('catalog', true, false) 23 | .pages.jsonly 24 | input#catalogfilter(type='text' placeholder='Filter') 25 | select.ml-5.right#catalogsort 26 | option(value="" disabled selected hidden) Sort by 27 | option(value="bump") Bump order 28 | option(value="date") Creation date 29 | option(value="replies") Reply count 30 | form(target='_blank' action=`/forms/board/${board._id}/${modview ? 'mod' : ''}actions` method='POST' enctype='application/x-www-form-urlencoded') 31 | if modview 32 | input(type='hidden' name='_csrf' value=csrf) 33 | hr(size=1) 34 | if threads.length === 0 35 | p No posts. 36 | else 37 | .catalog 38 | for thread, i in threads 39 | +catalogtile(board, thread, i+1) 40 | hr(size=1) 41 | if modview 42 | +managenav('catalog') 43 | else 44 | .pages 45 | +boardnav('catalog', true, false) 46 | if modview 47 | include ../includes/actionfooter_manage.pug 48 | else 49 | include ../includes/actionfooter.pug 50 | -------------------------------------------------------------------------------- /views/pages/changepassword.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title Change Password 5 | 6 | block content 7 | h1.board-title Change Password 8 | .form-wrapper.flex-center.mv-10 9 | form.form-post(action='/forms/changepassword' method='POST') 10 | .row 11 | .label Username 12 | input(type='text', name='username', maxlength='50' required) 13 | .row 14 | .label Existing Password 15 | input(type='password', name='password', maxlength='100' required) 16 | .row 17 | .label New Password 18 | input(type='password', name='newpassword', maxlength='100' required) 19 | .row 20 | .label Confirm New Password 21 | input(type='password', name='newpasswordconfirm', maxlength='100' required) 22 | .row 23 | .label Captcha 24 | span.col 25 | include ../includes/captcha.pug 26 | input(type='submit', value='Change Password') 27 | p: a(href='/login.html') Login 28 | if enableUserAccountCreation 29 | p: a(href='/register.html') Register 30 | -------------------------------------------------------------------------------- /views/pages/create.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title Create Board 5 | 6 | block content 7 | h1.board-title Create Board 8 | .form-wrapper.flex-center.mv-10 9 | form.form-post(action='/forms/create' method='POST') 10 | .row 11 | .label URI e.g. /uri/ 12 | input(type='text', name='uri', maxlength=globalLimits.fieldLength.uri pattern='[a-zA-Z0-9]+' required title='alphanumeric only') 13 | .row 14 | .label Name 15 | input(type='text', name='name', maxlength=globalLimits.fieldLength.boardname required) 16 | .row 17 | .label Description 18 | input(type='text', name='description', maxlength=globalLimits.fieldLength.description required) 19 | .row 20 | .label Tags 21 | textarea(name='tags' placeholder='newline separated, max 10') 22 | .row 23 | .label Captcha 24 | span.col 25 | include ../includes/captcha.pug 26 | input(type='submit', value='submit') 27 | 28 | -------------------------------------------------------------------------------- /views/pages/error.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block content 4 | h1 Internal server error 5 | -------------------------------------------------------------------------------- /views/pages/globalmanageaccounts.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/globalmanagenav.pug 3 | 4 | block head 5 | title Manage 6 | 7 | block content 8 | h1.board-title Global Management 9 | br 10 | +globalmanagenav('accounts') 11 | hr(size=1) 12 | .form-wrapper.flexleft 13 | h4.no-m-p Search: 14 | form.form-post.mv-5(action=`/globalmanage/accounts.html` method='GET') 15 | input(type='hidden' value=page) 16 | .row 17 | .label Username 18 | input(type='text' name='username' value=username) 19 | .row 20 | .label Board URI 21 | input(type='text' name='uri' value=uri) 22 | input(type='submit', value='Filter') 23 | h4.no-m-p Accounts: 24 | if accounts && accounts.length > 0 25 | form.form-post(action=`/forms/global/editaccounts` method='POST' enctype='application/x-www-form-urlencoded') 26 | input(type='hidden' name='_csrf' value=csrf) 27 | .table-container.flex-left 28 | table.fw 29 | tr 30 | th 31 | th Username 32 | th Auth Level 33 | th Own Boards 34 | th Mod Boards 35 | for account in accounts 36 | tr 37 | td: input(type='checkbox', name='checkedaccounts' value=account._id) 38 | td #{account._id} 39 | td #{account.authLevel} 40 | td 41 | if account.ownedBoards.length > 0 42 | for b in account.ownedBoards 43 | a(href=`/${b}/index.html`) /#{b}/ 44 | | 45 | else 46 | | - 47 | td 48 | if account.modBoards.length > 0 49 | for b in account.modBoards 50 | a(href=`/${b}/index.html`) /#{b}/ 51 | | 52 | else 53 | | - 54 | .pages.mv-5 55 | include ../includes/pages.pug 56 | .row 57 | .label Set Auth Level 58 | input(type='number' name='auth_level') 59 | .row 60 | .label Delete Accounts 61 | label.postform-style.ph-5 62 | input(type='checkbox', name='delete_account', value='true') 63 | input(type='submit', value='apply') 64 | else 65 | p No results. 66 | -------------------------------------------------------------------------------- /views/pages/globalmanagebans.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/ban.pug 3 | include ../mixins/globalmanagenav.pug 4 | 5 | block head 6 | title Manage 7 | 8 | block content 9 | h1.board-title Global Management 10 | br 11 | +globalmanagenav('bans') 12 | hr(size=1) 13 | h4.no-m-p Global Bans & Appeals: 14 | form(action=`/forms/global/editbans` method='POST' enctype='application/x-www-form-urlencoded') 15 | include ../includes/managebanform.pug 16 | -------------------------------------------------------------------------------- /views/pages/globalmanagelogs.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/ban.pug 3 | include ../mixins/globalmanagenav.pug 4 | 5 | block head 6 | title Manage 7 | 8 | block content 9 | h1.board-title Global Management 10 | br 11 | +globalmanagenav('logs') 12 | hr(size=1) 13 | .form-wrapper.flexleft 14 | h4.no-m-p Search: 15 | form.form-post.mv-5(action=`/globalmanage/globallogs.html` method='GET') 16 | input(type='hidden' value=page) 17 | .row 18 | .label Board URI 19 | input(type='text' name='uri' value=uri) 20 | .row 21 | .label Username 22 | input(type='text' name='username' value=username) 23 | .row 24 | .label IP 25 | input(type='text' name='ip' value=ip) 26 | input(type='submit', value='Filter') 27 | h4.no-m-p Global Logs: 28 | if logs && logs.length > 0 29 | .table-container.flex-center.mv-10.text-center 30 | table.fw 31 | tr 32 | th Date 33 | th Board 34 | th User 35 | th IP 36 | th Actions 37 | th Post IDs 38 | th Log Message 39 | for log in logs 40 | tr 41 | - const logDate = new Date(log.date); 42 | td: time.reltime(datetime=logDate.toISOString()) #{logDate.toLocaleString(undefined, {hour12:false})} 43 | td 44 | a(href=`/${log.board}/index.html`) /#{log.board}/ 45 | | 46 | a(href=`?uri=${log.board}`) [+] 47 | td 48 | if log.user !== 'Unregistered User' 49 | a(href=`accounts.html?username=${log.user}`) #{log.user} 50 | else 51 | | #{log.user} 52 | | 53 | a(href=`?username=${log.user}`) [+] 54 | td 55 | - const logIp = permLevel > ipHashPermLevel ? log.ip.single.slice(-10) : log.ip.raw; 56 | a(href=`recent.html?ip=${encodeURIComponent(logIp)}`) #{logIp} 57 | | 58 | a(href=`?ip=${encodeURIComponent(logIp)}`) [+] 59 | td #{log.actions} 60 | td #{log.postIds} 61 | td #{log.message || '-'} 62 | .pages.mv-5 63 | include ../includes/pages.pug 64 | else 65 | p No logs. 66 | -------------------------------------------------------------------------------- /views/pages/globalmanagenews.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/newspost.pug 3 | include ../mixins/globalmanagenav.pug 4 | 5 | block head 6 | title Manage 7 | 8 | block content 9 | h1.board-title Global Management 10 | br 11 | +globalmanagenav('news') 12 | hr(size=1) 13 | h4.no-m-p Add News: 14 | .form-wrapper.flexleft 15 | form.form-post(action=`/forms/global/addnews`, enctype='application/x-www-form-urlencoded', method='POST') 16 | input(type='hidden' name='_csrf' value=csrf) 17 | .row 18 | .label Title 19 | input(type='text' name='title' required) 20 | .row 21 | .label Message 22 | textarea(name='message' rows='10' placeholder='supports post styling' required) 23 | input(type='submit', value='submit') 24 | if news.length > 0 25 | hr(size=1) 26 | h4.no-m-p Delete News: 27 | .form-wrapper.flexleft 28 | form.form-post(action=`/forms/global/deletenews`, enctype='application/x-www-form-urlencoded', method='POST') 29 | input(type='hidden' name='_csrf' value=csrf) 30 | each post in news 31 | +newspost(post, true) 32 | if news.length === 1 33 | .anchor 34 | input(type='submit', value='delete') 35 | -------------------------------------------------------------------------------- /views/pages/globalmanagerecent.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/globalmanagenav.pug 3 | include ../mixins/post.pug 4 | 5 | block head 6 | title Manage 7 | 8 | block content 9 | h1.board-title Global Management 10 | br 11 | +globalmanagenav('recent') 12 | form(action=`/forms/global/actions` method='POST' enctype='application/x-www-form-urlencoded') 13 | input(type='hidden' name='_csrf' value=csrf) 14 | if posts.length === 0 15 | hr(size=1) 16 | p No posts. 17 | else 18 | hr(size=1) 19 | if ip 20 | h4.no-m-p Post history for #{ip} 21 | hr(size=1) 22 | for p in posts 23 | .thread 24 | +post(p, false, false, true) 25 | hr(size=1) 26 | .pages.mv-5 27 | include ../includes/pages.pug 28 | include ../includes/actionfooter_globalmanage.pug 29 | -------------------------------------------------------------------------------- /views/pages/globalmanagereports.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/post.pug 3 | include ../mixins/globalmanagenav.pug 4 | 5 | block head 6 | title Manage 7 | 8 | block content 9 | h1.board-title Global Management 10 | br 11 | +globalmanagenav('reports') 12 | form(action=`/forms/global/actions` method='POST' enctype='application/x-www-form-urlencoded') 13 | if reports.length === 0 14 | hr(size=1) 15 | p No reports. 16 | else 17 | input(type='hidden' name='_csrf' value=csrf) 18 | hr(size=1) 19 | if ip 20 | h4.no-m-p Reports against or by #{ip} 21 | hr(size=1) 22 | for report in reports 23 | .thread 24 | +post(report, false, false, true) 25 | hr(size=1) 26 | .pages.mv-5 27 | include ../includes/pages.pug 28 | include ../includes/actionfooter_globalmanage.pug 29 | -------------------------------------------------------------------------------- /views/pages/globalmanagesettings.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/globalmanagenav.pug 3 | 4 | block head 5 | title Manage 6 | 7 | block content 8 | h1.board-title Global Management 9 | br 10 | +globalmanagenav('settings') 11 | hr(size=1) 12 | h4.no-m-p Delete board: 13 | .form-wrapper.flexleft.mt-10 14 | form.form-post(action=`/forms/global/deleteboard`, enctype='application/x-www-form-urlencoded', method='POST') 15 | input(type='hidden' name='_csrf' value=csrf) 16 | .row 17 | .label Board URI 18 | input(type='text' name='uri' required) 19 | .row 20 | .label I'm sure 21 | label.postform-style.ph-5 22 | input(type='checkbox', name='confirm', value='true' required) 23 | input(type='submit', value='submit') 24 | hr(size=1) 25 | h4.no-m-p Settings: 26 | .form-wrapper.flexleft.mt-10 27 | form.form-post(action=`/forms/global/settings`, enctype='application/x-www-form-urlencoded', method='POST') 28 | input(type='hidden' name='_csrf' value=csrf) 29 | .row 30 | .label Filters 31 | textarea(name='filters' placeholder='newline separated, max 50') #{settings.filters.join('\n')} 32 | .row 33 | .label Filter Mode 34 | select(name='filter_mode') 35 | option(value='0', selected=settings.filterMode === 0) Do nothing 36 | option(value='1', selected=settings.filterMode === 1) Block post 37 | option(value='2', selected=settings.filterMode === 2) Ban 38 | .row 39 | .label Filter Auto Ban Duration 40 | input(type='text' name='ban_duration' placeholder='e.g. 1w' value=settings.filterBanDuration) 41 | input(type='submit', value='save settings') 42 | -------------------------------------------------------------------------------- /views/pages/login.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title Login 5 | 6 | block content 7 | h1.board-title Login 8 | .form-wrapper.flex-center.mv-10 9 | form.form-post(action='/forms/login' method='POST') 10 | input(type='hidden' name='goto' value=goto) 11 | .row 12 | .label Username 13 | input(type='text', name='username', maxlength='50' required) 14 | .row 15 | .label Password 16 | input(type='password', name='password', maxlength='100' required) 17 | input(type='submit', value='submit') 18 | if enableUserAccountCreation 19 | p: a(href='/register.html') Register 20 | p: a(href='/changepassword.html') Change Password 21 | 22 | -------------------------------------------------------------------------------- /views/pages/managebanners.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/managenav.pug 3 | include ../mixins/boardheader.pug 4 | 5 | block head 6 | title /#{board._id}/ - Manage Banners 7 | 8 | block content 9 | +boardheader('Banners') 10 | br 11 | +managenav('banners') 12 | hr(size=1) 13 | h4.no-m-p Add Banners: 14 | .form-wrapper.flexleft.mt-10 15 | form.form-post(action=`/forms/board/${board._id}/addbanners`, enctype='multipart/form-data', method='POST') 16 | input(type='hidden' name='_csrf' value=csrf) 17 | .row 18 | - const maxFiles = globalLimits.bannerFiles.max; 19 | .label 20 | span Banner#{maxFiles > 1 ? 's' : ''} 21 | span.required * 22 | if maxFiles > 1 23 | | 24 | | 25 | small (Max #{maxFiles}) 26 | span.col 27 | include ../includes/filelabel.pug 28 | input#file(type='file', name='file' multiple required) 29 | .upload-list 30 | input(type='submit', value='submit') 31 | if board.banners.length > 0 32 | hr(size=1) 33 | h4.no-m-p Delete Banners: 34 | .form-wrapper.flexleft.mt-10 35 | form.form-post(action=`/forms/board/${board._id}/deletebanners`, enctype='application/x-www-form-urlencoded', method='POST') 36 | input(type='hidden' name='_csrf' value=csrf) 37 | .catalog 38 | each banner in board.banners 39 | label.banner-check 40 | input(type='checkbox' name='checkedbanners' value=banner) 41 | img.board-banner(src=`/banner/${board._id}/${banner}` width='300' height='100' loading='lazy') 42 | input(type='submit', value='delete') 43 | -------------------------------------------------------------------------------- /views/pages/managebans.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/ban.pug 3 | include ../mixins/managenav.pug 4 | include ../mixins/boardheader.pug 5 | 6 | block head 7 | title /#{board._id}/ - Manage Bans & Appeals 8 | 9 | block content 10 | +boardheader('Bans') 11 | br 12 | +managenav('bans') 13 | hr(size=1) 14 | h4.no-m-p Bans & Appeals: 15 | form(action=`/forms/board/${board._id}/editbans` method='POST' enctype='application/x-www-form-urlencoded') 16 | include ../includes/managebanform.pug 17 | -------------------------------------------------------------------------------- /views/pages/managelogs.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/post.pug 3 | include ../mixins/ban.pug 4 | include ../mixins/managenav.pug 5 | include ../mixins/boardheader.pug 6 | 7 | block head 8 | title /#{board._id}/ - Manage 9 | 10 | block content 11 | +boardheader('Logs') 12 | br 13 | +managenav('logs') 14 | hr(size=1) 15 | .form-wrapper.flexleft 16 | h4.no-m-p Search: 17 | form.form-post.mv-5(action=`/${board._id}/manage/logs.html` method='GET') 18 | input(type='hidden' value=page) 19 | .row 20 | .label Username 21 | input(type='text' name='username' value=username) 22 | input(type='submit', value='Filter') 23 | h4.no-m-p Logs: 24 | if logs && logs.length > 0 25 | .table-container.flex-center.mv-10.text-center 26 | table.fw 27 | tr 28 | th Date 29 | th User 30 | th IP 31 | th Actions 32 | th Post IDs 33 | th Log Message 34 | for log in logs 35 | tr 36 | - const logDate = new Date(log.date); 37 | td: time.reltime(datetime=logDate.toISOString()) #{logDate.toLocaleString(undefined, {hour12:false})} 38 | td 39 | | #{log.user} 40 | | 41 | a(href=`?username=${log.user}`) [+] 42 | td 43 | - const logIp = permLevel > ipHashPermLevel ? log.ip.single.slice(-10) : log.ip.raw; 44 | | #{logIp} 45 | td #{log.actions} 46 | td #{log.postIds} 47 | td #{log.message || '-'} 48 | .pages.mv-5 49 | include ../includes/pages.pug 50 | else 51 | p No logs. 52 | -------------------------------------------------------------------------------- /views/pages/managerecent.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/post.pug 3 | include ../mixins/ban.pug 4 | include ../mixins/managenav.pug 5 | include ../mixins/boardheader.pug 6 | 7 | block head 8 | title /#{board._id}/ - Manage 9 | 10 | block content 11 | +boardheader('Recent Posts') 12 | br 13 | +managenav('recent') 14 | hr(size=1) 15 | form(action=`/forms/board/${board._id}/modactions` method='POST' enctype='application/x-www-form-urlencoded') 16 | input(type='hidden' name='_csrf' value=csrf) 17 | if posts.length === 0 18 | p No posts. 19 | else 20 | - const ip = permLevel > ipHashPermLevel ? posts[0].ip.single.slice(-10) : posts[0].ip.raw; 21 | if postId || (queryIp && queryIp === ip) 22 | h4.no-m-p Post history for #{ip} 23 | hr(size=1) 24 | for p in posts 25 | .thread 26 | +post(p, true, true) 27 | hr(size=1) 28 | .pages.mv-5 29 | include ../includes/pages.pug 30 | include ../includes/actionfooter_manage.pug 31 | -------------------------------------------------------------------------------- /views/pages/managereports.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/post.pug 3 | include ../mixins/ban.pug 4 | include ../mixins/managenav.pug 5 | include ../mixins/boardheader.pug 6 | 7 | block head 8 | title /#{board._id}/ - Manage 9 | 10 | block content 11 | +boardheader('Reports') 12 | br 13 | +managenav('reports') 14 | hr(size=1) 15 | h4.no-m-p Reports: 16 | form(action=`/forms/board/${board._id}/modactions` method='POST' enctype='application/x-www-form-urlencoded') 17 | if reports.length === 0 18 | p No reports. 19 | else 20 | input(type='hidden' name='_csrf' value=csrf) 21 | for report in reports 22 | .thread 23 | +post(report, false, true) 24 | hr(size=1) 25 | include ../includes/actionfooter_manage.pug 26 | -------------------------------------------------------------------------------- /views/pages/message.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title #{title} 5 | if redirect 6 | meta(http-equiv="refresh" content=`3;url=${redirect}`) 7 | 8 | block content 9 | h1 #{title} 10 | ul 11 | if message 12 | li #{message} 13 | if error 14 | li #{error} 15 | if messages 16 | each msg in messages 17 | li #{msg} 18 | if errors 19 | each error in errors 20 | li #{error} 21 | if link 22 | div 23 | a.button(href=link.href target='_blank') #{link.text} 24 | if redirect 25 | p You will be redirected shortly. If you are not redirected automatically, you can #[a(href=redirect) click here]. 26 | -------------------------------------------------------------------------------- /views/pages/modlog.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/boardheader.pug 3 | include ../mixins/boardnav.pug 4 | 5 | block head 6 | title /#{board._id}/ - Logs for #{startDate.toDateString()} 7 | 8 | block content 9 | +boardheader('Logs') 10 | br 11 | include ../includes/stickynav.pug 12 | .pages 13 | +boardnav('logs', true, true) 14 | hr(size=1) 15 | .table-container.flex-center.mv-10.text-center 16 | table 17 | tr 18 | th Date 19 | th User 20 | th Actions 21 | th Post IDs 22 | th Log Message 23 | for log in logs 24 | tr 25 | - const logDate = new Date(log.date); 26 | td: time.reltime(datetime=logDate.toISOString()) #{logDate.toLocaleString(undefined, {hour12:false})} 27 | td(class=(!log.showUser ? 'em' : '')) #{log.showUser ? log.user : 'Hidden User'} 28 | td #{log.actions} 29 | td #{log.postIds} 30 | td #{log.message || '-'} 31 | hr(size=1) 32 | .pages 33 | +boardnav('logs', true, true) 34 | -------------------------------------------------------------------------------- /views/pages/modloglist.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/boardheader.pug 3 | include ../mixins/boardnav.pug 4 | 5 | block head 6 | title /#{board._id}/ - Logs 7 | 8 | block content 9 | +boardheader('Logs') 10 | br 11 | include ../includes/stickynav.pug 12 | .pages 13 | +boardnav('logs', true, false) 14 | hr(size=1) 15 | if dates.length === 0 16 | p No Logs. 17 | else 18 | .table-container.flex-center.mv-10.text-center 19 | table 20 | tr 21 | th Date 22 | th Events 23 | for row in dates 24 | tr 25 | - 26 | const { date, count } = row; 27 | const day = ('0'+date.day).slice(-2); 28 | const month = ('0'+date.month).slice(-2); 29 | const year = date.year; 30 | const logName = `${month}-${day}-${year}`; 31 | td: a(href=`logs/${logName}.html`) #{logName} 32 | td #{count} 33 | hr(size=1) 34 | .pages 35 | +boardnav('logs', true, false) 36 | -------------------------------------------------------------------------------- /views/pages/news.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/newspost.pug 3 | 4 | block head 5 | title News 6 | 7 | block content 8 | h1.board-title News 9 | if news.length === 0 10 | p.text-center No news. 11 | else 12 | include ../includes/stickynav.pug 13 | each post in news 14 | +newspost(post) 15 | -------------------------------------------------------------------------------- /views/pages/register.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title Register 5 | 6 | block content 7 | h1.board-title Register 8 | .form-wrapper.flex-center.mv-10 9 | form.form-post(action='/forms/register' method='POST') 10 | .row 11 | .label Username 12 | input(type='text', name='username', maxlength='50' pattern='[a-zA-Z0-9]+' required title='alphanumeric only') 13 | .row 14 | .label Password 15 | input(type='password', name='password', maxlength='100' required) 16 | .row 17 | .label Confirm Password 18 | input(type='password', name='passwordconfirm', maxlength='100' required) 19 | .row 20 | .label Captcha 21 | span.col 22 | include ../includes/captcha.pug 23 | input(type='submit', value='Register') 24 | p: a(href='/login.html') Login 25 | p: a(href='/changepassword.html') Change Password 26 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process 4 | .on('uncaughtException', console.error) 5 | .on('unhandledRejection', console.error); 6 | 7 | const { debugLogs } = require(__dirname+'/configs/main.js') 8 | , Mongo = require(__dirname+'/db/db.js'); 9 | 10 | (async () => { 11 | 12 | debugLogs && console.log('CONNECTING TO MONGODB'); 13 | await Mongo.connect(); 14 | await Mongo.checkVersion(); 15 | 16 | const tasks = require(__dirname+'/helpers/tasks.js') 17 | , { queue } = require(__dirname+'/queue.js') 18 | 19 | queue 20 | .on('error', console.error) 21 | .on('failed', console.warn); 22 | 23 | queue.process(async job => { 24 | await tasks[job.data.task](job.data.options); 25 | return null; 26 | }); 27 | 28 | })(); 29 | 30 | 31 | --------------------------------------------------------------------------------