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,
},
//how many threads to show on overboard
overboardLimit: 20,
//cache templates in memory. disable only if editing templates and doing dev work
cacheTemplates: true,

@ -62,6 +62,7 @@ const express = require('express')
, appealController = require(__dirname+'/forms/appeal.js')
, globalActionController = require(__dirname+'/forms/globalactions.js')
, actionController = require(__dirname+'/forms/actions.js')
, addBanController = require(__dirname+'/forms/addban.js')
, addNewsController = require(__dirname+'/forms/addnews.js')
, deleteNewsController = require(__dirname+'/forms/deletenews.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/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/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/deleteboard', processIp, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn, hasPerms(2), deleteBoardController); //delete board
//global management forms
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/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

@ -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,
globalManageRecent, globalManageAccounts, globalManageNews, globalManageLogs } = require(__dirname+'/../models/pages/globalmanage/')
, { 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/');
//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/: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('/all.html', overboard); //overboard
router.get('/create.html', sessionRefresh, isLoggedIn, create); //create new board
router.get('/randombanner', randombanner); //random banner

@ -51,17 +51,30 @@ module.exports = {
insertOne: (data) => {
cache.del(`board:${data._id}`); //removing cached no_exist
if (!data.settings.unlistedLocal) {
cache.sadd('boards:listed', data._id);
}
return db.insertOne(data);
},
deleteOne: (board) => {
cache.del(`board:${board}`);
cache.del(`banners:${board}`);
cache.srem('boards:listed', board);
cache.srem('triggered', board);
return db.deleteOne({ '_id': board });
},
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}`);
return db.updateOne({
'_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) => {
const addedFilter = {};
if (!showUnlisted) {

@ -61,10 +61,20 @@ module.exports = {
if (!getSensitive) {
projection['ip'] = 0;
}
const threads = await db.find({
const threadsQuery = {
'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
}).sort({
'sticky': -1,
@ -76,7 +86,7 @@ module.exports = {
const previewRepliesLimit = thread.sticky ? stickyPreviewReplies : previewReplies;
const replies = previewRepliesLimit === 0 ? [] : await db.find({
'thread': thread.postId,
'board': board
'board': thread.board
},{
projection
}).sort({

@ -569,7 +569,6 @@ details.actions div {
.actions {
text-align: left;
max-width: 200px;
display: flex;
flex-direction: column;
margin: 2px 0;
@ -880,6 +879,13 @@ input:invalid, textarea:invalid {
font-weight: bold;
}
.you:after {
margin-left: 3px;
content: '(You)';
font-weight: lighter;
font-style: italic;
}
.noselect {
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)
}
} 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) {
doModal(json);
if (json.message === 'Incorrect captcha answer') {
//todo: create captcha form, add method to captcha frontend code
}
} else if (socket && socket.connected) {
window.myPostId = json.postId;
window.location.hash = json.postId
} else {
if (!isThread) {
@ -254,37 +261,31 @@ class formHandler {
this.fileInput.removeAttribute('required');
}
this.files.push(file);
//add to upload list
const listElem = document.createElement('div');
listElem.classList.add('upload-item');
const thumb = document.createElement('img');
const name = document.createElement('p');
const remove = document.createElement('a');
name.textContent = file.name;
remove.textContent = 'X';
const item = {
spoilers: this.fileUploadList.dataset.spoilers === 'true',
name: file.name
}
switch (file.type.split('/')[0]) {
case 'image':
thumb.src = URL.createObjectURL(file);
item.url = URL.createObjectURL(file);
break;
case 'audio':
thumb.src = '/file/audio.png'
item.url = '/file/audio.png'
break;
case 'video':
thumb.src = '/file/video.png'
item.url = '/file/video.png'
break;
default:
thumb.src = '/file/attachment.png'
item.url = '/file/attachment.png'
break;
}
thumb.classList.add('upload-thumb');
remove.classList.add('close');
listElem.appendChild(thumb);
listElem.appendChild(name);
listElem.appendChild(remove);
remove.addEventListener('click', () => {
this.removeFile(listElem, file.name, file.size);
const uploadItemHtml = uploaditem({ uploaditem: item });
this.fileUploadList.insertAdjacentHTML('beforeend', uploadItemHtml);
const fileElem = this.fileUploadList.lastChild;
const lastClose = fileElem.querySelector('.close');
lastClose.addEventListener('click', () => {
this.removeFile(fileElem, file.name, file.size);
})
this.fileUploadList.appendChild(listElem);
this.fileUploadList.style.display = 'unset';
}

@ -8,6 +8,7 @@ if (fileInput) {
}
let hidden;
let hiddenPostsList;
const loadHiddenStorage = () => {
try {
const loaded = localStorage.getItem('hidden')
@ -74,7 +75,9 @@ const changeOption = function(e) {
}
this.value = '';
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')) {
@ -85,23 +88,27 @@ for (let menu of document.getElementsByClassName('postmenu')) {
menu.addEventListener('change', changeOption, false);
}
for (let elem of hidden) {
let posts = [];
if (elem.includes('-')) {
const [board, postId] = elem.split('-');
const post = document.querySelector(`[data-board="${board}"][data-post-id="${postId}"]`);
if (post) {
posts.push(post);
}
} else {
const idPosts = document.querySelectorAll(`[data-user-id="${elem}"]`);
if (idPosts && idPosts.length > 0) {
posts = idPosts;
const getHiddenElems = () => {
const posts = [];
for (let elem of hidden) {
if (elem.includes('-')) {
const [board, postId] = elem.split('-');
const post = document.querySelector(`[data-board="${board}"][data-post-id="${postId}"]`);
if (post) {
posts.push(post);
}
} else {
const idPosts = document.querySelectorAll(`[data-user-id="${elem}"]`);
if (idPosts && idPosts.length > 0) {
posts = posts.concat(idPosts);
}
}
}
setHidden(posts, true);
return posts;
}
setHidden(getHiddenElems(), true);
const renderCSSLink = document.createElement('style');
renderCSSLink.type = 'text/css';
renderCSSLink.id = 'rendercss';
@ -176,3 +183,17 @@ window.addEventListener('addPost', function(e) {
menu.value = '';
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) {
setLocalStorage(`hovercache-${jsonPath}`, JSON.stringify(json));
hoverCacheList.value = Object.keys(localStorage).filter(k => k.startsWith('hovercache'));
if (json.postId == hash) {
postJson = json;
} else {
@ -136,6 +137,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
//need this event so handlers like post hiding still apply to hover introduced posts
const newPostEvent = new CustomEvent('addPost', {
detail: {
json: postJson,
post: hoveredPost,
postId: postJson.postId,
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('notifications', false);
setDefaultLocalStorage('scroll', false);
let liveEnabled = localStorage.getItem('live') == 'true';
let notificationsEnabled = localStorage.getItem('notifications') == 'true';
let scrollEnabled = localStorage.getItem('scroll') == 'true';
let socket;
let forceUpdate;
@ -63,17 +61,6 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
if (scrollEnabled) {
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', {
detail: {
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
window.dispatchEvent(newPostEvent);
setTimeout(() => {
window.dispatchEvent(newPostEvent);
}, 50);
}
let jsonParts = window.location.pathname.replace(/\.html$/, '.json').split('/');
@ -225,24 +214,6 @@ window.addEventListener('settingsReady', function(event) { //after domcontentloa
liveSetting.checked = liveEnabled;
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 toggleScroll = () => {
scrollEnabled = !scrollEnabled;

@ -6,15 +6,21 @@ function setLocalStorage(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {
clearLocalStorageJunk();
deleteStartsWith();
} finally {
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
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++) {
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/post.js', pug.compileFileClient(`${paths.pug.src}/includes/post.pug`, { compileDebug: false, debug: false, name: 'post' }));
fs.writeFileSync('gulp/res/js/modal.js', pug.compileFileClient(`${paths.pug.src}/includes/modal.pug`, { compileDebug: false, debug: false, name: 'modal' }));
fs.writeFileSync('gulp/res/js/uploaditem.js', pug.compileFileClient(`${paths.pug.src}/includes/uploaditem.pug`, { compileDebug: false, debug: false, name: 'uploaditem' }));
fs.symlinkSync(__dirname+'/node_modules/socket.io-client/dist/socket.io.slim.js', __dirname+'/gulp/res/js/socket.io.js', 'file');
} catch (e) {
if (e.code !== 'EEXIST') {
@ -217,6 +218,7 @@ function scripts() {
`${paths.scripts.src}/*.js`,
`!${paths.scripts.src}/dragable.js`,
`!${paths.scripts.src}/hide.js`,
`!${paths.scripts.src}/yous.js`,
`!${paths.scripts.src}/catalog.js`,
`!${paths.scripts.src}/time.js`,
])
@ -225,6 +227,7 @@ function scripts() {
.pipe(gulp.dest(paths.scripts.dest));
return gulp.src([
`${paths.scripts.src}/dragable.js`,
`${paths.scripts.src}/yous.js`,
`${paths.scripts.src}/hide.js`,
`${paths.scripts.src}/catalog.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';
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
, 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

@ -269,6 +269,7 @@ module.exports = async (req, res, next) => {
}
const parallelPromises = [];
//modlog
if (modlogActions.length > 0) {
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/')
, dynamicResponse = require(__dirname+'/../../helpers/dynamic.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')
, { boardDefaults } = require(__dirname+'/../../configs/main.js');

@ -178,12 +178,13 @@ module.exports = async (req, res, next) => {
//get metadata
let processedFile = {
hash: file.sha256,
filename: file.filename, //could probably remove since we have hash and extension
originalFilename: file.name,
mimetype: file.mimetype,
size: file.size,
extension,
spoiler: (res.locals.permLevel >= 4 || userPostSpoiler) && req.body.spoiler && req.body.spoiler.includes(file.name),
hash: file.sha256,
filename: file.filename, //could probably remove since we have hash and extension
originalFilename: req.body.strip_filename && req.body.strip_filename.includes(file.name) ? file.filename : file.name,
mimetype: file.mimetype,
size: file.size,
extension,
};
//type and subtype
@ -321,9 +322,8 @@ module.exports = async (req, res, next) => {
password = createHash('sha256').update(postPasswordSecret + req.body.postpassword).digest('base64');
}
//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 and sageonlyemail only allow sage email

@ -19,4 +19,5 @@ module.exports = {
modlog: require(__dirname+'/modlog.js'),
modloglist: require(__dirname+'/modloglist.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": {
"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"
}
},
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
@ -2526,8 +2547,8 @@
}
},
"express-fileupload": {
"version": "github:fatchan/express-fileupload#ecc5ad4f41771a1c23eed365e451220b9cc3e3c1",
"from": "github:fatchan/express-fileupload",
"version": "git+https://gitgud.io/fatchan/express-fileupload.git#9c5ff44438308ea1417abf22b82bc6e1b95cd284",
"from": "git+https://gitgud.io/fatchan/express-fileupload.git",
"requires": {
"busboy": "^0.3.1"
}
@ -2647,7 +2668,8 @@
},
"kind-of": {
"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": {
"version": "github:fatchan/gm#c2ffb2ce0db3f64fbf4082462601429985b6dca6",
"from": "github:fatchan/gm",
"version": "git+https://gitgud.io/fatchan/gm.git#b22827491ec6b00c4345a2052a49e24bca90c4c0",
"from": "git+https://gitgud.io/fatchan/gm.git",
"requires": {
"array-parallel": "^0.1.3",
"array-series": "^0.1.5",
@ -3745,16 +3767,6 @@
"tmp": "^0.2.1"
},
"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": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@ -3767,30 +3779,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"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": {
"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",
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",

@ -14,12 +14,12 @@
"del": "^5.1.0",
"dnsbl": "^3.2.0",
"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",
"fluent-ffmpeg": "^2.1.2",
"fs": "0.0.1-security",
"fs-extra": "^9.0.0",
"gm": "github:fatchan/gm",
"gm": "git+https://gitgud.io/fatchan/gm.git",
"gulp": "^4.0.2",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",

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

@ -42,9 +42,9 @@ details.toggle-label#actionform
input.post-check(type='checkbox', name='hide_name' value='1')
| Hide Username In Modlog
label
input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off')
input(type='text', name='ban_reason', placeholder='Ban reason' autocomplete='off')
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
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')

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

@ -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
iframe.captcha(src='/captcha.html' 'width=210' height='80' scrolling='no' loading='lazy')
.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
small.footer#bottom
| -
a(href='/news.html') news
| -
a(href='/rules.html') rules
| -
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}`)

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

@ -1,7 +1,7 @@
unless minimal
nav.navbar
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))
| Boards
if enableWebring

@ -26,7 +26,7 @@ section.form-wrapper.flex-center
.label Sage
label.postform-style.ph-5
input(type='checkbox', name='email', value='sage')
else
else
section.row
.label Email
input(type='text', name='email', autocomplete='off' maxlength=globalLimits.fieldLength.email)
@ -54,15 +54,16 @@ section.form-wrapper.flex-center
span.col
include ./filelabel.pug
input#file(type='file', name='file' multiple required=fileRequired )
.upload-list
.upload-list(data-spoilers=(board.settings.userPostSpoiler ? 'true' : 'false'))
if board.settings.userPostSpoiler
label.postform-style.ph-5.ml-1.fh
input(type='checkbox', name='spoiler', value='true')
| Spoiler
noscript
label.postform-style.ph-5.ml-1.fh
input(type='checkbox', name='spoiler_all', value='true')
| Spoiler
if board.settings.userPostSpoiler || board.settings.userPostDelete || board.settings.userPostUnlink || modview
section.row
.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
section.row
.label

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

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

@ -39,6 +39,10 @@ mixin modal(data)
label.postform-style.ph-5
input#notification-setting(type='checkbox')
.rlabel Notifications
.row
label.postform-style.ph-5
input#notification-yous-only(type='checkbox')
.rlabel Only notify (You)s
.row
label.postform-style.ph-5
input#scroll-setting(type='checkbox')
@ -69,7 +73,7 @@ mixin modal(data)
.row
label.postform-style.ph-5
input#hiderecursive-setting(type='checkbox')
.rlabel Recursive Post Hide
.rlabel Recursive post hide
.row
label.postform-style.ph-5
input#loop-setting(type='checkbox')
@ -90,16 +94,31 @@ mixin modal(data)
label.postform-style.ph-5
input#alwaysshowspoilers-setting(type='checkbox')
.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
.label Video/Audio Volume
.label Video/Audio volume
label.postform-style.ph-5
input#volume-setting(type='range' min='0' max='100')
.row
.label Post Password
.label Post password
input#postpassword-setting(type='password' name='postpassword')
.row
.label Default Name
.label Default name
input#name-setting(type='text' name='name')
.row
.label Theme

@ -63,7 +63,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
each file in post.files
.post-file
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
span
| (#{file.sizeString}
@ -75,7 +75,7 @@ mixin post(post, truncate, manage=false, globalmanage=false, ban=false)
- const type = file.mimetype.split('/')[0]
.post-file-src(data-type=type data-attachment=(file.attachment ? "true" : "false"))
a(target='_blank' href=`/file/${file.filename}`)
if post.spoiler
if post.spoiler || file.spoiler
div.spoilerimg.file-thumb
else if file.attachment
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
h1.board-title Board List
h4.board-description
| or try the
a(href='/all.html') overboard
.flexcenter.mv-10
form.form-post(action=`/boards.html` method='GET')
input(type='hidden' value=page)
.row
.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
.label Sort
select(name='sort')

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

@ -18,9 +18,9 @@ block content
label
if !post.thread
include ../includes/posticons.pug
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(type='text' name='name' placeholder='name' maxlength=globalLimits.fieldLength.name)
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(type='text' name='name' placeholder='Name' maxlength=globalLimits.fieldLength.name)
if post.country && post.country.code
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]
.post-data
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
p.ban
span.message USER WAS BANNED FOR THIS POST
@ -55,5 +55,5 @@ block content
input.post-check(type='checkbox', name='hide_name' value='1')
| Hide Username
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')

@ -10,6 +10,12 @@ block content
br
+globalmanagenav('bans')
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:
form(action=`/forms/global/editbans` method='POST' enctype='application/x-www-form-urlencoded')
include ../includes/managebanform.pug

@ -19,7 +19,7 @@ block content
input(type='text' name='title' required)
.row
.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')
if news.length > 0
hr(size=1)

@ -28,7 +28,7 @@ block content
input(type='hidden' name='_csrf' value=csrf)
.row
.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
.label Filter Mode
select(name='filter_mode')

@ -11,6 +11,12 @@ block content
br
+managenav('bans')
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:
form(action=`/forms/board/${board._id}/editbans` method='POST' enctype='application/x-www-form-urlencoded')
include ../includes/managebanform.pug

@ -48,13 +48,13 @@ block content
input(type='text' name='description' value=board.settings.description)
.row
.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
.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
.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
.label Anon Name
input(type='text' name='default_name' value=board.settings.defaultName)
@ -187,7 +187,7 @@ block content
if globalLimits.customCss.enabled
.row
.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
.label Captcha Mode
select(name='captcha_mode')
@ -221,7 +221,7 @@ block content
include ../includes/2charisocountries.pug
.row
.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
.label Strict Filtering
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:description', content=thread.nomarkup)
if thread.files.length > 0
if thread.spoiler
if thread.spoiler || thread.files[0].spoiler
meta(property='og:image', content='/file/spoiler.png')
else
- const file = thread.files[0];

Loading…
Cancel
Save