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