├── .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 |
--------------------------------------------------------------------------------