diff --git a/configs/main.js.example b/configs/main.js.example index 0cdef370..8c25b075 100644 --- a/configs/main.js.example +++ b/configs/main.js.example @@ -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, diff --git a/controllers/forms.js b/controllers/forms.js index 6ad8484a..b0c69d0a 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -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 diff --git a/controllers/forms/addban.js b/controllers/forms/addban.js new file mode 100644 index 00000000..d32080d6 --- /dev/null +++ b/controllers/forms/addban.js @@ -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); + } + +} + diff --git a/controllers/pages.js b/controllers/pages.js index e616ff51..0faacfd9 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -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 diff --git a/db/boards.js b/db/boards.js index c61faff9..1c7824cc 100644 --- a/db/boards.js +++ b/db/boards.js @@ -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) { diff --git a/db/posts.js b/db/posts.js index 59d44cfb..36816865 100644 --- a/db/posts.js +++ b/db/posts.js @@ -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({ diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index e09787b5..6466fc34 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -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; } diff --git a/gulp/res/js/forms.js b/gulp/res/js/forms.js index 8bc921bc..c4e74633 100644 --- a/gulp/res/js/forms.js +++ b/gulp/res/js/forms.js @@ -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'; } diff --git a/gulp/res/js/hide.js b/gulp/res/js/hide.js index f0450c4c..e9be2a65 100644 --- a/gulp/res/js/hide.js +++ b/gulp/res/js/hide.js @@ -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); +}); diff --git a/gulp/res/js/hover.js b/gulp/res/js/hover.js index 79a6d143..64d9f7de 100644 --- a/gulp/res/js/hover.js +++ b/gulp/res/js/hover.js @@ -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); +}); + diff --git a/gulp/res/js/live.js b/gulp/res/js/live.js index 3da8db48..a6615e09 100644 --- a/gulp/res/js/live.js +++ b/gulp/res/js/live.js @@ -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; diff --git a/gulp/res/js/localstorage.js b/gulp/res/js/localstorage.js index a6a94c12..8d4743fe 100644 --- a/gulp/res/js/localstorage.js +++ b/gulp/res/js/localstorage.js @@ -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]); } diff --git a/gulp/res/js/uploaditem.js b/gulp/res/js/uploaditem.js new file mode 100644 index 00000000..a6b81e81 --- /dev/null +++ b/gulp/res/js/uploaditem.js @@ -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,"'")+"'"} +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]/;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;} \ No newline at end of file diff --git a/gulp/res/js/yous.js b/gulp/res/js/yous.js new file mode 100644 index 00000000..d4e7ad8e --- /dev/null +++ b/gulp/res/js/yous.js @@ -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); + +}); diff --git a/gulpfile.js b/gulpfile.js index c9a1959c..6e24cdf3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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`, diff --git a/helpers/addmodlogs.js b/helpers/addmodlogs.js new file mode 100644 index 00000000..260b1b58 --- /dev/null +++ b/helpers/addmodlogs.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, + } + }); + } + } + } diff --git a/helpers/paramconverter.js b/helpers/paramconverter.js index 93d633fc..17e1cbad 100644 --- a/helpers/paramconverter.js +++ b/helpers/paramconverter.js @@ -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 diff --git a/models/forms/actionhandler.js b/models/forms/actionhandler.js index d74eb44e..4bf5e6dd 100644 --- a/models/forms/actionhandler.js +++ b/models/forms/actionhandler.js @@ -269,6 +269,7 @@ module.exports = async (req, res, next) => { } const parallelPromises = []; + //modlog if (modlogActions.length > 0) { const modlog = {}; diff --git a/models/forms/addban.js b/models/forms/addban.js new file mode 100644 index 00000000..66f80971 --- /dev/null +++ b/models/forms/addban.js @@ -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, + }); + +} diff --git a/models/forms/create.js b/models/forms/create.js index 63e8cd21..641c2410 100644 --- a/models/forms/create.js +++ b/models/forms/create.js @@ -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'); diff --git a/models/forms/makepost.js b/models/forms/makepost.js index fba6e79b..bf7c7f59 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.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 diff --git a/models/pages/index.js b/models/pages/index.js index 70bb2e30..db80f61f 100644 --- a/models/pages/index.js +++ b/models/pages/index.js @@ -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'), } diff --git a/models/pages/overboard.js b/models/pages/overboard.js new file mode 100644 index 00000000..a8e0dde8 --- /dev/null +++ b/models/pages/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, + }); + +} diff --git a/package-lock.json b/package-lock.json index a7f2f527..75ec9fad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 75ae6eb3..62086322 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/views/includes/actionfooter.pug b/views/includes/actionfooter.pug index c4bdcf5e..b4a89d2e 100644 --- a/views/includes/actionfooter.pug +++ b/views/includes/actionfooter.pug @@ -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 diff --git a/views/includes/actionfooter_globalmanage.pug b/views/includes/actionfooter_globalmanage.pug index 77275d3b..8f60a192 100644 --- a/views/includes/actionfooter_globalmanage.pug +++ b/views/includes/actionfooter_globalmanage.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') diff --git a/views/includes/actionfooter_manage.pug b/views/includes/actionfooter_manage.pug index 10848e74..e3a983a3 100644 --- a/views/includes/actionfooter_manage.pug +++ b/views/includes/actionfooter_manage.pug @@ -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') diff --git a/views/includes/addbanform.pug b/views/includes/addbanform.pug new file mode 100644 index 00000000..465b25b4 --- /dev/null +++ b/views/includes/addbanform.pug @@ -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') + diff --git a/views/includes/captcha.pug b/views/includes/captcha.pug index 9563f9b3..f41ce371 100644 --- a/views/includes/captcha.pug +++ b/views/includes/captcha.pug @@ -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') diff --git a/views/includes/footer.pug b/views/includes/footer.pug index 70ba3793..7c99b659 100644 --- a/views/includes/footer.pug +++ b/views/includes/footer.pug @@ -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}`) diff --git a/views/includes/managebanform.pug b/views/includes/managebanform.pug index 0597f414..e293cb74 100644 --- a/views/includes/managebanform.pug +++ b/views/includes/managebanform.pug @@ -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') diff --git a/views/includes/navbar.pug b/views/includes/navbar.pug index 9c124e6e..33dc5884 100644 --- a/views/includes/navbar.pug +++ b/views/includes/navbar.pug @@ -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 diff --git a/views/includes/postform.pug b/views/includes/postform.pug index d50125e4..390e6281 100644 --- a/views/includes/postform.pug +++ b/views/includes/postform.pug @@ -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 diff --git a/views/includes/uploaditem.pug b/views/includes/uploaditem.pug new file mode 100644 index 00000000..03cebd7b --- /dev/null +++ b/views/includes/uploaditem.pug @@ -0,0 +1,2 @@ +include ../mixins/uploaditem.pug ++uploaditem(uploaditem) diff --git a/views/mixins/catalogtile.pug b/views/mixins/catalogtile.pug index 7c038453..5b1287c4 100644 --- a/views/mixins/catalogtile.pug +++ b/views/mixins/catalogtile.pug @@ -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 diff --git a/views/mixins/modal.pug b/views/mixins/modal.pug index 5b96cccf..a8fb480b 100644 --- a/views/mixins/modal.pug +++ b/views/mixins/modal.pug @@ -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 diff --git a/views/mixins/post.pug b/views/mixins/post.pug index a8b7e732..b2f6cd9a 100644 --- a/views/mixins/post.pug +++ b/views/mixins/post.pug @@ -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 diff --git a/views/mixins/uploaditem.pug b/views/mixins/uploaditem.pug new file mode 100644 index 00000000..2c298198 --- /dev/null +++ b/views/mixins/uploaditem.pug @@ -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 diff --git a/views/pages/boardlist.pug b/views/pages/boardlist.pug index 0bc2b13a..d105dd3c 100644 --- a/views/pages/boardlist.pug +++ b/views/pages/boardlist.pug @@ -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') diff --git a/views/pages/create.pug b/views/pages/create.pug index 4ee8eede..33655850 100644 --- a/views/pages/create.pug +++ b/views/pages/create.pug @@ -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 diff --git a/views/pages/editpost.pug b/views/pages/editpost.pug index d088440d..92017c25 100644 --- a/views/pages/editpost.pug +++ b/views/pages/editpost.pug @@ -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') diff --git a/views/pages/globalmanagebans.pug b/views/pages/globalmanagebans.pug index 63aa9cc1..97d2e611 100644 --- a/views/pages/globalmanagebans.pug +++ b/views/pages/globalmanagebans.pug @@ -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 diff --git a/views/pages/globalmanagenews.pug b/views/pages/globalmanagenews.pug index 1b78f314..312ade63 100644 --- a/views/pages/globalmanagenews.pug +++ b/views/pages/globalmanagenews.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) diff --git a/views/pages/globalmanagesettings.pug b/views/pages/globalmanagesettings.pug index 1e3b2850..30fac5ce 100644 --- a/views/pages/globalmanagesettings.pug +++ b/views/pages/globalmanagesettings.pug @@ -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') diff --git a/views/pages/managebans.pug b/views/pages/managebans.pug index 282fc917..e4d1130d 100644 --- a/views/pages/managebans.pug +++ b/views/pages/managebans.pug @@ -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 diff --git a/views/pages/managesettings.pug b/views/pages/managesettings.pug index 2d76895e..60557fd0 100644 --- a/views/pages/managesettings.pug +++ b/views/pages/managesettings.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 diff --git a/views/pages/overboard.pug b/views/pages/overboard.pug new file mode 100644 index 00000000..dd76102e --- /dev/null +++ b/views/pages/overboard.pug @@ -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) diff --git a/views/pages/thread.pug b/views/pages/thread.pug index 4125f250..268611d1 100644 --- a/views/pages/thread.pug +++ b/views/pages/thread.pug @@ -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];