Merge branch 'dev'

merge-requests/208/head
Thomas Lynch 4 years ago
commit ec64faa1c7
  1. 3
      configs/main.js.example
  2. 3
      controllers/forms.js
  3. 50
      controllers/forms/addban.js
  4. 3
      controllers/pages.js
  5. 30
      db/boards.js
  6. 18
      db/posts.js
  7. 8
      gulp/res/css/style.css
  8. 43
      gulp/res/js/forms.js
  9. 49
      gulp/res/js/hide.js
  10. 15
      gulp/res/js/hover.js
  11. 35
      gulp/res/js/live.js
  12. 12
      gulp/res/js/localstorage.js
  13. 19
      gulp/res/js/uploaditem.js
  14. 133
      gulp/res/js/yous.js
  15. 3
      gulpfile.js
  16. 63
      helpers/addmodlogs.js
  17. 2
      helpers/paramconverter.js
  18. 1
      models/forms/actionhandler.js
  19. 69
      models/forms/addban.js
  20. 2
      models/forms/create.js
  21. 16
      models/forms/makepost.js
  22. 1
      models/pages/index.js
  23. 23
      models/pages/overboard.js
  24. 89
      package-lock.json
  25. 4
      package.json
  26. 4
      views/includes/actionfooter.pug
  27. 6
      views/includes/actionfooter_globalmanage.pug
  28. 156
      views/includes/actionfooter_manage.pug
  29. 22
      views/includes/addbanform.pug
  30. 2
      views/includes/captcha.pug
  31. 4
      views/includes/footer.pug
  32. 8
      views/includes/managebanform.pug
  33. 2
      views/includes/navbar.pug
  34. 13
      views/includes/postform.pug
  35. 2
      views/includes/uploaditem.pug
  36. 2
      views/mixins/catalogtile.pug
  37. 29
      views/mixins/modal.pug
  38. 4
      views/mixins/post.pug
  39. 14
      views/mixins/uploaditem.pug
  40. 5
      views/pages/boardlist.pug
  41. 2
      views/pages/create.pug
  42. 10
      views/pages/editpost.pug
  43. 6
      views/pages/globalmanagebans.pug
  44. 2
      views/pages/globalmanagenews.pug
  45. 2
      views/pages/globalmanagesettings.pug
  46. 6
      views/pages/managebans.pug
  47. 10
      views/pages/managesettings.pug
  48. 20
      views/pages/overboard.pug
  49. 2
      views/pages/thread.pug

@ -73,6 +73,9 @@ module.exports = {
editPost: 30, editPost: 30,
}, },
//how many threads to show on overboard
overboardLimit: 20,
//cache templates in memory. disable only if editing templates and doing dev work //cache templates in memory. disable only if editing templates and doing dev work
cacheTemplates: true, cacheTemplates: true,

@ -62,6 +62,7 @@ const express = require('express')
, appealController = require(__dirname+'/forms/appeal.js') , appealController = require(__dirname+'/forms/appeal.js')
, globalActionController = require(__dirname+'/forms/globalactions.js') , globalActionController = require(__dirname+'/forms/globalactions.js')
, actionController = require(__dirname+'/forms/actions.js') , actionController = require(__dirname+'/forms/actions.js')
, addBanController = require(__dirname+'/forms/addban.js')
, addNewsController = require(__dirname+'/forms/addnews.js') , addNewsController = require(__dirname+'/forms/addnews.js')
, deleteNewsController = require(__dirname+'/forms/deletenews.js') , deleteNewsController = require(__dirname+'/forms/deletenews.js')
, uploadBannersController = require(__dirname+'/forms/uploadbanners.js') , uploadBannersController = require(__dirname+'/forms/uploadbanners.js')
@ -100,11 +101,13 @@ router.post('/board/:board/transfer', processIp, sessionRefresh, csrf, Boards.ex
router.post('/board/:board/settings', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), paramConverter, boardSettingsController); router.post('/board/:board/settings', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), paramConverter, boardSettingsController);
router.post('/board/:board/addbanners', processIp, sessionRefresh, bannerFiles, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), paramConverter, numFiles, uploadBannersController); //add banners router.post('/board/:board/addbanners', processIp, sessionRefresh, bannerFiles, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), paramConverter, numFiles, uploadBannersController); //add banners
router.post('/board/:board/deletebanners', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), paramConverter, deleteBannersController); //delete banners router.post('/board/:board/deletebanners', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), paramConverter, deleteBannersController); //delete banners
router.post('/board/:board/addban', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(3), paramConverter, addBanController); //add ban manually without post
router.post('/board/:board/editbans', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(3), paramConverter, editBansController); //edit bans router.post('/board/:board/editbans', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(3), paramConverter, editBansController); //edit bans
router.post('/board/:board/deleteboard', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), deleteBoardController); //delete board router.post('/board/:board/deleteboard', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), deleteBoardController); //delete board
//global management forms //global management forms
router.post('/global/editbans', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(1), paramConverter, editBansController); //remove bans router.post('/global/editbans', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(1), paramConverter, editBansController); //remove bans
router.post('/global/addban', processIp, sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(1), paramConverter, addBanController); //add ban manually without post
router.post('/global/deleteboard', sessionRefresh, csrf, paramConverter, calcPerms, isLoggedIn, hasPerms(1), deleteBoardController); //delete board router.post('/global/deleteboard', sessionRefresh, csrf, paramConverter, calcPerms, isLoggedIn, hasPerms(1), deleteBoardController); //delete board
router.post('/global/addnews', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), addNewsController); //add new newspost router.post('/global/addnews', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), addNewsController); //add new newspost
router.post('/global/deletenews', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), paramConverter, deleteNewsController); //delete news router.post('/global/deletenews', sessionRefresh, csrf, calcPerms, isLoggedIn, hasPerms(0), paramConverter, deleteNewsController); //delete news

@ -0,0 +1,50 @@
'use strict';
const { globalLimits, ipHashPermLevel } = require(__dirname+'/../../configs/main.js')
, addBan = require(__dirname+'/../../models/forms/addban.js')
, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
, { isIP } = require('net');
module.exports = async (req, res, next) => {
const errors = [];
if (!req.body.ip || req.body.ip.length === 0) {
errors.push('Missing IP/hash input');
} else if (req.body.ip.length > 50) {
errors.push('IP/hash input must be less than 50 characters');
} else if (res.locals.permLevel > ipHashPermLevel && (isIP(req.body.ip) || req.body.ip.length !== 10)) {
errors.push('Invalid hash input');
}
if (req.body.ban_reason && req.body.ban_reason.length > globalLimits.fieldLength.ban_reason) {
errors.push(`Ban reason must be ${globalLimits.fieldLength.ban_reason} characters or less`);
}
if (req.body.log_message && req.body.log_message.length > globalLimits.fieldLength.log_message) {
errors.push(`Modlog message must be ${globalLimits.fieldLength.log_message} characters or less`);
}
let redirect = req.headers.referer;
if (!redirect) {
if (!req.params.board) {
redirect = '/globalmanage/bans.html';
} else {
redirect = `/${req.params.board}/manage/bans.html`;
}
}
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'errors': errors,
redirect,
});
}
try {
await addBan(req, res, redirect);
} catch (err) {
return next(err);
}
}

@ -19,7 +19,7 @@ const express = require('express')
, { globalManageSettings, globalManageReports, globalManageBans, , { globalManageSettings, globalManageReports, globalManageBans,
globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/') globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/')
, { changePassword, blockBypass, home, register, login, create, , { changePassword, blockBypass, home, register, login, create,
board, catalog, banners, randombanner, news, captchaPage, board, catalog, banners, randombanner, news, captchaPage, overboard,
captcha, thread, modlog, modloglist, account, boardlist } = require(__dirname+'/../models/pages/'); captcha, thread, modlog, modloglist, account, boardlist } = require(__dirname+'/../models/pages/');
//homepage //homepage
@ -38,6 +38,7 @@ router.get('/:board/catalog.html', Boards.exists, catalog); //catalog
router.get('/:board/logs.html', Boards.exists, modloglist);//modlog list router.get('/:board/logs.html', Boards.exists, modloglist);//modlog list
router.get('/:board/logs/:date(\\d{2}-\\d{2}-\\d{4}).html', Boards.exists, paramConverter, modlog); //daily log router.get('/:board/logs/:date(\\d{2}-\\d{2}-\\d{4}).html', Boards.exists, paramConverter, modlog); //daily log
router.get('/:board/banners.html', Boards.exists, banners); //banners router.get('/:board/banners.html', Boards.exists, banners); //banners
router.get('/all.html', overboard); //overboard
router.get('/create.html', sessionRefresh, isLoggedIn, create); //create new board router.get('/create.html', sessionRefresh, isLoggedIn, create); //create new board
router.get('/randombanner', randombanner); //random banner router.get('/randombanner', randombanner); //random banner

@ -51,17 +51,30 @@ module.exports = {
insertOne: (data) => { insertOne: (data) => {
cache.del(`board:${data._id}`); //removing cached no_exist cache.del(`board:${data._id}`); //removing cached no_exist
if (!data.settings.unlistedLocal) {
cache.sadd('boards:listed', data._id);
}
return db.insertOne(data); return db.insertOne(data);
}, },
deleteOne: (board) => { deleteOne: (board) => {
cache.del(`board:${board}`); cache.del(`board:${board}`);
cache.del(`banners:${board}`); cache.del(`banners:${board}`);
cache.srem('boards:listed', board);
cache.srem('triggered', board); cache.srem('triggered', board);
return db.deleteOne({ '_id': board }); return db.deleteOne({ '_id': board });
}, },
updateOne: (board, update) => { updateOne: (board, update) => {
if (update['$set']
&& update['$set'].settings
&& update['$set'].settings.unlistedLocal !== null) {
if (update['$set'].settings.unlistedLocal) {
cache.srem('boards:listed', board);
} else {
cache.sadd('boards:listed', board);
}
}
cache.del(`board:${board}`); cache.del(`board:${board}`);
return db.updateOne({ return db.updateOne({
'_id': board '_id': board
@ -102,6 +115,23 @@ module.exports = {
); );
}, },
getLocalListed: async () => {
let cachedListed = await cache.sgetall('boards:listed');
if (cachedListed) {
return cachedListed;
}
let listedBoards = await db.find({
'settings.unlistedLocal': false
}, {
'projection': {
'_id': 1,
}
});
listedBoards = listedBoards.map(b => b._id);
await cache.sadd('boards:listed', listedBoards);
return listedBoards;
},
boardSort: (skip=0, limit=50, sort={ ips:-1, pph:-1, sequence_value:-1 }, filter={}, showUnlisted=false) => { boardSort: (skip=0, limit=50, sort={ ips:-1, pph:-1, sequence_value:-1 }, filter={}, showUnlisted=false) => {
const addedFilter = {}; const addedFilter = {};
if (!showUnlisted) { if (!showUnlisted) {

@ -61,10 +61,20 @@ module.exports = {
if (!getSensitive) { if (!getSensitive) {
projection['ip'] = 0; projection['ip'] = 0;
} }
const threads = await db.find({ const threadsQuery = {
'thread': null, 'thread': null,
'board': board }
}, { if (board) {
if (Array.isArray(board)) {
//array for overboard
threadsQuery['board'] = {
'$in': board
}
} else {
threadsQuery['board'] = board;
}
}
const threads = await db.find(threadsQuery, {
projection projection
}).sort({ }).sort({
'sticky': -1, 'sticky': -1,
@ -76,7 +86,7 @@ module.exports = {
const previewRepliesLimit = thread.sticky ? stickyPreviewReplies : previewReplies; const previewRepliesLimit = thread.sticky ? stickyPreviewReplies : previewReplies;
const replies = previewRepliesLimit === 0 ? [] : await db.find({ const replies = previewRepliesLimit === 0 ? [] : await db.find({
'thread': thread.postId, 'thread': thread.postId,
'board': board 'board': thread.board
},{ },{
projection projection
}).sort({ }).sort({

@ -569,7 +569,6 @@ details.actions div {
.actions { .actions {
text-align: left; text-align: left;
max-width: 200px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 2px 0; margin: 2px 0;
@ -880,6 +879,13 @@ input:invalid, textarea:invalid {
font-weight: bold; font-weight: bold;
} }
.you:after {
margin-left: 3px;
content: '(You)';
font-weight: lighter;
font-style: italic;
}
.noselect { .noselect {
user-select: none; user-select: none;
} }

@ -174,13 +174,20 @@ class formHandler {
//todo: show success messages nicely for forms like actions (this doesnt apply to non file forms yet) //todo: show success messages nicely for forms like actions (this doesnt apply to non file forms yet)
} }
} else { } else {
if (json.postId) {
window.myPostId = json.postId;
}
if (json.redirect) {
const redirectBoard = json.redirect.split('/')[1];
const redirectPostId = json.redirect.split('#')[1];
appendLocalStorageArray('yous', `${redirectBoard}-${redirectPostId}`);
}
if (json.message || json.messages || json.error || json.errors) { if (json.message || json.messages || json.error || json.errors) {
doModal(json); doModal(json);
if (json.message === 'Incorrect captcha answer') { if (json.message === 'Incorrect captcha answer') {
//todo: create captcha form, add method to captcha frontend code //todo: create captcha form, add method to captcha frontend code
} }
} else if (socket && socket.connected) { } else if (socket && socket.connected) {
window.myPostId = json.postId;
window.location.hash = json.postId window.location.hash = json.postId
} else { } else {
if (!isThread) { if (!isThread) {
@ -254,37 +261,31 @@ class formHandler {
this.fileInput.removeAttribute('required'); this.fileInput.removeAttribute('required');
} }
this.files.push(file); this.files.push(file);
//add to upload list const item = {
const listElem = document.createElement('div'); spoilers: this.fileUploadList.dataset.spoilers === 'true',
listElem.classList.add('upload-item'); name: file.name
const thumb = document.createElement('img'); }
const name = document.createElement('p');
const remove = document.createElement('a');
name.textContent = file.name;
remove.textContent = 'X';
switch (file.type.split('/')[0]) { switch (file.type.split('/')[0]) {
case 'image': case 'image':
thumb.src = URL.createObjectURL(file); item.url = URL.createObjectURL(file);
break; break;
case 'audio': case 'audio':
thumb.src = '/file/audio.png' item.url = '/file/audio.png'
break; break;
case 'video': case 'video':
thumb.src = '/file/video.png' item.url = '/file/video.png'
break; break;
default: default:
thumb.src = '/file/attachment.png' item.url = '/file/attachment.png'
break; break;
} }
thumb.classList.add('upload-thumb'); const uploadItemHtml = uploaditem({ uploaditem: item });
remove.classList.add('close'); this.fileUploadList.insertAdjacentHTML('beforeend', uploadItemHtml);
listElem.appendChild(thumb); const fileElem = this.fileUploadList.lastChild;
listElem.appendChild(name); const lastClose = fileElem.querySelector('.close');
listElem.appendChild(remove); lastClose.addEventListener('click', () => {
remove.addEventListener('click', () => { this.removeFile(fileElem, file.name, file.size);
this.removeFile(listElem, file.name, file.size);
}) })
this.fileUploadList.appendChild(listElem);
this.fileUploadList.style.display = 'unset'; this.fileUploadList.style.display = 'unset';
} }

@ -8,6 +8,7 @@ if (fileInput) {
} }
let hidden; let hidden;
let hiddenPostsList;
const loadHiddenStorage = () => { const loadHiddenStorage = () => {
try { try {
const loaded = localStorage.getItem('hidden') const loaded = localStorage.getItem('hidden')
@ -74,7 +75,9 @@ const changeOption = function(e) {
} }
this.value = ''; this.value = '';
setHidden(posts, hiding); setHidden(posts, hiding);
setLocalStorage('hidden', JSON.stringify([...hidden])); const hiddenArray = [...hidden];
hiddenPostsList.value = hiddenArray.toString();
setLocalStorage('hidden', JSON.stringify(hiddenArray));
} }
for (let menu of document.getElementsByClassName('postmenu')) { for (let menu of document.getElementsByClassName('postmenu')) {
@ -85,23 +88,27 @@ for (let menu of document.getElementsByClassName('postmenu')) {
menu.addEventListener('change', changeOption, false); menu.addEventListener('change', changeOption, false);
} }
for (let elem of hidden) { const getHiddenElems = () => {
let posts = []; const posts = [];
if (elem.includes('-')) { for (let elem of hidden) {
const [board, postId] = elem.split('-'); if (elem.includes('-')) {
const post = document.querySelector(`[data-board="${board}"][data-post-id="${postId}"]`); const [board, postId] = elem.split('-');
if (post) { const post = document.querySelector(`[data-board="${board}"][data-post-id="${postId}"]`);
posts.push(post); if (post) {
} posts.push(post);
} else { }
const idPosts = document.querySelectorAll(`[data-user-id="${elem}"]`); } else {
if (idPosts && idPosts.length > 0) { const idPosts = document.querySelectorAll(`[data-user-id="${elem}"]`);
posts = idPosts; if (idPosts && idPosts.length > 0) {
posts = posts.concat(idPosts);
}
} }
} }
setHidden(posts, true); return posts;
} }
setHidden(getHiddenElems(), true);
const renderCSSLink = document.createElement('style'); const renderCSSLink = document.createElement('style');
renderCSSLink.type = 'text/css'; renderCSSLink.type = 'text/css';
renderCSSLink.id = 'rendercss'; renderCSSLink.id = 'rendercss';
@ -176,3 +183,17 @@ window.addEventListener('addPost', function(e) {
menu.value = ''; menu.value = '';
menu.addEventListener('change', changeOption, false); menu.addEventListener('change', changeOption, false);
}); });
window.addEventListener('settingsReady', function(e) {
hiddenPostsList = document.getElementById('hiddenpostslist-setting');
hiddenPostsList.value = [...hidden];
const hiddenPostsListClearButton = document.getElementById('hiddenpostslist-clear');
const clearhiddenPostsList = () => {
setHidden(getHiddenElems(), false);
hidden = new Set();
hiddenPostsList.value = '';
setLocalStorage('hidden', '[]');
console.log('cleared hidden posts');
}
hiddenPostsListClearButton.addEventListener('click', clearhiddenPostsList, false);
});

@ -114,6 +114,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
} }
if (json) { if (json) {
setLocalStorage(`hovercache-${jsonPath}`, JSON.stringify(json)); setLocalStorage(`hovercache-${jsonPath}`, JSON.stringify(json));
hoverCacheList.value = Object.keys(localStorage).filter(k => k.startsWith('hovercache'));
if (json.postId == hash) { if (json.postId == hash) {
postJson = json; postJson = json;
} else { } else {
@ -136,6 +137,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
//need this event so handlers like post hiding still apply to hover introduced posts //need this event so handlers like post hiding still apply to hover introduced posts
const newPostEvent = new CustomEvent('addPost', { const newPostEvent = new CustomEvent('addPost', {
detail: { detail: {
json: postJson,
post: hoveredPost, post: hoveredPost,
postId: postJson.postId, postId: postJson.postId,
hover: true hover: true
@ -173,3 +175,16 @@ window.addEventListener('DOMContentLoaded', (event) => {
}); });
window.addEventListener('settingsReady', function(e) {
hoverCacheList = document.getElementById('hovercachelist-setting');
hoverCacheList.value = Object.keys(localStorage).filter(k => k.startsWith('hovercache'));
const hoverCacheListClearButton = document.getElementById('hovercachelist-clear');
const clearHoverCacheList = () => {
deleteStartsWith('hovercache');
hoverCacheList.value = '';
console.log('cleared hover cache');
}
hoverCacheListClearButton.addEventListener('click', clearHoverCacheList, false);
});

@ -1,8 +1,6 @@
setDefaultLocalStorage('live', true); setDefaultLocalStorage('live', true);
setDefaultLocalStorage('notifications', false);
setDefaultLocalStorage('scroll', false); setDefaultLocalStorage('scroll', false);
let liveEnabled = localStorage.getItem('live') == 'true'; let liveEnabled = localStorage.getItem('live') == 'true';
let notificationsEnabled = localStorage.getItem('notifications') == 'true';
let scrollEnabled = localStorage.getItem('scroll') == 'true'; let scrollEnabled = localStorage.getItem('scroll') == 'true';
let socket; let socket;
let forceUpdate; let forceUpdate;
@ -63,17 +61,6 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
if (scrollEnabled) { if (scrollEnabled) {
newPostAnchor.scrollIntoView(); //scroll to post if enabled; newPostAnchor.scrollIntoView(); //scroll to post if enabled;
} }
if (notificationsEnabled) {
if (!window.myPostId || window.myPostId != postData.postId) {
const notifTitle = document.title;
const notifOptions = {
body: postData.nomarkup ? postData.nomarkup.substring(0,100) : ''
}
try {
new Notification(notifTitle, notifOptions);
} catch (e) { /* dont break when notification cant send for some reason */ }
}
}
const newPostEvent = new CustomEvent('addPost', { const newPostEvent = new CustomEvent('addPost', {
detail: { detail: {
post: newPost, post: newPost,
@ -82,7 +69,9 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
} }
}); });
//dispatch the event so quote click handlers, image expand, etc can be added in separate scripts by listening to the event //dispatch the event so quote click handlers, image expand, etc can be added in separate scripts by listening to the event
window.dispatchEvent(newPostEvent); setTimeout(() => {
window.dispatchEvent(newPostEvent);
}, 50);
} }
let jsonParts = window.location.pathname.replace(/\.html$/, '.json').split('/'); let jsonParts = window.location.pathname.replace(/\.html$/, '.json').split('/');
@ -225,24 +214,6 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
liveSetting.checked = liveEnabled; liveSetting.checked = liveEnabled;
liveSetting.addEventListener('change', toggleLive, false); liveSetting.addEventListener('change', toggleLive, false);
const notificationSetting = document.getElementById('notification-setting');
const toggleNotifications = async () => {
notificationsEnabled = !notificationsEnabled;
if (notificationsEnabled) {
const result = await Notification.requestPermission()
if (result != 'granted') {
//user denied permission popup
notificationsEnabled = false;
notificationSetting.checked = false;
return;
}
}
console.log('toggling notifications', notificationsEnabled);
setLocalStorage('notifications', notificationsEnabled);
}
notificationSetting.checked = notificationsEnabled;
notificationSetting.addEventListener('change', toggleNotifications, false);
const scrollSetting = document.getElementById('scroll-setting'); const scrollSetting = document.getElementById('scroll-setting');
const toggleScroll = () => { const toggleScroll = () => {
scrollEnabled = !scrollEnabled; scrollEnabled = !scrollEnabled;

@ -6,15 +6,21 @@ function setLocalStorage(key, value) {
try { try {
localStorage.setItem(key, value); localStorage.setItem(key, value);
} catch (e) { } catch (e) {
clearLocalStorageJunk(); deleteStartsWith();
} finally { } finally {
localStorage.setItem(key, value); localStorage.setItem(key, value);
} }
} }
function clearLocalStorageJunk() { function appendLocalStorageArray(key, value) {
const storedArray = JSON.parse(localStorage.getItem(key));
storedArray.push(value);
setLocalStorage(key, JSON.stringify(storedArray));
}
function deleteStartsWith(startString = 'hovercache') {
//clears hover cache when localstorage gets full //clears hover cache when localstorage gets full
const hoverCaches = Object.keys(localStorage).filter(k => k.startsWith('hovercache')); const hoverCaches = Object.keys(localStorage).filter(k => k.startsWith(startString));
for(let i = 0; i < hoverCaches.length; i++) { for(let i = 0; i < hoverCaches.length; i++) {
localStorage.removeItem(hoverCaches[i]); localStorage.removeItem(hoverCaches[i]);
} }

@ -0,0 +1,19 @@
function pug_attr(t,e,n,r){if(!1===e||null==e||!e&&("class"===t||"style"===t))return"";if(!0===e)return" "+(r?t:t+'="'+t+'"');var f=typeof e;return"object"!==f&&"function"!==f||"function"!=typeof e.toJSON||(e=e.toJSON()),"string"==typeof e||(e=JSON.stringify(e),n||-1===e.indexOf('"'))?(n&&(e=pug_escape(e))," "+t+'="'+e+'"'):" "+t+"='"+e.replace(/'/g,"&#39;")+"'"}
function pug_escape(e){var a=""+e,t=pug_match_html.exec(a);if(!t)return e;var r,c,n,s="";for(r=t.index,c=0;r<a.length;r++){switch(a.charCodeAt(r)){case 34:n="&quot;";break;case 38:n="&amp;";break;case 60:n="&lt;";break;case 62:n="&gt;";break;default:continue}c!==r&&(s+=a.substring(c,r)),c=r+1,s+=n}return c!==r?s+a.substring(c,r):s}
var pug_match_html=/["&<>]/;function uploaditem(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;
var locals_for_with = (locals || {});
(function (uploaditem) {
pug_mixins["uploaditem"] = pug_interp = function(item){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Cdiv\u003E\u003Cdiv class=\"upload-item\"\u003E\u003Cimg" + (" class=\"upload-thumb\""+pug_attr("src", item.url, true, false)) + "\u002F\u003E\u003Cp\u003E" + (pug_escape(null == (pug_interp = item.name) ? "" : pug_interp)) + "\u003C\u002Fp\u003E\u003Ca class=\"close\"\u003EX\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"row sb\"\u003E";
if (item.spoilers) {
pug_html = pug_html + "\u003Clabel\u003E\u003Cinput" + (" type=\"checkbox\" name=\"spoiler\""+pug_attr("value", item.name, true, false)) + "\u002F\u003ESpoiler\u003C\u002Flabel\u003E";
}
pug_html = pug_html + "\u003Clabel\u003E\u003Cinput" + (" type=\"checkbox\" name=\"strip_filename\""+pug_attr("value", item.name, true, false)) + "\u002F\u003EStrip Filename\u003C\u002Flabel\u003E\u003C\u002Fdiv\u003E\u003C\u002Fdiv\u003E";
};
pug_mixins["uploaditem"](uploaditem);
}.call(this, "uploaditem" in locals_for_with ?
locals_for_with.uploaditem :
typeof uploaditem !== 'undefined' ? uploaditem : undefined));
;;return pug_html;}

@ -0,0 +1,133 @@
setDefaultLocalStorage('notifications', false);
let notificationsEnabled = localStorage.getItem('notifications') == 'true';
setDefaultLocalStorage('notification-yous-only', false);
let notificationYousOnly = localStorage.getItem('notification-yous-only') == 'true';
setDefaultLocalStorage('yous-setting', true);
let yousEnabled = localStorage.getItem('yous-setting') == 'true';
setDefaultLocalStorage('yous', '[]');
let savedYous = new Set(JSON.parse(localStorage.getItem('yous')));
let yousList;
const toggleAll = (state) => savedYous.forEach(y => toggleOne(y, state));
const toggleQuotes = (quotes, state) => {
quotes.forEach(q => {
q.classList[state?'add':'remove']('you');
});
}
const toggleOne = (you, state) => {
const [board, postId] = you.split('-');
const post = document.querySelector(`[data-board="${board}"][data-post-id="${postId}"]`);
if (post) {
const postName = post.querySelector('.post-name');
if (postName) {
postName.classList[state?'add':'remove']('you');
}
}
const quotes = document.querySelectorAll(`.quote[href^="/${board}/"][href$="#${postId}"]`);
if (quotes) {
toggleQuotes(quotes, state);
}
}
if (yousEnabled) {
toggleAll(yousEnabled);
}
window.addEventListener('addPost', (e) => {
const postYou = `${e.detail.json.board}-${e.detail.postId}`;
const isYou = window.myPostId == e.detail.postId
if (isYou) {
//save you
savedYous.add(postYou);
const arrayYous = [...savedYous];
yousList.value = arrayYous.toString();
setLocalStorage('yous', JSON.stringify(arrayYous));
}
if (savedYous.has(postYou)) {
//toggle forn own post for name field
toggleOne(postYou, yousEnabled);
}
const quotesYou = e.detail.json.quotes
.map(q => `${e.detail.json.board}-${q.postId}`)
.filter(y => savedYous.has(y))
.length > 0;
const youHoverQuotes = e.detail.json.quotes
.concat(e.detail.json.backlinks)
.map(q => `${e.detail.json.board}-${q.postId}`)
.filter(y => savedYous.has(y))
.map(y => {
const [board, postId] = y.split('-');
return e.detail.post.querySelector(`.quote[href^="/${board}/"][href$="#${postId}"]`)
});
//toggle for any quotes in a new post that quote (you)
toggleQuotes(youHoverQuotes, yousEnabled);
//if not a hover newpost, and enabled/for yous, send notification
if (!e.detail.hover && notificationsEnabled && !isYou) {
if (notificationYousOnly && !quotesYou) {
return; //only send notif for (you) if setting
}
try {
console.log('attempting to send notification', postYou);
new Notification(`${quotesYou ? 'New quote in: ' : ''}${document.title}`, {
body: postData.nomarkup ? postData.nomarkup.substring(0,100) : ''
});
} catch (e) { /* notification cant send for some reason -- user revoked perms in browser? */ }
}
});
window.addEventListener('settingsReady', () => {
yousList = document.getElementById('youslist-setting');
yousList.value = [...savedYous];
const yousListClearButton = document.getElementById('youslist-clear');
const clearYousList = () => {
if (yousEnabled) {
toggleAll(false);
}
savedYous = new Set();
yousList.value = '';
setLocalStorage('yous', '[]');
console.log('cleared yous');
}
yousListClearButton.addEventListener('click', clearYousList, false);
const yousSetting = document.getElementById('yous-setting');
const toggleYousSetting = () => {
yousEnabled = !yousEnabled;
setLocalStorage('yous-setting', yousEnabled);
toggleAll(yousEnabled);
console.log('toggling yous', yousEnabled);
}
yousSetting.checked = yousEnabled;
yousSetting.addEventListener('change', toggleYousSetting, false);
const notificationYousOnlySetting = document.getElementById('notification-yous-only');
const toggleNotificationYousOnlySetting = () => {
notificationYousOnly = !notificationYousOnly;
setLocalStorage('notification-yous-only', notificationYousOnly);
console.log('toggling notification only for yous', yousEnabled);
}
notificationYousOnlySetting.checked = notificationYousOnly;
notificationYousOnlySetting.addEventListener('change', toggleNotificationYousOnlySetting, false);
const notificationSetting = document.getElementById('notification-setting');
const toggleNotifications = async () => {
notificationsEnabled = !notificationsEnabled;
if (notificationsEnabled) {
const result = await Notification.requestPermission()
if (result != 'granted') {
//user denied permission popup
notificationsEnabled = false;
notificationSetting.checked = false;
return;
}
}
console.log('toggling notifications', notificationsEnabled);
setLocalStorage('notifications', notificationsEnabled);
}
notificationSetting.checked = notificationsEnabled;
notificationSetting.addEventListener('change', toggleNotifications, false);
});

@ -197,6 +197,7 @@ function scripts() {
fs.writeFileSync('gulp/res/js/timezone.js', serverTimeZone); fs.writeFileSync('gulp/res/js/timezone.js', serverTimeZone);
fs.writeFileSync('gulp/res/js/post.js', pug.compileFileClient(`${paths.pug.src}/includes/post.pug`, { compileDebug: false, debug: false, name: 'post' })); fs.writeFileSync('gulp/res/js/post.js', pug.compileFileClient(`${paths.pug.src}/includes/post.pug`, { compileDebug: false, debug: false, name: 'post' }));
fs.writeFileSync('gulp/res/js/modal.js', pug.compileFileClient(`${paths.pug.src}/includes/modal.pug`, { compileDebug: false, debug: false, name: 'modal' })); fs.writeFileSync('gulp/res/js/modal.js', pug.compileFileClient(`${paths.pug.src}/includes/modal.pug`, { compileDebug: false, debug: false, name: 'modal' }));
fs.writeFileSync('gulp/res/js/uploaditem.js', pug.compileFileClient(`${paths.pug.src}/includes/uploaditem.pug`, { compileDebug: false, debug: false, name: 'uploaditem' }));
fs.symlinkSync(__dirname+'/node_modules/socket.io-client/dist/socket.io.slim.js', __dirname+'/gulp/res/js/socket.io.js', 'file'); fs.symlinkSync(__dirname+'/node_modules/socket.io-client/dist/socket.io.slim.js', __dirname+'/gulp/res/js/socket.io.js', 'file');
} catch (e) { } catch (e) {
if (e.code !== 'EEXIST') { if (e.code !== 'EEXIST') {
@ -217,6 +218,7 @@ function scripts() {
`${paths.scripts.src}/*.js`, `${paths.scripts.src}/*.js`,
`!${paths.scripts.src}/dragable.js`, `!${paths.scripts.src}/dragable.js`,
`!${paths.scripts.src}/hide.js`, `!${paths.scripts.src}/hide.js`,
`!${paths.scripts.src}/yous.js`,
`!${paths.scripts.src}/catalog.js`, `!${paths.scripts.src}/catalog.js`,
`!${paths.scripts.src}/time.js`, `!${paths.scripts.src}/time.js`,
]) ])
@ -225,6 +227,7 @@ function scripts() {
.pipe(gulp.dest(paths.scripts.dest)); .pipe(gulp.dest(paths.scripts.dest));
return gulp.src([ return gulp.src([
`${paths.scripts.src}/dragable.js`, `${paths.scripts.src}/dragable.js`,
`${paths.scripts.src}/yous.js`,
`${paths.scripts.src}/hide.js`, `${paths.scripts.src}/hide.js`,
`${paths.scripts.src}/catalog.js`, `${paths.scripts.src}/catalog.js`,
`${paths.scripts.src}/time.js`, `${paths.scripts.src}/time.js`,

@ -0,0 +1,63 @@
'use strict';
//modlog
if (modlogActions.length > 0) {
const modlog = {};
const logDate = new Date(); //all events current date
const message = req.body.log_message || null;
let logUser;
if (res.locals.permLevel < 4) { //if staff
logUser = req.session.user.username;
} else {
logUser = 'Unregistered User';
}
for (let i = 0; i < res.locals.posts.length; i++) {
const post = res.locals.posts[i];
if (!modlog[post.board]) {
//per board actions, all actions combined to one event
modlog[post.board] = {
postIds: [],
actions: modlogActions,
date: logDate,
showUser: !req.body.hide_name || logUser === 'Unregistered User' ? true : false,
message: message,
user: logUser,
ip: {
single: res.locals.ip.single,
raw: res.locals.ip.raw
}
};
}
//push each post id
modlog[post.board].postIds.push(post.postId);
}
const modlogDocuments = [];
for (let i = 0; i < threadBoards.length; i++) {
const boardName = threadBoards[i];
const boardLog = modlog[boardName];
//make it into documents for the db
modlogDocuments.push({
...boardLog,
'board': boardName
});
}
if (modlogDocuments.length > 0) {
//insert the modlog docs
await Modlogs.insertMany(modlogDocuments);
for (let i = 0; i < threadBoards.length; i++) {
const board = buildBoards[threadBoards[i]];
buildQueue.push({
'task': 'buildModLog',
'options': {
'board': board,
}
});
buildQueue.push({
'task': 'buildModLogList',
'options': {
'board': board,
}
});
}
}
}

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const { ObjectId } = require(__dirname+'/../db/db.js') const { ObjectId } = require(__dirname+'/../db/db.js')
, allowedArrays = new Set(['checkednews', 'checkedposts', 'globalcheckedposts', , allowedArrays = new Set(['checkednews', 'checkedposts', 'globalcheckedposts', 'spoiler', 'strip_filename',
'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts', 'countries']) //only these should be arrays, since express bodyparser can output arrays 'checkedreports', 'checkedbans', 'checkedbanners', 'checkedaccounts', 'countries']) //only these should be arrays, since express bodyparser can output arrays
, trimFields = ['tags', 'uri', 'moderators', 'filters', 'announcement', 'description', 'message', , trimFields = ['tags', 'uri', 'moderators', 'filters', 'announcement', 'description', 'message',
'name', 'subject', 'email', 'postpassword', 'password', 'default_name', 'report_reason', 'ban_reason', 'log_message', 'custom_css'] //trim if we dont want filed with whitespace 'name', 'subject', 'email', 'postpassword', 'password', 'default_name', 'report_reason', 'ban_reason', 'log_message', 'custom_css'] //trim if we dont want filed with whitespace

@ -269,6 +269,7 @@ module.exports = async (req, res, next) => {
} }
const parallelPromises = []; const parallelPromises = [];
//modlog //modlog
if (modlogActions.length > 0) { if (modlogActions.length > 0) {
const modlog = {}; const modlog = {};

@ -0,0 +1,69 @@
'use strict';
const { Bans, Modlogs } = require(__dirname+'/../../db/')
, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
, hashIp = require(__dirname+'/../../helpers/dynamic.js')
, buildQueue = require(__dirname+'/../../queue.js')
, { isIP } = require('net')
, { ipHashPermLevel, defaultBanDuration } = require(__dirname+'/../../configs/main.js');
module.exports = async (req, res, redirect) => {
const actionDate = new Date();
const banPromise = Bans.insertOne({
//note: raw ip and type single because of
'type': 'single',
'ip': {
'single': isIP(req.body.ip) ? hashIp(req.body.ip) : req.body.ip,
'raw': req.body.ip,
},
'reason': req.body.ban_reason || req.body.log_message || 'No reason specified',
'board': req.params.board || null,
'posts': null,
'issuer': req.session.user.username,
'date': actionDate,
'expireAt': new Date(actionDate.getTime() + (req.body.ban_duration || defaultBanDuration)),
'allowAppeal': req.body.no_appeal ? false : true,
'appeal': null,
'seen': false,
});
const modlogPromise = Modlogs.insertOne({
'board': req.params.board || null,
'postIds': [],
'actions': [(req.params.board ? 'Ban' : 'Global Ban')],
'date': actionDate,
'showUser': !req.body.hide_name || res.locals.permLevel >= 4 ? true : false,
'message': req.body.log_message || null,
'user': res.locals.permLevel < 4 ? req.session.user.username : 'Unregistered User',
'ip': {
'single': res.locals.ip.single,
'raw': res.locals.ip.raw
}
});
await Promise.all([banPromise, modlogPromise]);
if (req.params.board) {
buildQueue.push({
'task': 'buildModLog',
'options': {
'board': res.locals.board,
}
});
buildQueue.push({
'task': 'buildModLogList',
'options': {
'board': res.locals.board,
}
});
}
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Added ban',
redirect,
});
}

@ -3,7 +3,7 @@
const { Boards, Accounts } = require(__dirname+'/../../db/') const { Boards, Accounts } = require(__dirname+'/../../db/')
, dynamicResponse = require(__dirname+'/../../helpers/dynamic.js') , dynamicResponse = require(__dirname+'/../../helpers/dynamic.js')
, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js') , uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
, restrictedURIs = new Set(['captcha', 'forms', 'randombanner']) , restrictedURIs = new Set(['captcha', 'forms', 'randombanner', 'all'])
, { ensureDir } = require('fs-extra') , { ensureDir } = require('fs-extra')
, { boardDefaults } = require(__dirname+'/../../configs/main.js'); , { boardDefaults } = require(__dirname+'/../../configs/main.js');

@ -178,12 +178,13 @@ module.exports = async (req, res, next) => {
//get metadata //get metadata
let processedFile = { let processedFile = {
hash: file.sha256, spoiler: (res.locals.permLevel >= 4 || userPostSpoiler) && req.body.spoiler && req.body.spoiler.includes(file.name),
filename: file.filename, //could probably remove since we have hash and extension hash: file.sha256,
originalFilename: file.name, filename: file.filename, //could probably remove since we have hash and extension
mimetype: file.mimetype, originalFilename: req.body.strip_filename && req.body.strip_filename.includes(file.name) ? file.filename : file.name,
size: file.size, mimetype: file.mimetype,
extension, size: file.size,
extension,
}; };
//type and subtype //type and subtype
@ -321,9 +322,8 @@ module.exports = async (req, res, next) => {
password = createHash('sha256').update(postPasswordSecret + req.body.postpassword).digest('base64'); password = createHash('sha256').update(postPasswordSecret + req.body.postpassword).digest('base64');
} }
//spoiler files only if board settings allow //spoiler files only if board settings allow
const spoiler = userPostSpoiler && req.body.spoiler ? true : false; const spoiler = (res.locals.permLevel >= 4 || userPostSpoiler) && req.body.spoiler_all ? true : false;
//forceanon hide reply subjects so cant be used as name for replies //forceanon hide reply subjects so cant be used as name for replies
//forceanon and sageonlyemail only allow sage email //forceanon and sageonlyemail only allow sage email

@ -19,4 +19,5 @@ module.exports = {
modlog: require(__dirname+'/modlog.js'), modlog: require(__dirname+'/modlog.js'),
modloglist: require(__dirname+'/modloglist.js'), modloglist: require(__dirname+'/modloglist.js'),
boardlist: require(__dirname+'/boardlist.js'), boardlist: require(__dirname+'/boardlist.js'),
overboard: require(__dirname+'/overboard.js'),
} }

@ -0,0 +1,23 @@
'use strict';
const { Posts, Boards } = require(__dirname+'/../../db/')
, cache = require(__dirname+'/../../redis.js')
, { overboardLimit } = require(__dirname+'/../../configs/main.js');
module.exports = async (req, res, next) => {
let threads = [];
try {
const listedBoards = await Boards.getLocalListed();
threads = await Posts.getRecent(listedBoards, 1, overboardLimit, false);
} catch (err) {
return next(err);
}
res
.set('Cache-Control', 'public, max-age=60')
.render('overboard', {
threads,
});
}

89
package-lock.json generated

@ -1001,7 +1001,8 @@
}, },
"kind-of": { "kind-of": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "" "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
} }
} }
}, },
@ -1730,6 +1731,26 @@
"moment-timezone": "^0.5.25" "moment-timezone": "^0.5.25"
} }
}, },
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"dependencies": {
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"csrf": { "csrf": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
@ -2526,8 +2547,8 @@
} }
}, },
"express-fileupload": { "express-fileupload": {
"version": "github:fatchan/express-fileupload#ecc5ad4f41771a1c23eed365e451220b9cc3e3c1", "version": "git+https://gitgud.io/fatchan/express-fileupload.git#9c5ff44438308ea1417abf22b82bc6e1b95cd284",
"from": "github:fatchan/express-fileupload", "from": "git+https://gitgud.io/fatchan/express-fileupload.git",
"requires": { "requires": {
"busboy": "^0.3.1" "busboy": "^0.3.1"
} }
@ -2647,7 +2668,8 @@
}, },
"kind-of": { "kind-of": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "" "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
} }
} }
}, },
@ -3735,8 +3757,8 @@
} }
}, },
"gm": { "gm": {
"version": "github:fatchan/gm#c2ffb2ce0db3f64fbf4082462601429985b6dca6", "version": "git+https://gitgud.io/fatchan/gm.git#b22827491ec6b00c4345a2052a49e24bca90c4c0",
"from": "github:fatchan/gm", "from": "git+https://gitgud.io/fatchan/gm.git",
"requires": { "requires": {
"array-parallel": "^0.1.3", "array-parallel": "^0.1.3",
"array-series": "^0.1.5", "array-series": "^0.1.5",
@ -3745,16 +3767,6 @@
"tmp": "^0.2.1" "tmp": "^0.2.1"
}, },
"dependencies": { "dependencies": {
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"debug": { "debug": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@ -3767,30 +3779,6 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
},
"tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"requires": {
"rimraf": "^3.0.0"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"requires": {
"isexe": "^2.0.0"
}
} }
} }
}, },
@ -7451,7 +7439,8 @@
}, },
"kind-of": { "kind-of": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "" "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
} }
} }
}, },
@ -7997,6 +7986,24 @@
"resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
"integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM="
}, },
"tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"requires": {
"rimraf": "^3.0.0"
},
"dependencies": {
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
}
}
},
"to-absolute-glob": { "to-absolute-glob": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",

@ -14,12 +14,12 @@
"del": "^5.1.0", "del": "^5.1.0",
"dnsbl": "^3.2.0", "dnsbl": "^3.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "github:fatchan/express-fileupload", "express-fileupload": "git+https://gitgud.io/fatchan/express-fileupload.git",
"express-session": "^1.17.0", "express-session": "^1.17.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"fs": "0.0.1-security", "fs": "0.0.1-security",
"fs-extra": "^9.0.0", "fs-extra": "^9.0.0",
"gm": "github:fatchan/gm", "gm": "git+https://gitgud.io/fatchan/gm.git",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-clean-css": "^4.3.0", "gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",

@ -12,7 +12,7 @@ details.toggle-label#actionform
input.post-check(type='checkbox', name='spoiler' value='1') input.post-check(type='checkbox', name='spoiler' value='1')
| Spoiler Files | Spoiler Files
label label
input#password(type='password', name='postpassword', placeholder='post password' autocomplete='off') input#password(type='password', name='postpassword', placeholder='Post password' autocomplete='off')
label label
input.post-check(type='checkbox', name='report' value='1') input.post-check(type='checkbox', name='report' value='1')
| Report | Report
@ -20,7 +20,7 @@ details.toggle-label#actionform
input.post-check(type='checkbox', name='global_report' value='1') input.post-check(type='checkbox', name='global_report' value='1')
| Global Report | Global Report
label label
input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off') input#report(type='text', name='report_reason', placeholder='Report reason' autocomplete='off')
.actions .actions
h4.no-m-p Captcha: h4.no-m-p Captcha:
include ./captcha.pug include ./captcha.pug

@ -42,9 +42,9 @@ details.toggle-label#actionform
input.post-check(type='checkbox', name='hide_name' value='1') input.post-check(type='checkbox', name='hide_name' value='1')
| Hide Username In Modlog | Hide Username In Modlog
label label
input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') input(type='text', name='ban_reason', placeholder='Ban reason' autocomplete='off')
label label
input(type='text', name='ban_duration', placeholder='ban duration e.g. 7d' autocomplete='off') input(type='text', name='ban_duration', placeholder='Ban duration e.g. 7d' autocomplete='off')
label label
input(type='text', name='log_message', placeholder='modlog message' autocomplete='off') input(type='text', name='log_message', placeholder='Modlog message' autocomplete='off')
input(type='submit', value='submit') input(type='submit', value='submit')

@ -2,80 +2,84 @@ details.toggle-label#actionform
summary.toggle-summary Show Post Actions summary.toggle-summary Show Post Actions
.actions .actions
h4.no-m-p Actions: h4.no-m-p Actions:
label .row.wrap
input.post-check(type='checkbox', name='delete' value='1') .col.mr-5
| Delete Posts label
label input.post-check(type='checkbox', name='delete' value='1')
input.post-check(type='checkbox', name='delete_file' value='1') | Delete Posts
| Delete Files label
label input.post-check(type='checkbox', name='delete_file' value='1')
input.post-check(type='checkbox', name='spoiler' value='1') | Delete Files
| Spoiler Files label
label input.post-check(type='checkbox', name='delete_ip_thread' value='1')
input.post-check(type='checkbox', name='edit' value='1') | Delete from IP in thread
| Edit Post label
label input.post-check(type='checkbox', name='delete_ip_board' value='1')
input.post-check(type='checkbox', name='global_report' value='1') | Delete from IP on board
| Global Report label
label input.post-check(type='checkbox', name='delete_ip_global' value='1')
input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off') | Delete from IP globally
label label
input.post-check(type='checkbox', name='delete_ip_thread' value='1') input.post-check(type='checkbox', name='spoiler' value='1')
| Delete from IP in thread | Spoiler Files
label label
input.post-check(type='checkbox', name='delete_ip_board' value='1') input.post-check(type='checkbox', name='dismiss' value='1')
| Delete from IP on board | Dismiss Reports
label label
input.post-check(type='checkbox', name='delete_ip_global' value='1') input.post-check(type='checkbox', name='global_report' value='1')
| Delete from IP globally | Global Report
label label
input.post-check(type='checkbox', name='dismiss' value='1') input#report(type='text', name='report_reason', placeholder='Report reason' autocomplete='off')
| Dismiss Reports label
label input.post-check(type='checkbox', name='move' value='1')
input.post-check(type='checkbox', name='report_ban' value='1') | Move
| Ban Reporters label
label input(type='number', name='move_to_thread', placeholder='Destination thread No.' autocomplete='off')
input.post-check(type='checkbox', name='ban' value='1') .col.mr-5
| Ban Poster label
label input.post-check(type='checkbox', name='report_ban' value='1')
input.post-check(type='checkbox', name='global_ban' value='1') | Ban Reporters
| Global Ban Poster label
label input.post-check(type='checkbox', name='ban' value='1')
input.post-check(type='checkbox', name='ban_q' value='1') | Ban Poster
| 1/4 Range label
label input.post-check(type='checkbox', name='global_ban' value='1')
input.post-check(type='checkbox', name='ban_h' value='1') | Global Ban Poster
| 1/2 Range label
label input.post-check(type='checkbox', name='ban_q' value='1')
input.post-check(type='checkbox', name='no_appeal' value='1') | 1/4 Range
| Non-appealable Ban label
label input.post-check(type='checkbox', name='ban_h' value='1')
input.post-check(type='checkbox', name='preserve_post' value='1') | 1/2 Range
| Show Post In Ban label
label input.post-check(type='checkbox', name='no_appeal' value='1')
input.post-check(type='checkbox', name='hide_name' value='1') | Non-appealable Ban
| Hide Username In Modlog label
label input.post-check(type='checkbox', name='preserve_post' value='1')
input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') | Show Post In Ban
label label
input(type='text', name='ban_duration', placeholder='ban duration e.g. 7d' autocomplete='off') input.post-check(type='checkbox', name='hide_name' value='1')
label | Hide Username In Modlog
input(type='text', name='log_message', placeholder='modlog message' autocomplete='off') label
label input(type='text', name='ban_reason', placeholder='Ban reason' autocomplete='off')
input.post-check(type='checkbox', name='move' value='1') label
| Move input(type='text', name='ban_duration', placeholder='Ban duration e.g. 7d' autocomplete='off')
label label
input(type='number', name='move_to_thread', placeholder='destination thread No.' autocomplete='off') input(type='text', name='log_message', placeholder='Modlog message' autocomplete='off')
label .col
input.post-check(type='checkbox', name='sticky' value='1') label
| Toggle Sticky input.post-check(type='checkbox', name='edit' value='1')
label | Edit Post
input.post-check(type='checkbox', name='lock' value='1') label
| Toggle Lock input.post-check(type='checkbox', name='sticky' value='1')
label | Toggle Sticky
input.post-check(type='checkbox', name='bumplock' value='1') label
| Toggle Bumplock input.post-check(type='checkbox', name='lock' value='1')
label | Toggle Lock
input.post-check(type='checkbox', name='cyclic' value='1') label
| Toggle Cycle input.post-check(type='checkbox', name='bumplock' value='1')
| Toggle Bumplock
label
input.post-check(type='checkbox', name='cyclic' value='1')
| Toggle Cycle
input(type='submit', value='submit') input(type='submit', value='submit')

@ -0,0 +1,22 @@
.row
.label IP/Hash
input(type='text' name='ip' required)
.row
.label Ban Reason
input(type='text' name='ban_reason')
.row
.label Modlog Message
input(type='text' name='log_message')
.row
.label Ban Duration
input(type='text' name='ban_duration' placeholder='e.g. 7d')
.row
.label Non-appealable Ban
label.postform-style.ph-5
input(type='checkbox', name='no_appeal' value='1')
.row
.label Hide Username In Modlog
label.postform-style.ph-5
input(type='checkbox', name='hide_name' value='1')
input(type='submit', value='submit')

@ -1,4 +1,4 @@
noscript.no-m-p noscript.no-m-p
iframe.captcha(src='/captcha.html' 'width=210' height='80' scrolling='no' loading='lazy') iframe.captcha(src='/captcha.html' 'width=210' height='80' scrolling='no' loading='lazy')
.jsonly.captcha(style='display:none;') .jsonly.captcha(style='display:none;')
input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='captcha text' pattern=".{6}" required title='6 characters') input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='Captcha text' pattern=".{6}" required title='6 characters')

@ -1,10 +1,12 @@
unless minimal unless minimal
small.footer#bottom small.footer#bottom
| - | -
a(href='/news.html') news
| -
a(href='/rules.html') rules a(href='/rules.html') rules
| - | -
a(href='/faq.html') faq a(href='/faq.html') faq
| - | -
a(href='https://github.com/fatchan/jschan/') source code a(href='https://gitgud.io/fatchan/jschan/') source code
| - | -
script(src=`/js/render.js?v=${commit}`) script(src=`/js/render.js?v=${commit}`)

@ -7,12 +7,12 @@ else
+ban(ban) +ban(ban)
.action-wrapper.mv-10 .action-wrapper.mv-10
.row .row
label .label Unban
label.postform-style.ph-5
input(type='radio' name='option' value='unban' checked='checked') input(type='radio' name='option' value='unban' checked='checked')
| Unban
.row .row
label .label Deny Appeal
label.postform-style.ph-5
input(type='radio' name='option' value='deny_appeal') input(type='radio' name='option' value='deny_appeal')
| Deny Appeal
input(type='submit' value='submit') input(type='submit' value='submit')

@ -1,7 +1,7 @@
unless minimal unless minimal
nav.navbar nav.navbar
a.nav-item(href='/index.html') Home a.nav-item(href='/index.html') Home
a.nav-item(href='/news.html') News //a.nav-item(href='/all.html') Overboard
a.nav-item(href='/boards.html' style=(enableWebring ? 'line-height: 1.5em' : null)) a.nav-item(href='/boards.html' style=(enableWebring ? 'line-height: 1.5em' : null))
| Boards | Boards
if enableWebring if enableWebring

@ -26,7 +26,7 @@ section.form-wrapper.flex-center
.label Sage .label Sage
label.postform-style.ph-5 label.postform-style.ph-5
input(type='checkbox', name='email', value='sage') input(type='checkbox', name='email', value='sage')
else else
section.row section.row
.label Email .label Email
input(type='text', name='email', autocomplete='off' maxlength=globalLimits.fieldLength.email) input(type='text', name='email', autocomplete='off' maxlength=globalLimits.fieldLength.email)
@ -54,15 +54,16 @@ section.form-wrapper.flex-center
span.col span.col
include ./filelabel.pug include ./filelabel.pug
input#file(type='file', name='file' multiple required=fileRequired ) input#file(type='file', name='file' multiple required=fileRequired )
.upload-list .upload-list(data-spoilers=(board.settings.userPostSpoiler ? 'true' : 'false'))
if board.settings.userPostSpoiler if board.settings.userPostSpoiler
label.postform-style.ph-5.ml-1.fh noscript
input(type='checkbox', name='spoiler', value='true') label.postform-style.ph-5.ml-1.fh
| Spoiler input(type='checkbox', name='spoiler_all', value='true')
| Spoiler
if board.settings.userPostSpoiler || board.settings.userPostDelete || board.settings.userPostUnlink || modview if board.settings.userPostSpoiler || board.settings.userPostDelete || board.settings.userPostUnlink || modview
section.row section.row
.label Password .label Password
input(type='password', name='postpassword', placeholder='password to delete/spoiler/unlink later' maxlength='50') input(type='password', name='postpassword', placeholder='Password to delete/spoiler/unlink later' maxlength='50')
if ((board.settings.captchaMode === 1 && !isThread) || board.settings.captchaMode === 2) && !modview if ((board.settings.captchaMode === 1 && !isThread) || board.settings.captchaMode === 2) && !modview
section.row section.row
.label .label

@ -0,0 +1,2 @@
include ../mixins/uploaditem.pug
+uploaditem(uploaditem)

@ -22,7 +22,7 @@ mixin catalogtile(board, post, index)
.post-file-src .post-file-src
a(href=postURL) a(href=postURL)
- const file = post.files[0] - const file = post.files[0]
if post.spoiler if post.spoiler || file.spoiler
div.spoilerimg.catalog-thumb div.spoilerimg.catalog-thumb
else if file.attachment else if file.attachment
div.attachmentimg.catalog-thumb div.attachmentimg.catalog-thumb

@ -39,6 +39,10 @@ mixin modal(data)
label.postform-style.ph-5 label.postform-style.ph-5
input#notification-setting(type='checkbox') input#notification-setting(type='checkbox')
.rlabel Notifications .rlabel Notifications
.row
label.postform-style.ph-5
input#notification-yous-only(type='checkbox')
.rlabel Only notify (You)s
.row .row
label.postform-style.ph-5 label.postform-style.ph-5
input#scroll-setting(type='checkbox') input#scroll-setting(type='checkbox')
@ -69,7 +73,7 @@ mixin modal(data)
.row .row
label.postform-style.ph-5 label.postform-style.ph-5
input#hiderecursive-setting(type='checkbox') input#hiderecursive-setting(type='checkbox')
.rlabel Recursive Post Hide .rlabel Recursive post hide
.row .row
label.postform-style.ph-5 label.postform-style.ph-5
input#loop-setting(type='checkbox') input#loop-setting(type='checkbox')
@ -90,16 +94,31 @@ mixin modal(data)
label.postform-style.ph-5 label.postform-style.ph-5
input#alwaysshowspoilers-setting(type='checkbox') input#alwaysshowspoilers-setting(type='checkbox')
.rlabel Always reveal spoilers .rlabel Always reveal spoilers
.row
label.postform-style.ph-5
input#yous-setting(type='checkbox')
.rlabel Show (You)s
.row
.label (You)s
input.mr-1#youslist-setting(type='text' readonly)
input#youslist-clear(type='button' value='Clear')
.row
.label Hidden posts
input.mr-1#hiddenpostslist-setting(type='text' readonly)
input#hiddenpostslist-clear(type='button' value='Clear')
.row
.label Hover Cache
input.mr-1#hovercachelist-setting(type='text' readonly)
input#hovercachelist-clear(type='button' value='Clear')
.row .row
.label Video/Audio Volume .label Video/Audio volume
label.postform-style.ph-5 label.postform-style.ph-5
input#volume-setting(type='range' min='0' max='100') input#volume-setting(type='range' min='0' max='100')
.row .row
.label Post Password .label Post password
input#postpassword-setting(type='password' name='postpassword') input#postpassword-setting(type='password' name='postpassword')
.row .row
.label Default Name .label Default name
input#name-setting(type='text' name='name') input#name-setting(type='text' name='name')
.row .row
.label Theme .label Theme

@ -63,7 +63,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
each file in post.files each file in post.files
.post-file .post-file
span.post-file-info span.post-file-info
span: a(href='/file/'+file.filename title='Download '+file.originalFilename download=file.originalFilename) #{post.spoiler ? 'Spoiler File' : file.originalFilename} span: a(href='/file/'+file.filename title='Download '+file.originalFilename download=file.originalFilename) #{post.spoiler || file.spoiler ? 'Spoiler File' : file.originalFilename}
br br
span span
| (#{file.sizeString} | (#{file.sizeString}
@ -75,7 +75,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
- const type = file.mimetype.split('/')[0] - const type = file.mimetype.split('/')[0]
.post-file-src(data-type=type data-attachment=(file.attachment ? "true" : "false")) .post-file-src(data-type=type data-attachment=(file.attachment ? "true" : "false"))
a(target='_blank' href=`/file/${file.filename}`) a(target='_blank' href=`/file/${file.filename}`)
if post.spoiler if post.spoiler || file.spoiler
div.spoilerimg.file-thumb div.spoilerimg.file-thumb
else if file.attachment else if file.attachment
div.attachmentimg.file-thumb div.attachmentimg.file-thumb

@ -0,0 +1,14 @@
mixin uploaditem(item)
div
.upload-item
img.upload-thumb(src=item.url)
p #{item.name}
a.close X
.row.sb
if item.spoilers
label
input(type='checkbox', name='spoiler', value=item.name)
| Spoiler
label
input(type='checkbox', name='strip_filename', value=item.name)
| Strip Filename

@ -5,12 +5,15 @@ block head
block content block content
h1.board-title Board List h1.board-title Board List
h4.board-description
| or try the
a(href='/all.html') overboard
.flexcenter.mv-10 .flexcenter.mv-10
form.form-post(action=`/boards.html` method='GET') form.form-post(action=`/boards.html` method='GET')
input(type='hidden' value=page) input(type='hidden' value=page)
.row .row
.label Search .label Search
input(type='text' name='search' value=search placeholder='uri or tags') input(type='text' name='search' value=search placeholder='Uri or tags')
.row .row
.label Sort .label Sort
select(name='sort') select(name='sort')

@ -18,7 +18,7 @@ block content
input(type='text', name='description', maxlength=globalLimits.fieldLength.description required) input(type='text', name='description', maxlength=globalLimits.fieldLength.description required)
.row .row
.label Tags .label Tags
textarea(name='tags' placeholder='newline separated, max 10') textarea(name='tags' placeholder='Newline separated, max 10')
.row .row
.label Captcha .label Captcha
span.col span.col

@ -18,9 +18,9 @@ block content
label label
if !post.thread if !post.thread
include ../includes/posticons.pug include ../includes/posticons.pug
input.edit.post-subject(value=post.subject placeholder='subject' type='text' name='subject' maxlength=globalLimits.fieldLength.subject) input.edit.post-subject(value=post.subject placeholder='Subject' type='text' name='subject' maxlength=globalLimits.fieldLength.subject)
input.edit.post-name(value=post.email type='text' name='email' placeholder='email' maxlength=globalLimits.fieldLength.email) input.edit.post-name(value=post.email type='text' name='email' placeholder='Email' maxlength=globalLimits.fieldLength.email)
input.edit.post-name(type='text' name='name' placeholder='name' maxlength=globalLimits.fieldLength.name) input.edit.post-name(type='text' name='name' placeholder='Name' maxlength=globalLimits.fieldLength.name)
if post.country && post.country.code if post.country && post.country.code
span(class=`flag flag-${post.country.code.toLowerCase()}` title=post.country.name alt=post.country.name) span(class=`flag flag-${post.country.code.toLowerCase()}` title=post.country.name alt=post.country.name)
| |
@ -45,7 +45,7 @@ block content
span.noselect: a(href=`${postURL}#postform`) [Reply] span.noselect: a(href=`${postURL}#postform`) [Reply]
.post-data .post-data
pre.post-message pre.post-message
textarea.edit.fw(name='message' rows='15' placeholder='message') #{post.nomarkup} textarea.edit.fw(name='message' rows='15' placeholder='Message') #{post.nomarkup}
if post.banmessage if post.banmessage
p.ban p.ban
span.message USER WAS BANNED FOR THIS POST span.message USER WAS BANNED FOR THIS POST
@ -55,5 +55,5 @@ block content
input.post-check(type='checkbox', name='hide_name' value='1') input.post-check(type='checkbox', name='hide_name' value='1')
| Hide Username | Hide Username
label.mv-5 label.mv-5
input(type='text', name='log_message', placeholder='modlog message' autocomplete='off') input(type='text', name='log_message', placeholder='Modlog message' autocomplete='off')
input(type='submit', value='save') input(type='submit', value='save')

@ -10,6 +10,12 @@ block content
br br
+globalmanagenav('bans') +globalmanagenav('bans')
hr(size=1) hr(size=1)
h4.no-m-p Add Ban:
.form-wrapper.flexleft
form.form-post(action=`/forms/global/addban`, enctype='application/x-www-form-urlencoded', method='POST')
input(type='hidden' name='_csrf' value=csrf)
include ../includes/addbanform.pug
hr(size=1)
h4.no-m-p Global Bans & Appeals: h4.no-m-p Global Bans & Appeals:
form(action=`/forms/global/editbans` method='POST' enctype='application/x-www-form-urlencoded') form(action=`/forms/global/editbans` method='POST' enctype='application/x-www-form-urlencoded')
include ../includes/managebanform.pug include ../includes/managebanform.pug

@ -19,7 +19,7 @@ block content
input(type='text' name='title' required) input(type='text' name='title' required)
.row .row
.label Message .label Message
textarea(name='message' rows='10' placeholder='supports post styling' required) textarea(name='message' rows='10' placeholder='Supports post styling' required)
input(type='submit', value='submit') input(type='submit', value='submit')
if news.length > 0 if news.length > 0
hr(size=1) hr(size=1)

@ -28,7 +28,7 @@ block content
input(type='hidden' name='_csrf' value=csrf) input(type='hidden' name='_csrf' value=csrf)
.row .row
.label Filters .label Filters
textarea(name='filters' placeholder='newline separated, max 50') #{settings.filters.join('\n')} textarea(name='filters' placeholder='Newline separated, max 50') #{settings.filters.join('\n')}
.row .row
.label Filter Mode .label Filter Mode
select(name='filter_mode') select(name='filter_mode')

@ -11,6 +11,12 @@ block content
br br
+managenav('bans') +managenav('bans')
hr(size=1) hr(size=1)
h4.no-m-p Add Ban:
.form-wrapper.flexleft
form.form-post(action=`/forms/board/${board._id}/addban`, enctype='application/x-www-form-urlencoded', method='POST')
input(type='hidden' name='_csrf' value=csrf)
include ../includes/addbanform.pug
hr(size=1)
h4.no-m-p Bans & Appeals: h4.no-m-p Bans & Appeals:
form(action=`/forms/board/${board._id}/editbans` method='POST' enctype='application/x-www-form-urlencoded') form(action=`/forms/board/${board._id}/editbans` method='POST' enctype='application/x-www-form-urlencoded')
include ../includes/managebanform.pug include ../includes/managebanform.pug

@ -48,13 +48,13 @@ block content
input(type='text' name='description' value=board.settings.description) input(type='text' name='description' value=board.settings.description)
.row .row
.label Tags .label Tags
textarea(name='tags' placeholder='newline separated, max 10') #{board.settings.tags.join('\n')} textarea(name='tags' placeholder='Newline separated, max 10') #{board.settings.tags.join('\n')}
.row .row
.label Moderators .label Moderators
textarea(name='moderators' placeholder='newline separated, max 10') #{board.settings.moderators.join('\n')} textarea(name='moderators' placeholder='Newline separated, max 10') #{board.settings.moderators.join('\n')}
.row .row
.label Announcement .label Announcement
textarea(name='announcement' placeholder='supports post styling') #{board.settings.announcement.raw} textarea(name='announcement' placeholder='Supports post styling') #{board.settings.announcement.raw}
.row .row
.label Anon Name .label Anon Name
input(type='text' name='default_name' value=board.settings.defaultName) input(type='text' name='default_name' value=board.settings.defaultName)
@ -187,7 +187,7 @@ block content
if globalLimits.customCss.enabled if globalLimits.customCss.enabled
.row .row
.label Custom CSS .label Custom CSS
textarea(name='custom_css' placeholder='test first in top-right settings if you have javascript enabled' maxlength=globalLimits.customCss.max) #{board.settings.customCss} textarea(name='custom_css' placeholder='Test first in top-right settings if you have javascript enabled' maxlength=globalLimits.customCss.max) #{board.settings.customCss}
.row .row
.label Captcha Mode .label Captcha Mode
select(name='captcha_mode') select(name='captcha_mode')
@ -221,7 +221,7 @@ block content
include ../includes/2charisocountries.pug include ../includes/2charisocountries.pug
.row .row
.label Filters .label Filters
textarea(name='filters' placeholder='newline separated, max 50') #{board.settings.filters.join('\n')} textarea(name='filters' placeholder='Newline separated, max 50') #{board.settings.filters.join('\n')}
.row .row
.label Strict Filtering .label Strict Filtering
label.postform-style.ph-5 label.postform-style.ph-5

@ -0,0 +1,20 @@
extends ../layout.pug
include ../mixins/post.pug
block head
title Overboard
block content
.board-header
h1.board-title Overboard
h4.board-description Recently bumped threads from all listed boards
hr(size=1)
if threads.length === 0
p No posts.
hr(size=1)
for thread in threads
.thread
+post(thread, true)
for post in thread.replies
+post(post, true)
hr(size=1)

@ -13,7 +13,7 @@ block head
meta(property='og:url', content=meta.url) meta(property='og:url', content=meta.url)
meta(property='og:description', content=thread.nomarkup) meta(property='og:description', content=thread.nomarkup)
if thread.files.length > 0 if thread.files.length > 0
if thread.spoiler if thread.spoiler || thread.files[0].spoiler
meta(property='og:image', content='/file/spoiler.png') meta(property='og:image', content='/file/spoiler.png')
else else
- const file = thread.files[0]; - const file = thread.files[0];

Loading…
Cancel
Save