Files
drone-detector/server/services/alertService.js
2025-09-15 21:26:15 +02:00

871 lines
31 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const twilio = require('twilio');
const { AlertRule, AlertLog, User, Device, DroneDetection } = require('../models');
const { Op } = require('sequelize');
const { getDroneTypeInfo } = require('../utils/droneTypes');
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;
}
// Get drone type information using our comprehensive mapping
const droneTypeInfo = getDroneTypeInfo(droneType);
// Store original distance-based assessment
const distanceBasedThreat = threatLevel;
// Adjust threat level based on drone type and category
if (droneTypeInfo.category.includes('Military') && droneTypeInfo.name !== 'Unknown') {
// Special handling for known military drones at very far distances
// If it's a recognized military drone at long range, escalate to critical
if (rssi < -80 && (droneTypeInfo.name === 'Orlan' || droneTypeInfo.name === 'Zala' || droneTypeInfo.name === 'Eleron')) {
threatLevel = 'critical';
description = `CRITICAL THREAT: ${droneTypeInfo.name.toUpperCase()} DETECTED - IMMEDIATE RESPONSE REQUIRED`;
actionRequired = true;
console.log(`🚨 MILITARY DRONE DETECTED: ${droneTypeInfo.name} - Force escalating to CRITICAL at long range (RSSI: ${rssi})`);
} else {
// For closer military drones, preserve distance-based assessment but add annotation
description += ` - ${droneTypeInfo.name.toUpperCase()} MILITARY DRONE DETECTED`;
console.log(`🚨 MILITARY DRONE DETECTED: ${droneTypeInfo.name} - Using distance-based threat level: ${threatLevel} (RSSI: ${rssi})`);
}
} else if (droneTypeInfo.threat_level === 'high' || droneTypeInfo.category.includes('Professional')) {
// Professional/Commercial drone - escalate threat one level from distance-based
if (distanceBasedThreat === 'monitoring') threatLevel = 'low';
else if (distanceBasedThreat === 'low') threatLevel = 'medium';
else if (distanceBasedThreat === 'medium') threatLevel = 'high';
description += ` - ${droneTypeInfo.name.toUpperCase()} DETECTED`;
actionRequired = (threatLevel !== 'low' && threatLevel !== 'monitoring');
} else if (droneTypeInfo.category.includes('Racing')) {
// Racing/Fast drone - escalate if close
if (rssi >= -55) {
if (distanceBasedThreat === 'monitoring') threatLevel = 'low';
else if (distanceBasedThreat === 'low') threatLevel = 'medium';
else if (distanceBasedThreat === 'medium') threatLevel = 'high';
description += ` - HIGH-SPEED ${droneTypeInfo.name.toUpperCase()} DETECTED`;
actionRequired = true;
}
}
return {
level: threatLevel,
estimatedDistance: Math.round(estimatedDistance),
rssi,
droneType: droneTypeInfo.name,
droneCategory: droneTypeInfo.category,
threatLevel: droneTypeInfo.threat_level,
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 or empty, disable SMS functionality
if (!accountSid || !authToken || !phoneNumber ||
accountSid.trim() === '' || authToken.trim() === '' || phoneNumber.trim() === '') {
console.log('📱 Twilio credentials not configured - SMS alerts disabled (Development Mode)');
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 (only if provided)
if (accountSid && !accountSid.startsWith('AC')) {
console.log('⚠️ Invalid Twilio Account SID format - SMS alerts disabled (Development Mode)');
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 {
// Skip alert processing for drone type 0 (None) - no actual detection
if (detection.drone_type === 0) {
console.log(`🔍 Skipping alert processing for drone type 0 (None) - detection ${detection.id}`);
return;
}
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 }
}],
order: [
// Order by priority: critical, high, medium, low
['priority', 'DESC']
]
});
console.log(`📋 Found ${alertRules.length} active alert rules`);
// ENHANCED: Filter rules that match this detection and find the highest priority one
const matchingRules = [];
for (const rule of alertRules) {
if (await this.shouldTriggerAlert(rule, detection, threatAssessment)) {
matchingRules.push(rule);
console.log(`✅ Rule "${rule.name}" matches (priority: ${rule.priority})`);
} else {
console.log(`❌ Rule "${rule.name}" does not match conditions`);
}
}
if (matchingRules.length === 0) {
console.log('📭 No alert rules match this detection');
return;
}
// PRIORITY-BASED DEDUPLICATION: Only trigger the highest priority rule
const highestPriorityRule = matchingRules[0]; // Already sorted by priority DESC
console.log(`🎯 ALERT DEDUPLICATION: ${matchingRules.length} rules matched, triggering only highest priority:`);
console.log(` Selected Rule: "${highestPriorityRule.name}" (priority: ${highestPriorityRule.priority})`);
if (matchingRules.length > 1) {
console.log(` Skipped Rules:`);
matchingRules.slice(1).forEach(rule => {
console.log(` - "${rule.name}" (priority: ${rule.priority})`);
});
}
// Trigger only the highest priority alert
await this.triggerAlert(highestPriorityRule, detection, threatAssessment);
// Track active alert for potential clear notification
const alertKey = `${highestPriorityRule.id}-${detection.device_id}`;
this.activeAlerts.set(alertKey, {
rule: highestPriorityRule,
detection,
threatAssessment,
alertTime: new Date()
});
} catch (error) {
console.error('Error processing alert:', error);
throw error;
}
}
async shouldTriggerAlert(rule, detection, threatAssessment) {
try {
console.log(`🔍 Evaluating rule "${rule.name}" for detection from device ${detection.device_id}`);
// PRIORITY 1: Device-specific filter (most important)
if (rule.device_ids && rule.device_ids.length > 0) {
if (!rule.device_ids.includes(detection.device_id)) {
console.log(`❌ Rule "${rule.name}": Device ${detection.device_id} not in allowed devices [${rule.device_ids.join(', ')}]`);
return false;
} else {
console.log(`✅ Rule "${rule.name}": Device ${detection.device_id} is in allowed devices`);
}
} else {
console.log(`✅ Rule "${rule.name}": No device filter (applies to all devices)`);
}
// PRIORITY 2: Critical threats and Orlan drones (security overrides)
const isOrlanDrone = detection.drone_type === 1;
const isCriticalThreat = threatAssessment.level === 'critical';
if (isOrlanDrone) {
console.log(`🚨 ORLAN DRONE DETECTED - Rule "${rule.name}" will trigger (security override)`);
return true;
}
if (isCriticalThreat) {
console.log(`🚨 CRITICAL THREAT DETECTED - Rule "${rule.name}" will trigger (security override)`);
return true;
}
// PRIORITY 3: Threat level requirements
if (rule.min_threat_level) {
const threatLevels = { 'monitoring': 0, 'low': 1, 'medium': 2, 'high': 3, 'critical': 4 };
const requiredLevel = threatLevels[rule.min_threat_level] || 0;
const currentLevel = threatLevels[threatAssessment.level] || 0;
if (currentLevel < requiredLevel) {
console.log(`❌ Rule "${rule.name}": Threat level ${threatAssessment.level} below minimum ${rule.min_threat_level}`);
return false;
} else {
console.log(`✅ Rule "${rule.name}": Threat level ${threatAssessment.level} meets minimum ${rule.min_threat_level}`);
}
}
// PRIORITY 4: Drone type filter
if (rule.drone_types && rule.drone_types.length > 0 &&
!rule.drone_types.includes(detection.drone_type)) {
console.log(`❌ Rule "${rule.name}": Drone type ${detection.drone_type} not in allowed types [${rule.drone_types.join(', ')}]`);
return false;
}
// PRIORITY 5: RSSI thresholds (distance-based)
if (rule.min_rssi && detection.rssi < rule.min_rssi) {
console.log(`❌ Rule "${rule.name}": RSSI ${detection.rssi} below minimum ${rule.min_rssi}`);
return false;
}
if (rule.max_rssi && detection.rssi > rule.max_rssi) {
console.log(`❌ Rule "${rule.name}": RSSI ${detection.rssi} above maximum ${rule.max_rssi}`);
return false;
}
// PRIORITY 6: Distance requirements
if (rule.max_distance && threatAssessment.estimatedDistance > rule.max_distance) {
console.log(`❌ Rule "${rule.name}": Distance ${threatAssessment.estimatedDistance}m exceeds maximum ${rule.max_distance}m`);
return false;
}
// PRIORITY 7: 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) {
console.log(`❌ Rule "${rule.name}": Frequency ${detection.freq}MHz not in allowed ranges`);
return false;
}
}
// PRIORITY 8: 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) {
console.log(`❌ Rule "${rule.name}": Only ${recentDetections} detections, need ${rule.min_detections}`);
return false;
}
}
// PRIORITY 9: Cooldown period check
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) {
console.log(`❌ Rule "${rule.name}": Still in cooldown period`);
return false;
}
}
// PRIORITY 10: 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)) {
console.log(`❌ Rule "${rule.name}": Not active on day ${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) {
console.log(`❌ Rule "${rule.name}": Outside active hours`);
return false;
}
} else {
// Overnight range
if (currentTime < startTime && currentTime > endTime) {
console.log(`❌ Rule "${rule.name}": Outside active hours (overnight)`);
return false;
}
}
}
}
}
console.log(`✅ Rule "${rule.name}": All conditions met - WILL TRIGGER`);
return true;
} catch (error) {
console.error(`Error evaluating alert rule ${rule.name}:`, 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.alert_channels.includes('email') && (rule.email || user.email)) {
alertLog = await this.sendEmailAlert(rule.email || user.email, message, rule, detection, threatAssessment);
}
break;
case 'webhook':
if (rule.alert_channels.includes('webhook') && rule.webhook_url) {
alertLog = await this.sendWebhookAlert(rule.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' ? (rule.sms_phone_number || 'SMS_CONFIGURED') :
channel === 'email' ? (rule.email || user.email || 'EMAIL_CONFIGURED') :
(rule.webhook_url || 'WEBHOOK_CONFIGURED'),
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.`;
}
/**
* Check alert rules for a detection and trigger alerts if needed
* @param {Object} detection - The drone detection object
* @returns {Promise<Array>} - Array of triggered alerts
*/
async checkAlertRules(detection) {
try {
// Get the device to determine tenant context
const device = await Device.findByPk(detection.device_id);
if (!device) {
console.log(`Device ${detection.device_id} not found for detection`);
return [];
}
const rules = await AlertRule.findAll({
where: {
tenant_id: device.tenant_id,
is_active: true
}
});
const triggeredAlerts = [];
for (const rule of rules) {
let shouldTrigger = true;
// Check drone type filter
if (rule.drone_type !== null && rule.drone_type !== detection.drone_type) {
shouldTrigger = false;
}
// Check minimum RSSI
if (rule.min_rssi !== null && detection.rssi < rule.min_rssi) {
shouldTrigger = false;
}
// Check maximum distance (if coordinates are available)
if (rule.max_distance !== null && detection.geo_lat && detection.geo_lon) {
// Calculate distance logic would go here
// For now, assume within range
}
if (shouldTrigger) {
triggeredAlerts.push(rule);
}
}
return triggeredAlerts;
} catch (error) {
console.error('Error checking alert rules:', error);
return [];
}
}
/**
* Log an alert to the database
* @param {Object} rule - The alert rule that was triggered
* @param {Object} detection - The detection that triggered the alert
* @param {string} alertType - Type of alert (sms, email, etc.)
* @param {string} recipient - Alert recipient
* @returns {Promise<Object>} - The created alert log entry
*/
async logAlert(rule, detection, alertType = 'sms', recipient = 'test@example.com') {
try {
const alertLog = await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: alertType,
recipient: recipient,
message: `Alert triggered for ${rule.name}`,
status: 'sent',
sent_at: new Date()
});
return alertLog;
} catch (error) {
console.error('Error logging alert:', error);
throw error;
}
}
/**
* Process a detection alert workflow
* @param {Object} detection - The drone detection
* @returns {Promise<Array>} - Array of processed alerts
*/
async processDetectionAlert(detection) {
try {
const triggeredRules = await this.checkAlertRules(detection);
const processedAlerts = [];
for (const rule of triggeredRules) {
// Assess threat level
const threat = this.assessThreatLevel(detection.rssi, detection.drone_type);
// Log the alert
const alertLog = await this.logAlert(rule, detection);
// Send notification if threshold is met
if (threat.threatLevel === 'critical' || threat.threatLevel === 'high') {
await this.sendSMSAlert(detection, threat, rule);
}
processedAlerts.push({
rule,
threat,
alertLog
});
}
return processedAlerts;
} catch (error) {
console.error('Error processing detection alert:', error);
return [];
}
}
/**
* Clear expired alerts from active tracking
* @returns {Promise<number>} - Number of alerts cleared
*/
async clearExpiredAlerts() {
try {
const expiredCount = this.activeAlerts.size;
this.activeAlerts.clear();
return expiredCount;
} catch (error) {
console.error('Error clearing expired alerts:', error);
return 0;
}
}
/**
* Cleanup old alerts from the database
* @param {number} maxAge - Maximum age in milliseconds (default: 30 days)
* @returns {Promise<number>} - Number of alerts cleaned up
*/
async cleanupOldAlerts(maxAge = 30 * 24 * 60 * 60 * 1000) {
try {
const cutoffDate = new Date(Date.now() - maxAge);
const deletedCount = await AlertLog.destroy({
where: {
sent_at: {
[Op.lt]: cutoffDate
}
}
});
return deletedCount;
} catch (error) {
console.error('Error cleaning up old alerts:', error);
return 0;
}
}
}
// Export the class directly (not a singleton instance)
module.exports = AlertService;