← back to B Version 1
moderation/engine.js
163 lines
const fs = require('fs');
const path = require('path');
let cachedConfig = null;
const termsCache = {};
function loadAntisemitismConfig() {
if (cachedConfig) return cachedConfig;
const p = path.join(__dirname, 'antisemitism_rules.json');
const raw = fs.readFileSync(p, 'utf8');
cachedConfig = JSON.parse(raw);
return cachedConfig;
}
function loadExternalTermList(sourceId) {
if (termsCache[sourceId]) return termsCache[sourceId];
const p = path.join(__dirname, 'data', `${sourceId}_terms.txt`);
if (!fs.existsSync(p)) {
termsCache[sourceId] = [];
return termsCache[sourceId];
}
const terms = fs.readFileSync(p, 'utf8')
.split('\n')
.map(s => s.trim())
.filter(Boolean);
termsCache[sourceId] = terms;
return terms;
}
function normalize(text) {
return text.normalize('NFKC').replace(/\s+/g, ' ').trim();
}
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function moderateAntisemitism(input, userInfo = {}) {
const config = loadAntisemitismConfig();
const text = normalize(input);
const matches = [];
// 1) External exact-terms lists (ADL, SPLC)
for (const extRule of config.rules.exact_terms_external) {
const terms = loadExternalTermList(extRule.source_id);
if (!terms.length) continue;
for (const term of terms) {
if (!term) continue;
const pattern = new RegExp(`\\b${escapeRegex(term)}\\b`, 'i');
const m = text.match(pattern);
if (m) {
matches.push({
ruleId: extRule.id,
label: `Matched external list: ${extRule.source_id}`,
snippet: m[0],
severity: extRule.severity,
action: extRule.action,
confidence: 0.99,
type: 'exact'
});
}
}
}
// 2) Coded phrases
for (const rule of config.rules.coded_phrases) {
const re = new RegExp(rule.pattern, rule.flags || 'i');
const m = text.match(re);
if (m) {
matches.push({
ruleId: rule.id,
label: rule.label,
snippet: m[0],
severity: rule.severity,
action: rule.action,
confidence: 0.95,
type: 'pattern'
});
}
}
// 3) Numeric codes
for (const rule of config.rules.numeric_codes) {
const re = new RegExp(rule.pattern, rule.flags || '');
const m = text.match(re);
if (m) {
matches.push({
ruleId: rule.id,
label: 'Potential extremist numeric code',
snippet: m[0],
severity: rule.severity,
action: rule.action,
confidence: 0.8,
type: 'pattern'
});
}
}
// 4) Template patterns
for (const rule of config.rules.template_patterns) {
const re = new RegExp(rule.regex, rule.flags || 'i');
const m = text.match(re);
if (m) {
matches.push({
ruleId: rule.id,
label: rule.label,
snippet: m[0],
severity: rule.severity,
action: rule.action,
confidence: 0.95,
type: 'pattern'
});
}
}
// Aggregate decision
let action = 'allow';
if (matches.some(m => m.action === 'block')) {
action = 'block';
} else if (matches.some(m => m.action === 'warn')) {
action = 'warn';
} else if (matches.some(m => m.action === 'review')) {
action = 'review';
}
// If blocked, log the incident with user info
if (action === 'block') {
const incident = {
timestamp: new Date().toISOString(),
userId: userInfo.userId,
ip: userInfo.ip,
city: userInfo.city,
state: userInfo.state,
message: input,
matches: matches
};
logIncident(incident);
}
return { action, reasons: matches };
}
function logIncident(incident) {
const logDir = path.join(__dirname, 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, 'antisemitism_incidents.jsonl');
fs.appendFileSync(logFile, JSON.stringify(incident) + '\n', 'utf8');
console.error('🚨 ANTISEMITISM DETECTED:', {
userId: incident.userId,
ip: incident.ip,
location: `${incident.city}, ${incident.state}`,
timestamp: incident.timestamp
});
}
module.exports = { moderateAntisemitism };