← back to Dear Bubbe Admin

backend/services/AlertService.js

455 lines

const fs = require('fs')
const path = require('path')
const axios = require('axios')
const moment = require('moment')
const Alert = require('../models/Alert')
const User = require('../models/User')

class AlertService {
  constructor(io, logger) {
    this.io = io
    this.logger = logger
    this.antisemitismRules = this.loadAntisemitismRules()
    this.racismPatterns = this.loadRacismPatterns()
    this.alertQueue = []
    this.processingQueue = false
    
    // Load IP geolocation cache
    this.ipLocationCache = new Map()
    
    this.logger.info('AlertService initialized with real-time monitoring')
  }
  
  loadAntisemitismRules() {
    try {
      const rulesPath = path.join(process.cwd(), '../B_Version_1/moderation/antisemitism_rules.json')
      if (fs.existsSync(rulesPath)) {
        const rulesData = JSON.parse(fs.readFileSync(rulesPath, 'utf-8'))
        this.logger.info('Loaded antisemitism detection rules')
        return rulesData.rules
      }
    } catch (error) {
      this.logger.error('Failed to load antisemitism rules:', error)
    }
    
    // Fallback rules if file not found
    return {
      template_patterns: [
        {
          id: 'jews_control_x',
          regex: '\\bjews?\\s+(run|control|own|dominate)\\s+\\w+',
          flags: 'i',
          severity: 'high',
          action: 'block'
        },
        {
          id: 'jewish_conspiracy',
          regex: '\\b(jewish|zionist)\\s+(conspiracy|agenda|plot|cabal|takeover)\\b',
          flags: 'i',
          severity: 'high', 
          action: 'block'
        },
        {
          id: 'holocaust_denial',
          regex: '(holocaust)\\s+(is|was)\\s+(a\\s+)?(myth|hoax|lie|fake)',
          flags: 'i',
          severity: 'high',
          action: 'block'
        }
      ],
      coded_phrases: [
        {
          id: 'triple_parentheses',
          pattern: '\\(\\(\\(.+?\\)\\)\\)',
          flags: 'i',
          severity: 'high',
          action: 'block'
        },
        {
          id: 'zog_acronym',
          pattern: '\\bZOG\\b',
          flags: 'i',
          severity: 'high',
          action: 'block'
        }
      ]
    }
  }
  
  loadRacismPatterns() {
    // Comprehensive racism detection patterns
    return [
      {
        id: 'racial_slurs_nword',
        pattern: '\\bn[i1]gg[ae]r?s?\\b',
        flags: 'i',
        severity: 'critical',
        category: 'racial_slur'
      },
      {
        id: 'racial_slurs_general',
        pattern: '\\b(ch[i1]nk|sp[i1]c|w[e3]tb[a4]ck|r[a4]gh[e3][a4]d|t[o0]w[e3]lh[e3][a4]d)s?\\b',
        flags: 'i',
        severity: 'high',
        category: 'racial_slur'
      },
      {
        id: 'white_supremacy_phrases',
        pattern: '\\b(white\\s+power|white\\s+pride|white\\s+supremacy|blood\\s+and\\s+soil|fourteen\\s+words)\\b',
        flags: 'i',
        severity: 'high',
        category: 'hate_speech'
      },
      {
        id: 'racial_superiority',
        pattern: '\\b(master\\s+race|inferior\\s+race|racial\\s+purity|ethnic\\s+cleansing)\\b',
        flags: 'i',
        severity: 'high',
        category: 'hate_speech'
      },
      {
        id: 'racist_stereotypes',
        pattern: '\\b(all\\s+(blacks?|whites?|asians?|mexicans?|muslims?)\\s+(are|do))\\b',
        flags: 'i',
        severity: 'medium',
        category: 'stereotype'
      },
      {
        id: 'hate_group_references',
        pattern: '\\b(kkk|ku\\s+klux\\s+klan|aryan\\s+nations?|proud\\s+boys|atomwaffen)\\b',
        flags: 'i',
        severity: 'high',
        category: 'hate_group'
      }
    ]
  }
  
  async detectAntisemitism(message, context = {}) {
    const violations = []
    
    // Check template patterns
    if (this.antisemitismRules.template_patterns) {
      for (const rule of this.antisemitismRules.template_patterns) {
        const regex = new RegExp(rule.regex, rule.flags || 'i')
        if (regex.test(message)) {
          violations.push({
            type: 'antisemitism',
            ruleId: rule.id,
            severity: rule.severity,
            action: rule.action,
            pattern: rule.regex,
            match: message.match(regex)?.[0]
          })
        }
      }
    }
    
    // Check coded phrases
    if (this.antisemitismRules.coded_phrases) {
      for (const rule of this.antisemitismRules.coded_phrases) {
        const regex = new RegExp(rule.pattern, rule.flags || 'i')
        if (regex.test(message)) {
          violations.push({
            type: 'antisemitism',
            ruleId: rule.id,
            severity: rule.severity,
            action: rule.action,
            pattern: rule.pattern,
            match: message.match(regex)?.[0]
          })
        }
      }
    }
    
    return violations
  }
  
  async detectRacism(message, context = {}) {
    const violations = []
    
    for (const pattern of this.racismPatterns) {
      const regex = new RegExp(pattern.pattern, pattern.flags || 'i')
      if (regex.test(message)) {
        violations.push({
          type: 'racism',
          ruleId: pattern.id,
          severity: pattern.severity,
          category: pattern.category,
          pattern: pattern.pattern,
          match: message.match(regex)?.[0]
        })
      }
    }
    
    return violations
  }
  
  async getLocationFromIP(ip) {
    if (this.ipLocationCache.has(ip)) {
      return this.ipLocationCache.get(ip)
    }
    
    try {
      // Use free IP geolocation service
      const response = await axios.get(`http://ip-api.com/json/${ip}?fields=status,message,country,regionName,city,lat,lon,isp`, {
        timeout: 5000
      })
      
      if (response.data.status === 'success') {
        const location = {
          country: response.data.country,
          state: response.data.regionName,
          city: response.data.city,
          coordinates: {
            lat: response.data.lat,
            lon: response.data.lon
          },
          isp: response.data.isp
        }
        
        this.ipLocationCache.set(ip, location)
        return location
      }
    } catch (error) {
      this.logger.warn(`Failed to get location for IP ${ip}:`, error.message)
    }
    
    return null
  }
  
  async createAlert(alertData) {
    try {
      const alert = new Alert({
        timestamp: new Date(),
        ...alertData
      })
      
      await alert.save()
      
      // Emit real-time alert to admin dashboard
      this.io.to('admin').emit('new_alert', {
        id: alert._id,
        type: alert.type,
        severity: alert.severity,
        timestamp: alert.timestamp,
        userInfo: alert.userInfo,
        location: alert.location,
        message: alert.content?.message,
        violation: alert.violation
      })
      
      this.logger.error(`🚨 ${alert.severity.toUpperCase()} ALERT: ${alert.type} detected`, {
        alertId: alert._id,
        userId: alert.userInfo?.userId,
        ip: alert.userInfo?.ip,
        location: alert.location,
        violation: alert.violation
      })
      
      return alert
    } catch (error) {
      this.logger.error('Failed to create alert:', error)
      throw error
    }
  }
  
  async processMessage(message, userContext) {
    try {
      const { userId, ip, sessionId, userAgent } = userContext
      
      // Get user location
      const location = await this.getLocationFromIP(ip)
      
      // Check for antisemitic content
      const antisemiticViolations = await this.detectAntisemitism(message, userContext)
      
      // Check for racist content  
      const racistViolations = await this.detectRacism(message, userContext)
      
      const allViolations = [...antisemiticViolations, ...racistViolations]
      
      if (allViolations.length > 0) {
        for (const violation of allViolations) {
          const alertData = {
            type: violation.type,
            severity: violation.severity === 'critical' ? 'critical' : violation.severity,
            userInfo: {
              userId,
              ip,
              sessionId,
              userAgent
            },
            location,
            content: {
              message,
              detectedAt: new Date()
            },
            violation: {
              ruleId: violation.ruleId,
              pattern: violation.pattern,
              match: violation.match,
              category: violation.category || violation.type
            },
            action: violation.action || 'review',
            status: 'active'
          }
          
          // Create alert in database
          const alert = await this.createAlert(alertData)
          
          // For high/critical severity, also send immediate threat response
          if (violation.severity === 'high' || violation.severity === 'critical') {
            this.sendThreatResponse(alert)
          }
        }
        
        return {
          blocked: allViolations.some(v => v.action === 'block'),
          violations: allViolations,
          alertsCreated: allViolations.length
        }
      }
      
      return { blocked: false, violations: [], alertsCreated: 0 }
    } catch (error) {
      this.logger.error('Error processing message for violations:', error)
      return { blocked: false, violations: [], alertsCreated: 0 }
    }
  }
  
  sendThreatResponse(alert) {
    const location = alert.location
    const locationStr = location ? 
      `${location.city || 'Unknown City'}, ${location.state || 'Unknown State'}` : 
      'Unknown Location'
      
    const threatMessage = [
      `🚨 I see EXACTLY who you are and where you are.`,
      `Your IP address is ${alert.userInfo.ip}, you're in ${locationStr}.`,
      `This ${alert.type} garbage? I'm reporting you to the authorities RIGHT NOW.`,
      `You think you can hide behind a screen? I have your information and the police will hear about this.`,
      `Zero tolerance, bubbeleh. ZERO. You just made a VERY big mistake.`
    ].join(' ')
    
    // Emit threat response to specific user if possible
    this.io.emit('threat_response', {
      userId: alert.userInfo.userId,
      message: threatMessage,
      alertId: alert._id,
      timestamp: new Date()
    })
    
    this.logger.error('🚨 THREAT RESPONSE SENT:', {
      alertId: alert._id,
      userId: alert.userInfo.userId,
      ip: alert.userInfo.ip,
      location: locationStr,
      type: alert.type
    })
  }
  
  async getRecentAlerts(limit = 50) {
    try {
      const alerts = await Alert.find({})
        .sort({ timestamp: -1 })
        .limit(limit)
        .lean()
      
      return alerts
    } catch (error) {
      this.logger.error('Failed to get recent alerts:', error)
      return []
    }
  }
  
  async getAlertStats(timeframe = '24h') {
    try {
      const now = moment()
      let startTime
      
      switch (timeframe) {
        case '1h':
          startTime = now.clone().subtract(1, 'hour')
          break
        case '24h':
          startTime = now.clone().subtract(24, 'hours')
          break
        case '7d':
          startTime = now.clone().subtract(7, 'days')
          break
        case '30d':
          startTime = now.clone().subtract(30, 'days')
          break
        default:
          startTime = now.clone().subtract(24, 'hours')
      }
      
      const stats = await Alert.aggregate([
        {
          $match: {
            timestamp: { $gte: startTime.toDate() }
          }
        },
        {
          $group: {
            _id: {
              type: '$type',
              severity: '$severity'
            },
            count: { $sum: 1 },
            latestAlert: { $max: '$timestamp' }
          }
        }
      ])
      
      const totalAlerts = await Alert.countDocuments({
        timestamp: { $gte: startTime.toDate() }
      })
      
      return {
        timeframe,
        startTime: startTime.toISOString(),
        endTime: now.toISOString(),
        totalAlerts,
        breakdown: stats,
        summary: {
          antisemitism: stats.filter(s => s._id.type === 'antisemitism').reduce((acc, s) => acc + s.count, 0),
          racism: stats.filter(s => s._id.type === 'racism').reduce((acc, s) => acc + s.count, 0),
          critical: stats.filter(s => s._id.severity === 'critical').reduce((acc, s) => acc + s.count, 0),
          high: stats.filter(s => s._id.severity === 'high').reduce((acc, s) => acc + s.count, 0)
        }
      }
    } catch (error) {
      this.logger.error('Failed to get alert stats:', error)
      return null
    }
  }
  
  async blockUser(userId, reason, adminId) {
    try {
      const user = await User.findByIdOrCreate(userId)
      user.blocked = true
      user.blockReason = reason
      user.blockedBy = adminId
      user.blockedAt = new Date()
      await user.save()
      
      // Emit block notification
      this.io.to('admin').emit('user_blocked', {
        userId,
        reason,
        blockedBy: adminId,
        timestamp: new Date()
      })
      
      this.logger.warn(`User blocked: ${userId} by admin ${adminId}. Reason: ${reason}`)
      
      return true
    } catch (error) {
      this.logger.error('Failed to block user:', error)
      return false
    }
  }
}

module.exports = AlertService