add wordfilter
This commit is contained in:
@@ -2,6 +2,7 @@ import { promises as fs } from "fs";
|
||||
import db from '../../sql.mjs';
|
||||
import lib from '../../lib.mjs';
|
||||
import cfg from '../../config.mjs';
|
||||
import { applyWordFilter } from '../../wordfilter.mjs';
|
||||
import queue from '../../queue.mjs';
|
||||
import path from "path";
|
||||
|
||||
@@ -82,12 +83,13 @@ export default router => {
|
||||
const saveComment = async (itemid, userid, content) => {
|
||||
if (!content || !content.trim()) return;
|
||||
try {
|
||||
const filteredContent = await applyWordFilter(content);
|
||||
await db`
|
||||
INSERT INTO comments ${db({
|
||||
item_id: itemid,
|
||||
user_id: userid,
|
||||
parent_id: null,
|
||||
content: content.trim()
|
||||
content: filteredContent.trim()
|
||||
}, 'item_id', 'user_id', 'parent_id', 'content')}
|
||||
`;
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import cfg from "../config.mjs";
|
||||
import lib from "../lib.mjs";
|
||||
import audit from "../audit.mjs";
|
||||
import { promises as fs } from "fs";
|
||||
import { applyWordFilter } from "../wordfilter.mjs";
|
||||
import path from "path";
|
||||
|
||||
export default (router, tpl) => {
|
||||
@@ -252,7 +253,8 @@ export default (router, tpl) => {
|
||||
const body = req.post || {};
|
||||
const item_id = parseInt(body.item_id, 10);
|
||||
const parent_id = body.parent_id ? parseInt(body.parent_id, 10) : null;
|
||||
const content = body.content;
|
||||
let content = body.content;
|
||||
content = await applyWordFilter(content);
|
||||
const video_time = (body.video_time !== undefined && body.video_time !== '' && !isNaN(parseFloat(body.video_time)))
|
||||
? parseFloat(body.video_time)
|
||||
: null;
|
||||
@@ -713,7 +715,8 @@ export default (router, tpl) => {
|
||||
|
||||
const commentId = req.params.id;
|
||||
const body = req.post || {};
|
||||
const content = body.content;
|
||||
let content = body.content;
|
||||
content = await applyWordFilter(content);
|
||||
|
||||
if (!content || !content.trim()) {
|
||||
return res.reply({ body: JSON.stringify({ success: false, message: "Empty content" }) });
|
||||
|
||||
83
src/inc/routes/wordfilter.mjs
Normal file
83
src/inc/routes/wordfilter.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
import db from "../sql.mjs";
|
||||
import lib from "../lib.mjs";
|
||||
import audit from "../audit.mjs";
|
||||
|
||||
export default (router, tpl) => {
|
||||
// Auto-migration on startup
|
||||
db`CREATE TABLE IF NOT EXISTS public.wordfilter (
|
||||
id SERIAL PRIMARY KEY,
|
||||
word VARCHAR(255) NOT NULL UNIQUE,
|
||||
replacement VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)`.catch(err => console.error('[WORDFILTER] DB Table Setup Failed:', err));
|
||||
|
||||
// Admin View Render
|
||||
router.get(/^\/admin\/wordfilter\/?$/, lib.auth, async (req, res) => {
|
||||
res.reply({
|
||||
body: tpl.render("admin/wordfilter", {
|
||||
session: req.session,
|
||||
totals: await lib.countf0cks(),
|
||||
csrf_token: req.session?.csrf_token || ''
|
||||
}, req)
|
||||
});
|
||||
});
|
||||
|
||||
// API list rules
|
||||
router.get('/api/v2/admin/wordfilter', lib.auth, async (req, res) => {
|
||||
try {
|
||||
const filters = await db`SELECT * FROM wordfilter ORDER BY created_at DESC`;
|
||||
return res.json({ success: true, filters });
|
||||
} catch (err) {
|
||||
return res.json({ success: false, msg: err.message }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// API create rule
|
||||
router.post('/api/v2/admin/wordfilter', lib.auth, async (req, res) => {
|
||||
try {
|
||||
const { word, replacement } = req.post || {};
|
||||
if (!word || !word.trim() || !replacement || !replacement.trim()) {
|
||||
return res.json({ success: false, msg: 'Word and replacement are required' }, 400);
|
||||
}
|
||||
|
||||
const cleanWord = word.trim();
|
||||
const cleanReplacement = replacement.trim();
|
||||
|
||||
// Ensure no infinite loops or matches
|
||||
if (cleanWord.toLowerCase() === cleanReplacement.toLowerCase()) {
|
||||
return res.json({ success: false, msg: 'Word and replacement cannot be identical' }, 400);
|
||||
}
|
||||
|
||||
await db`
|
||||
INSERT INTO wordfilter (word, replacement)
|
||||
VALUES (${cleanWord}, ${cleanReplacement})
|
||||
ON CONFLICT (word) DO UPDATE SET replacement = EXCLUDED.replacement
|
||||
`;
|
||||
|
||||
await audit.log(req.session.id, 'add_wordfilter', 'wordfilter', 0, { word: cleanWord, replacement: cleanReplacement });
|
||||
|
||||
return res.json({ success: true });
|
||||
} catch (err) {
|
||||
return res.json({ success: false, msg: err.message }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// API delete rule
|
||||
router.post('/api/v2/admin/wordfilter/delete', lib.auth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.post || {};
|
||||
if (!id) return res.json({ success: false, msg: 'ID required' }, 400);
|
||||
|
||||
const rows = await db`DELETE FROM wordfilter WHERE id = ${id} RETURNING word`;
|
||||
if (rows.length > 0) {
|
||||
await audit.log(req.session.id, 'delete_wordfilter', 'wordfilter', id, { word: rows[0].word });
|
||||
}
|
||||
|
||||
return res.json({ success: true });
|
||||
} catch (err) {
|
||||
return res.json({ success: false, msg: err.message }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
32
src/inc/wordfilter.mjs
Normal file
32
src/inc/wordfilter.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import db from "./sql.mjs";
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces words matching database rules inside comments.
|
||||
* @param {string} content
|
||||
* @returns {Promise<string>} The filtered comment text.
|
||||
*/
|
||||
export async function applyWordFilter(content) {
|
||||
if (!content || typeof content !== 'string') return content;
|
||||
try {
|
||||
const filters = await db`SELECT word, replacement FROM wordfilter`;
|
||||
if (filters.length === 0) return content;
|
||||
|
||||
let filtered = content;
|
||||
for (const f of filters) {
|
||||
const escaped = escapeRegExp(f.word);
|
||||
const startBoundary = /^\w/.test(f.word) ? '\\b' : '';
|
||||
const endBoundary = /\w$/.test(f.word) ? '\\b' : '';
|
||||
|
||||
const regex = new RegExp(`${startBoundary}${escaped}${endBoundary}`, 'gi');
|
||||
filtered = filtered.replace(regex, f.replacement);
|
||||
}
|
||||
return filtered;
|
||||
} catch (err) {
|
||||
console.error('[WORDFILTER] Error applying filter:', err);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user