const fs = require('node:fs/promises'); const path = require('node:path'); const ROOT_DIRECTORY = path.join(__dirname, '..'); const NOTIFICATION_CONFIG_FILE = process.env.NOTIFICATION_CONFIG_FILE ? path.resolve(process.env.NOTIFICATION_CONFIG_FILE) : path.join(ROOT_DIRECTORY, 'config', 'notifications.json'); const SEVERITY_RANK = { RECOVERY: 0, NORMAL: 0, WARNING: 1, OFFLINE: 2, CRITICAL: 3 }; const DEFAULT_NOTIFICATION_CONFIG = { enabled: false, minSeverity: 'CRITICAL', channels: { webhook: { enabled: false, url: '', headers: {} }, telegram: { enabled: false, botToken: '', chatId: '' } } }; function mergeNotificationConfig(config = {}) { return { enabled: config.enabled === true, minSeverity: SEVERITY_RANK[config.minSeverity] === undefined ? DEFAULT_NOTIFICATION_CONFIG.minSeverity : config.minSeverity, channels: { webhook: { ...DEFAULT_NOTIFICATION_CONFIG.channels.webhook, ...(config.channels && config.channels.webhook) }, telegram: { ...DEFAULT_NOTIFICATION_CONFIG.channels.telegram, ...(config.channels && config.channels.telegram) } } }; } async function readNotificationConfig() { try { const rawConfig = await fs.readFile(NOTIFICATION_CONFIG_FILE, 'utf8'); return mergeNotificationConfig(JSON.parse(rawConfig)); } catch { return mergeNotificationConfig(); } } function validateNotificationConfig(rawConfig) { const config = mergeNotificationConfig(rawConfig); if (SEVERITY_RANK[config.minSeverity] === undefined) { throw Object.assign(new Error('Severidad minima no valida.'), { status: 400 }); } if (config.channels.webhook.enabled && !isValidHttpUrl(config.channels.webhook.url)) { throw Object.assign(new Error('URL de webhook no valida.'), { status: 400 }); } if (config.channels.telegram.enabled) { if (!config.channels.telegram.botToken || !config.channels.telegram.chatId) { throw Object.assign( new Error('Telegram requiere botToken y chatId.'), { status: 400 } ); } } if ( config.channels.webhook.headers && (typeof config.channels.webhook.headers !== 'object' || Array.isArray(config.channels.webhook.headers)) ) { throw Object.assign(new Error('Headers de webhook invalidos.'), { status: 400 }); } return config; } async function writeNotificationConfig(rawConfig) { const config = validateNotificationConfig(rawConfig); await fs.mkdir(path.dirname(NOTIFICATION_CONFIG_FILE), { recursive: true }); await fs.writeFile( NOTIFICATION_CONFIG_FILE, `${JSON.stringify(config, null, 4)}\n`, 'utf8' ); return config; } function redactNotificationConfig(config) { return { ...config, channels: { webhook: { ...config.channels.webhook, headers: Object.keys(config.channels.webhook.headers || {}).length > 0 ? '[configured]' : {} }, telegram: { ...config.channels.telegram, botToken: config.channels.telegram.botToken ? '[configured]' : '' } } }; } function isValidHttpUrl(value) { try { const url = new URL(value); return ['http:', 'https:'].includes(url.protocol); } catch { return false; } } function shouldNotify(event, minSeverity) { const eventRank = SEVERITY_RANK[event.state] ?? 0; const minimumRank = SEVERITY_RANK[minSeverity] ?? SEVERITY_RANK.CRITICAL; return eventRank >= minimumRank; } function buildNotificationMessage(event) { const value = event.value === '' || event.value === null || event.value === undefined ? 'N/A' : event.value; return [ `Photobioreactor alarm: ${event.state}`, `Sensor: ${event.sensorId || event.sensor}`, `Value: ${value}`, `Message: ${event.message}`, `Time: ${event.timestamp}` ].join('\n'); } async function dispatchAlarmNotifications(events) { const config = await readNotificationConfig(); if (!config.enabled || !Array.isArray(events) || events.length === 0) { return []; } const eligibleEvents = events.filter((event) => shouldNotify(event, config.minSeverity) ); const results = []; for (const event of eligibleEvents) { if (config.channels.webhook.enabled) { results.push(await sendSafely('webhook', event, () => sendWebhookNotification(event, config.channels.webhook) )); } if (config.channels.telegram.enabled) { results.push(await sendSafely('telegram', event, () => sendTelegramNotification(event, config.channels.telegram) )); } } return results; } async function sendSafely(channel, event, send) { try { await send(); return { channel, sensorId: event.sensorId || event.sensor, state: event.state, success: true }; } catch (error) { return { channel, sensorId: event.sensorId || event.sensor, state: event.state, success: false, error: error.message }; } } async function sendWebhookNotification(event, channelConfig) { await postJson(channelConfig.url, { event, text: buildNotificationMessage(event) }, channelConfig.headers || {}); } async function sendTelegramNotification(event, channelConfig) { const url = `https://api.telegram.org/bot${channelConfig.botToken}/sendMessage`; await postJson(url, { chat_id: channelConfig.chatId, text: buildNotificationMessage(event) }); } async function postJson(url, payload, headers = {}) { if (typeof fetch !== 'function') { throw new Error('fetch no esta disponible en este runtime.'); } const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } } module.exports = { DEFAULT_NOTIFICATION_CONFIG, NOTIFICATION_CONFIG_FILE, dispatchAlarmNotifications, readNotificationConfig, redactNotificationConfig, shouldNotify, validateNotificationConfig, writeNotificationConfig };