iot-agent/services/notificationService.js
2025-08-03 23:07:33 +05:30

465 lines
14 KiB
JavaScript

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 = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${notification.title}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: ${color}; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background-color: #f8f9fa; }
.footer { text-align: center; padding: 20px; color: #6c757d; font-size: 12px; }
.severity { display: inline-block; padding: 4px 8px; border-radius: 4px; color: white; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${notification.title}</h1>
<span class="severity" style="background-color: ${color};">${notification.severity.toUpperCase()}</span>
</div>
<div class="content">
<p>${notification.message}</p>
${notification.deviceId ? `<p><strong>Device ID:</strong> ${notification.deviceId}</p>` : ''}
${notification.data ? `<p><strong>Details:</strong> ${JSON.stringify(notification.data, null, 2)}</p>` : ''}
<p><strong>Timestamp:</strong> ${new Date().toLocaleString()}</p>
</div>
<div class="footer">
<p>This is an automated notification from the AI Agent IoT Dashboard.</p>
</div>
</div>
</body>
</html>
`;
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();