const nodemailer = require('nodemailer'); const twilio = require('twilio'); const logger = require('../utils/logger'); const database = require('../config/database'); const redis = require('../config/redis'); class NotificationService { constructor() { this.emailTransporter = null; this.twilioClient = null; this.isInitialized = false; } async initialize() { try { await this.setupEmailTransporter(); await this.setupTwilioClient(); this.isInitialized = true; logger.info('Notification service initialized successfully'); } catch (error) { logger.error('Failed to initialize Notification service:', error); throw error; } } async setupEmailTransporter() { try { this.emailTransporter = nodemailer.createTransporter({ host: process.env.SMTP_HOST, port: process.env.SMTP_PORT, secure: process.env.SMTP_PORT === '465', auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } }); // Verify connection await this.emailTransporter.verify(); logger.info('Email transporter configured successfully'); } catch (error) { logger.error('Failed to setup email transporter:', error); this.emailTransporter = null; } } async setupTwilioClient() { try { if (process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN) { this.twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); logger.info('Twilio client configured successfully'); } else { logger.warn('Twilio credentials not provided, SMS notifications disabled'); } } catch (error) { logger.error('Failed to setup Twilio client:', error); this.twilioClient = null; } } async sendNotification(userId, notification) { try { const startTime = Date.now(); // Get user notification preferences const user = await this.getUserNotificationPreferences(userId); if (!user) { logger.warn(`User ${userId} not found for notification`); return false; } // Store notification in database const notificationId = await this.storeNotification(userId, notification); // Send notifications based on user preferences const results = { email: false, sms: false, inApp: true // Always store in-app }; // Send email if enabled if (user.email_enabled && user.email) { results.email = await this.sendEmail(user.email, notification); } // Send SMS if enabled if (user.sms_enabled && user.phone && this.twilioClient) { results.sms = await this.sendSMS(user.phone, notification); } // Update notification status await this.updateNotificationStatus(notificationId, results); const processingTime = Date.now() - startTime; logger.logNotification('multi_channel', userId, notification.title, results); logger.logPerformance('notification_sending', processingTime, { userId, channels: Object.keys(results).filter(k => results[k]).length }); return results; } catch (error) { logger.error('Error sending notification:', error); return false; } } async sendEmail(recipients, notification) { try { if (!this.emailTransporter) { logger.warn('Email transporter not configured'); return false; } const emailContent = this.formatEmailContent(notification); const mailOptions = { from: process.env.SMTP_USER, to: Array.isArray(recipients) ? recipients.join(',') : recipients, subject: emailContent.subject, html: emailContent.html, text: emailContent.text }; const result = await this.emailTransporter.sendMail(mailOptions); logger.logNotification('email', recipients, notification.title, 'sent'); return true; } catch (error) { logger.error('Error sending email:', error); return false; } } async sendSMS(recipients, notification) { try { if (!this.twilioClient) { logger.warn('Twilio client not configured'); return false; } const message = this.formatSMSContent(notification); const phoneNumbers = Array.isArray(recipients) ? recipients : [recipients]; const results = await Promise.allSettled( phoneNumbers.map(phone => this.twilioClient.messages.create({ body: message, from: process.env.TWILIO_PHONE_NUMBER, to: phone }) ) ); const successCount = results.filter(r => r.status === 'fulfilled').length; const success = successCount === phoneNumbers.length; if (success) { logger.logNotification('sms', recipients, notification.title, 'sent'); } else { logger.error('Some SMS messages failed to send'); } return success; } catch (error) { logger.error('Error sending SMS:', error); return false; } } async sendWebhook(url, data) { try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }); const success = response.ok; if (success) { logger.logNotification('webhook', url, 'Webhook notification', 'sent'); } else { logger.error(`Webhook failed with status: ${response.status}`); } return success; } catch (error) { logger.error('Error sending webhook:', error); return false; } } formatEmailContent(notification) { const severityColors = { critical: '#dc3545', warning: '#ffc107', info: '#17a2b8', success: '#28a745' }; const color = severityColors[notification.severity] || '#6c757d'; const html = ` ${notification.title}

${notification.title}

${notification.severity.toUpperCase()}

${notification.message}

${notification.deviceId ? `

Device ID: ${notification.deviceId}

` : ''} ${notification.data ? `

Details: ${JSON.stringify(notification.data, null, 2)}

` : ''}

Timestamp: ${new Date().toLocaleString()}

`; const text = ` ${notification.title} Severity: ${notification.severity.toUpperCase()} ${notification.message} ${notification.deviceId ? `Device ID: ${notification.deviceId}` : ''} ${notification.data ? `Details: ${JSON.stringify(notification.data)}` : ''} Timestamp: ${new Date().toLocaleString()} This is an automated notification from the AI Agent IoT Dashboard. `; return { subject: `[${notification.severity.toUpperCase()}] ${notification.title}`, html: html, text: text }; } formatSMSContent(notification) { let message = `${notification.severity.toUpperCase()}: ${notification.title}`; if (notification.message) { message += `\n${notification.message}`; } if (notification.deviceId) { message += `\nDevice: ${notification.deviceId}`; } return message.substring(0, 160); // SMS character limit } async getUserNotificationPreferences(userId) { try { const [users] = await database.query( `SELECT id, username, email, phone, email_enabled, sms_enabled, notification_preferences FROM users WHERE id = ? AND status = "active"`, [userId] ); if (users.length === 0) { return null; } const user = users[0]; const preferences = JSON.parse(user.notification_preferences || '{}'); return { ...user, notification_preferences: preferences }; } catch (error) { logger.error('Error getting user notification preferences:', error); return null; } } async storeNotification(userId, notification) { try { const result = await database.query( `INSERT INTO notifications (user_id, type, title, message, severity, device_id, data, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())`, [ userId, notification.type, notification.title, notification.message, notification.severity, notification.deviceId, JSON.stringify(notification.data || {}), 'sent' ] ); return result.insertId; } catch (error) { logger.error('Failed to store notification:', error); throw error; } } async updateNotificationStatus(notificationId, results) { try { const status = results.email || results.sms ? 'delivered' : 'failed'; const deliveryInfo = JSON.stringify(results); await database.query( 'UPDATE notifications SET status = ?, delivery_info = ?, updated_at = NOW() WHERE id = ?', [status, deliveryInfo, notificationId] ); } catch (error) { logger.error('Failed to update notification status:', error); } } async getUserNotifications(userId, limit = 50, offset = 0) { try { const notifications = await database.query( `SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`, [userId, limit, offset] ); return notifications; } catch (error) { logger.error('Error getting user notifications:', error); return []; } } async markNotificationAsRead(notificationId, userId) { try { await database.query( 'UPDATE notifications SET read_at = NOW() WHERE id = ? AND user_id = ?', [notificationId, userId] ); logger.info(`Notification ${notificationId} marked as read by user ${userId}`); } catch (error) { logger.error('Error marking notification as read:', error); throw error; } } async markAllNotificationsAsRead(userId) { try { await database.query( 'UPDATE notifications SET read_at = NOW() WHERE user_id = ? AND read_at IS NULL', [userId] ); logger.info(`All notifications marked as read for user ${userId}`); } catch (error) { logger.error('Error marking all notifications as read:', error); throw error; } } async deleteNotification(notificationId, userId) { try { await database.query( 'DELETE FROM notifications WHERE id = ? AND user_id = ?', [notificationId, userId] ); logger.info(`Notification ${notificationId} deleted by user ${userId}`); } catch (error) { logger.error('Error deleting notification:', error); throw error; } } async getNotificationStatistics(userId = null) { try { let query = ` SELECT type, severity, status, COUNT(*) as count, DATE(created_at) as date FROM notifications WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) `; const params = []; if (userId) { query += ' AND user_id = ?'; params.push(userId); } query += ' GROUP BY type, severity, status, DATE(created_at) ORDER BY date DESC'; const stats = await database.query(query, params); return stats; } catch (error) { logger.error('Error getting notification statistics:', error); return []; } } async cleanupOldNotifications(daysToKeep = 30) { try { const result = await database.query( 'DELETE FROM notifications WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)', [daysToKeep] ); logger.info(`Cleaned up ${result.affectedRows} old notifications`); return result.affectedRows; } catch (error) { logger.error('Error cleaning up old notifications:', error); return 0; } } async healthCheck() { try { const emailStatus = this.emailTransporter ? 'configured' : 'not_configured'; const smsStatus = this.twilioClient ? 'configured' : 'not_configured'; return { status: this.isInitialized ? 'healthy' : 'not_initialized', message: this.isInitialized ? 'Notification service is healthy' : 'Service not initialized', email: emailStatus, sms: smsStatus }; } catch (error) { return { status: 'unhealthy', message: 'Notification service health check failed', error: error.message }; } } } module.exports = new NotificationService();