mirror of https://gitgud.io/fatchan/jschan.git
commit
ec64faa1c7
49 changed files with 781 additions and 246 deletions
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
@ -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<a.length;r++){switch(a.charCodeAt(r)){case 34:n=""";break;case 38:n="&";break;case 60:n="<";break;case 62:n=">";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); |
||||||
|
|
||||||
|
}); |
@ -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, |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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, |
||||||
|
}); |
||||||
|
|
||||||
|
} |
@ -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, |
||||||
|
}); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
.row |
||||||
|
.label IP/Hash |
||||||
|
input(type='text' name='ip' required) |
||||||
|
.row |
||||||
|
.label Ban Reason |
||||||
|
input(type='text' name='ban_reason') |
||||||
|
.row |
||||||
|
.label Modlog Message |
||||||
|
input(type='text' name='log_message') |
||||||
|
.row |
||||||
|
.label Ban Duration |
||||||
|
input(type='text' name='ban_duration' placeholder='e.g. 7d') |
||||||
|
.row |
||||||
|
.label Non-appealable Ban |
||||||
|
label.postform-style.ph-5 |
||||||
|
input(type='checkbox', name='no_appeal' value='1') |
||||||
|
.row |
||||||
|
.label Hide Username In Modlog |
||||||
|
label.postform-style.ph-5 |
||||||
|
input(type='checkbox', name='hide_name' value='1') |
||||||
|
input(type='submit', value='submit') |
||||||
|
|
@ -1,4 +1,4 @@ |
|||||||
noscript.no-m-p |
noscript.no-m-p |
||||||
iframe.captcha(src='/captcha.html' 'width=210' height='80' scrolling='no' loading='lazy') |
iframe.captcha(src='/captcha.html' 'width=210' height='80' scrolling='no' loading='lazy') |
||||||
.jsonly.captcha(style='display:none;') |
.jsonly.captcha(style='display:none;') |
||||||
input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='captcha text' pattern=".{6}" required title='6 characters') |
input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='Captcha text' pattern=".{6}" required title='6 characters') |
||||||
|
@ -1,10 +1,12 @@ |
|||||||
unless minimal |
unless minimal |
||||||
small.footer#bottom |
small.footer#bottom |
||||||
| - |
| - |
||||||
|
a(href='/news.html') news |
||||||
|
| - |
||||||
a(href='/rules.html') rules |
a(href='/rules.html') rules |
||||||
| - |
| - |
||||||
a(href='/faq.html') faq |
a(href='/faq.html') faq |
||||||
| - |
| - |
||||||
a(href='https://github.com/fatchan/jschan/') source code |
a(href='https://gitgud.io/fatchan/jschan/') source code |
||||||
| - |
| - |
||||||
script(src=`/js/render.js?v=${commit}`) |
script(src=`/js/render.js?v=${commit}`) |
||||||
|
@ -0,0 +1,2 @@ |
|||||||
|
include ../mixins/uploaditem.pug |
||||||
|
+uploaditem(uploaditem) |
@ -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 |
@ -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) |
Loading…
Reference in new issue