add wordfilter

This commit is contained in:
2026-05-23 22:55:53 +02:00
parent 9a9b787fd7
commit a0ac4607cc
7 changed files with 277 additions and 4 deletions

View File

@@ -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) {

View File

@@ -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" }) });

View 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
View 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;
}
}