const twilio = require('twilio'); const { AlertRule, AlertLog, User, Device } = require('../models'); const { Op } = require('sequelize'); class AlertService { constructor() { this.twilioClient = null; this.twilioPhone = null; this.twilioEnabled = false; this.activeAlerts = new Map(); // Track active alerts for clear notifications this.initializeTwilio(); } // RSSI-based threat assessment for security installations assessThreatLevel(rssi, droneType) { // RSSI typically ranges from -30 (very close) to -100 (very far) // For 15km range detection, we need to establish threat zones for sensitive facilities let threatLevel = 'low'; let estimatedDistance = 0; let description = ''; let actionRequired = false; // Convert RSSI to estimated distance (rough calculation) // Formula: Distance (m) = 10^((RSSI_at_1m - RSSI) / (10 * n)) // Where n = path loss exponent (typically 2-4 for outdoor environments) const rssiAt1m = -30; // Typical RSSI at 1 meter const pathLossExponent = 3; // Outdoor environment with obstacles estimatedDistance = Math.pow(10, (rssiAt1m - rssi) / (10 * pathLossExponent)); // Threat level assessment based on distance zones for sensitive facilities if (rssi >= -40) { // Very close: 0-50 meters - CRITICAL THREAT threatLevel = 'critical'; description = 'IMMEDIATE THREAT: Drone within security perimeter (0-50m)'; actionRequired = true; } else if (rssi >= -55) { // Close: 50-200 meters - HIGH THREAT threatLevel = 'high'; description = 'HIGH THREAT: Drone approaching facility (50-200m)'; actionRequired = true; } else if (rssi >= -70) { // Medium: 200-1000 meters - MEDIUM THREAT threatLevel = 'medium'; description = 'MEDIUM THREAT: Drone in facility vicinity (200m-1km)'; actionRequired = false; } else if (rssi >= -85) { // Far: 1-5 kilometers - LOW THREAT threatLevel = 'low'; description = 'LOW THREAT: Drone detected at distance (1-5km)'; actionRequired = false; } else { // Very far: 5-15 kilometers - MONITORING ONLY threatLevel = 'monitoring'; description = 'MONITORING: Drone detected at long range (5-15km)'; actionRequired = false; } // Adjust threat level based on drone type (if classified) const droneTypes = { 0: 'Consumer/Hobby', 1: 'Professional/Military', 2: 'Racing/High-speed', 3: 'Unknown/Custom' }; if (droneType === 1) { // Military/Professional drone - escalate threat if (threatLevel === 'low') threatLevel = 'medium'; if (threatLevel === 'medium') threatLevel = 'high'; if (threatLevel === 'high') threatLevel = 'critical'; description += ' - PROFESSIONAL/MILITARY DRONE DETECTED'; actionRequired = true; } else if (droneType === 2) { // Racing/Fast drone - escalate if close if (rssi >= -55 && threatLevel !== 'critical') { threatLevel = 'high'; description += ' - HIGH-SPEED DRONE DETECTED'; actionRequired = true; } } return { level: threatLevel, estimatedDistance: Math.round(estimatedDistance), rssi, droneType: droneTypes[droneType] || 'Unknown', description, requiresImmediateAction: actionRequired, priority: threatLevel === 'critical' ? 1 : threatLevel === 'high' ? 2 : threatLevel === 'medium' ? 3 : 4 }; } initializeTwilio() { // Check if Twilio credentials are provided const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const phoneNumber = process.env.TWILIO_PHONE_NUMBER; // If any Twilio credential is missing, disable SMS functionality if (!accountSid || !authToken || !phoneNumber || accountSid.trim() === '' || authToken.trim() === '' || phoneNumber.trim() === '') { console.log('šŸ“± Twilio credentials not configured - SMS alerts disabled'); console.log('ā„¹ļø To enable SMS alerts, set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER'); this.twilioEnabled = false; this.twilioClient = null; return; } // Validate Twilio Account SID format if (!accountSid.startsWith('AC')) { console.log('āš ļø Invalid Twilio Account SID format - SMS alerts disabled'); console.log('ā„¹ļø Account SID must start with "AC"'); this.twilioEnabled = false; this.twilioClient = null; return; } try { this.twilioClient = twilio(accountSid, authToken); this.twilioPhone = phoneNumber; this.twilioEnabled = true; console.log('šŸ“± Twilio SMS service initialized successfully'); } catch (error) { console.error('āŒ Failed to initialize Twilio:', error.message); console.log('šŸ“± SMS alerts disabled due to Twilio initialization error'); this.twilioEnabled = false; this.twilioClient = null; } } async processAlert(detection) { try { console.log(`šŸ” Processing alert for detection ${detection.id}`); // Assess threat level based on RSSI and drone type const threatAssessment = this.assessThreatLevel(detection.rssi, detection.drone_type); console.log('āš ļø Threat assessment:', threatAssessment); // Update detection with threat assessment await detection.update({ processed: true, threat_level: threatAssessment.level, estimated_distance: threatAssessment.estimatedDistance }); // Get all active alert rules const alertRules = await AlertRule.findAll({ where: { is_active: true }, include: [{ model: User, as: 'user', where: { is_active: true } }] }); for (const rule of alertRules) { if (await this.shouldTriggerAlert(rule, detection, threatAssessment)) { await this.triggerAlert(rule, detection, threatAssessment); // Track active alert for potential clear notification const alertKey = `${rule.id}-${detection.device_id}`; this.activeAlerts.set(alertKey, { rule, detection, threatAssessment, alertTime: new Date() }); } } } catch (error) { console.error('Error processing alert:', error); throw error; } } async shouldTriggerAlert(rule, detection, threatAssessment) { try { // SECURITY ENHANCEMENT: Check threat level requirements if (rule.conditions.min_threat_level) { const threatLevels = { 'monitoring': 0, 'low': 1, 'medium': 2, 'high': 3, 'critical': 4 }; const requiredLevel = threatLevels[rule.conditions.min_threat_level] || 0; const currentLevel = threatLevels[threatAssessment.level] || 0; if (currentLevel < requiredLevel) { console.log(`Alert rule ${rule.name}: Threat level ${threatAssessment.level} below minimum ${rule.conditions.min_threat_level}`); return false; } } // SECURITY ENHANCEMENT: For government/sensitive sites, always alert on critical threats if (threatAssessment.level === 'critical') { console.log(`🚨 CRITICAL THREAT DETECTED - Force triggering alert for rule ${rule.name}`); return true; } // Check device filter if (rule.conditions.device_ids && rule.conditions.device_ids.length > 0 && !rule.conditions.device_ids.includes(detection.device_id)) { return false; } // Check drone type filter if (rule.conditions.drone_types && rule.conditions.drone_types.length > 0 && !rule.conditions.drone_types.includes(detection.drone_type)) { return false; } // Check RSSI thresholds (enhanced for security) if (rule.conditions.rssi_threshold && detection.rssi < rule.conditions.rssi_threshold) { return false; } // SECURITY ENHANCEMENT: Check estimated distance if (rule.conditions.max_distance && threatAssessment.estimatedDistance > rule.conditions.max_distance) { console.log(`Alert rule ${rule.name}: Distance ${threatAssessment.estimatedDistance}m exceeds maximum ${rule.conditions.max_distance}m`); return false; } // Check frequency ranges if (rule.frequency_ranges && rule.frequency_ranges.length > 0) { const inRange = rule.frequency_ranges.some(range => detection.freq >= range.min && detection.freq <= range.max ); if (!inRange) { return false; } } // Check time window and minimum detections if (rule.min_detections > 1) { const timeWindowStart = new Date(Date.now() - rule.time_window * 1000); const recentDetections = await DroneDetection.count({ where: { device_id: detection.device_id, drone_id: detection.drone_id, server_timestamp: { [Op.gte]: timeWindowStart } } }); if (recentDetections < rule.min_detections) { return false; } } // Check cooldown period if (rule.cooldown_period > 0) { const cooldownStart = new Date(Date.now() - rule.cooldown_period * 1000); const recentAlert = await AlertLog.findOne({ where: { alert_rule_id: rule.id, status: 'sent', sent_at: { [Op.gte]: cooldownStart } }, include: [{ model: DroneDetection, as: 'detection', where: { device_id: detection.device_id, drone_id: detection.drone_id } }] }); if (recentAlert) { return false; } } // Check active hours and days if (rule.active_hours || rule.active_days) { const now = new Date(); const currentHour = now.getHours(); const currentMinute = now.getMinutes(); const currentTime = currentHour * 60 + currentMinute; const currentDay = now.getDay() || 7; // Convert Sunday from 0 to 7 if (rule.active_days && !rule.active_days.includes(currentDay)) { return false; } if (rule.active_hours) { const startTime = this.parseTime(rule.active_hours.start); const endTime = this.parseTime(rule.active_hours.end); if (startTime !== null && endTime !== null) { if (startTime <= endTime) { // Same day range if (currentTime < startTime || currentTime > endTime) { return false; } } else { // Overnight range if (currentTime < startTime && currentTime > endTime) { return false; } } } } } return true; } catch (error) { console.error('Error checking alert conditions:', error); return false; } } async triggerAlert(rule, detection, threatAssessment) { try { const user = rule.user; const device = await Device.findByPk(detection.device_id); // Generate enhanced alert message with threat assessment const message = this.generateSecurityAlertMessage(detection, device, rule, threatAssessment); // SECURITY ENHANCEMENT: For critical threats, send to all available channels const channels = threatAssessment.level === 'critical' ? ['sms', 'email', 'webhook'] // Force all channels for critical threats : rule.alert_channels || ['sms']; // Send alerts through configured channels for (const channel of channels) { let alertLog = null; try { switch (channel) { case 'sms': if (rule.alert_channels.includes('sms') && rule.sms_phone_number) { alertLog = await this.sendSMSAlert(rule.sms_phone_number, message, rule, detection, threatAssessment); } break; case 'email': if (rule.actions.email && user.email) { alertLog = await this.sendEmailAlert(user.email, message, rule, detection, threatAssessment); } break; case 'webhook': if (rule.actions.webhook_url) { alertLog = await this.sendWebhookAlert(rule.actions.webhook_url, detection, device, rule, threatAssessment); } break; default: console.warn(`Unknown alert channel: ${channel}`); } if (alertLog) { console.log(`🚨 ${threatAssessment.level.toUpperCase()} THREAT: Alert sent via ${channel} for detection ${detection.id}`); } } catch (channelError) { console.error(`Error sending ${channel} alert:`, channelError); // Log failed alert await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: channel, recipient: channel === 'sms' ? user.phone_number : channel === 'email' ? user.email : rule.webhook_url, message: message, status: 'failed', error_message: channelError.message, priority: rule.priority }); } } } catch (error) { console.error('Error triggering alert:', error); throw error; } } async sendSMSAlert(phoneNumber, message, rule, detection) { // Check if Twilio is enabled if (!this.twilioEnabled || !this.twilioClient) { console.log('šŸ“± SMS alert skipped - Twilio not configured'); console.log(`šŸ“± Would have sent to ${phoneNumber}:`); console.log(`šŸ“± Message: ${message}`); return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'sms', recipient: phoneNumber, message: message, status: 'failed', sent_at: new Date(), external_id: null, priority: rule.priority, error_message: 'SMS service not configured' }); } try { console.log(`šŸ“± Sending SMS alert to ${phoneNumber} for rule: ${rule.name}`); console.log(`šŸ“± Message: ${message}`); const twilioMessage = await this.twilioClient.messages.create({ body: message, from: this.twilioPhone, to: phoneNumber }); console.log(`āœ… SMS sent successfully to ${phoneNumber}: ${twilioMessage.sid}`); return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'sms', recipient: phoneNumber, message: message, status: 'sent', sent_at: new Date(), external_id: twilioMessage.sid, priority: rule.priority }); } catch (error) { console.error(`āŒ Failed to send SMS to ${phoneNumber}:`, error.message); return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'sms', recipient: phoneNumber, message: message, status: 'failed', sent_at: new Date(), external_id: null, priority: rule.priority, error_message: error.message }); } } async sendEmailAlert(email, message, rule, detection) { // Email implementation would go here // For now, just log the alert console.log(`Email alert would be sent to ${email}: ${message}`); return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'email', recipient: email, message: message, status: 'sent', sent_at: new Date(), priority: rule.priority }); } async sendWebhookAlert(webhookUrl, detection, device, rule) { const payload = { event: 'drone_detection', timestamp: new Date().toISOString(), detection: { id: detection.id, device_id: detection.device_id, drone_id: detection.drone_id, drone_type: detection.drone_type, rssi: detection.rssi, freq: detection.freq, geo_lat: detection.geo_lat, geo_lon: detection.geo_lon, server_timestamp: detection.server_timestamp }, device: { id: device.id, name: device.name, geo_lat: device.geo_lat, geo_lon: device.geo_lon, location_description: device.location_description }, alert_rule: { id: rule.id, name: rule.name, priority: rule.priority } }; const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'DroneDetectionSystem/1.0' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`Webhook failed with status ${response.status}`); } return await AlertLog.create({ alert_rule_id: rule.id, detection_id: detection.id, alert_type: 'webhook', recipient: webhookUrl, message: JSON.stringify(payload), status: 'sent', sent_at: new Date(), priority: rule.priority }); } generateAlertMessage(detection, device, rule) { const deviceName = device.name || `Device ${device.id}`; const location = device.location_description || `${device.geo_lat}, ${device.geo_lon}` || 'Unknown location'; return `🚨 DRONE ALERT: Drone detected by ${deviceName} at ${location}. ` + `Drone ID: ${detection.drone_id}, Frequency: ${detection.freq}MHz, ` + `RSSI: ${detection.rssi}dBm. Time: ${new Date().toLocaleString()}`; } // SECURITY ENHANCEMENT: Enhanced message generation with threat assessment generateSecurityAlertMessage(detection, device, rule, threatAssessment) { const timestamp = new Date().toLocaleString('sv-SE', { timeZone: 'Europe/Stockholm' }); const deviceName = device ? device.name : `Device ${detection.device_id}`; const location = device ? device.location : 'Unknown location'; // Create security-focused alert message let message = `🚨 SECURITY ALERT 🚨\n`; message += `THREAT LEVEL: ${threatAssessment.level.toUpperCase()}\n`; message += `${threatAssessment.description}\n\n`; message += `šŸ“ LOCATION: ${location}\n`; message += `šŸ”§ DEVICE: ${deviceName}\n`; message += `šŸ“ DISTANCE: ~${threatAssessment.estimatedDistance}m\n`; message += `šŸ“¶ SIGNAL: ${detection.rssi} dBm\n`; message += `🚁 DRONE TYPE: ${threatAssessment.droneType}\n`; message += `ā° TIME: ${timestamp}\n`; if (threatAssessment.requiresImmediateAction) { message += `\nāš ļø IMMEDIATE ACTION REQUIRED\n`; message += `Security personnel should respond immediately.`; } return message; } parseTime(timeString) { if (!timeString) return null; const match = timeString.match(/^(\d{1,2}):(\d{2})$/); if (!match) return null; const hours = parseInt(match[1]); const minutes = parseInt(match[2]); if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { return null; } return hours * 60 + minutes; } // Check for cleared alerts (call this periodically) async checkClearedAlerts() { try { const now = new Date(); const clearThreshold = 5 * 60 * 1000; // 5 minutes without detection = cleared for (const [alertKey, alertData] of this.activeAlerts.entries()) { const timeSinceAlert = now - alertData.alertTime; if (timeSinceAlert > clearThreshold) { // Check if there are any recent detections from this device const recentDetections = await DroneDetection.count({ where: { device_id: alertData.detection.device_id, device_timestamp: { [Op.gte]: new Date(now - clearThreshold) } } }); if (recentDetections === 0) { await this.sendClearAlert(alertData); this.activeAlerts.delete(alertKey); } } } } catch (error) { console.error('Error checking cleared alerts:', error); } } async sendClearAlert(alertData) { const { rule, detection } = alertData; if (rule.alert_channels.includes('sms') && rule.sms_phone_number) { const device = await Device.findByPk(detection.device_id); const clearMessage = this.generateClearMessage(device, rule); console.log(`🟢 ALERT CLEARED - Sending clear notification for ${device.name} to ${rule.sms_phone_number}`); await this.sendSMSAlert(rule.sms_phone_number, clearMessage, rule, detection, null); } } generateClearMessage(device, rule) { return `🟢 ALL CLEAR 🟢\n\n` + `šŸ“ LOCATION: ${device.location_description || device.name}\n` + `šŸ”§ DEVICE: ${device.name}\n` + `ā° TIME: ${new Date().toLocaleString('sv-SE')}\n\n` + `āœ… No drone activity detected for 5+ minutes.\n` + `šŸ›”ļø Area is secure.`; } } // Export the class directly (not a singleton instance) module.exports = AlertService;