You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
6.7 KiB
JavaScript
246 lines
6.7 KiB
JavaScript
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
|
|
};
|