import nodemailer from 'nodemailer'; const env = process.env; // ---- Telegram ---- async function sendTelegram(text) { const token = env.TELEGRAM_BOT_TOKEN; const chats = (env.TELEGRAM_CHAT_IDS || '').split(',').map(s => s.trim()).filter(Boolean); if (!token || chats.length === 0) return null; const results = []; for (const chatId of chats) { try { const r = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text, parse_mode: 'HTML', disable_notification: false }) }); if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`); results.push({ chat: chatId, ok: true }); } catch (e) { results.push({ chat: chatId, ok: false, error: e.message }); } } return results; } // ---- ntfy.sh push ---- async function sendNtfy(text, title, priority) { const topic = env.NTFY_TOPIC; if (!topic) return null; const server = env.NTFY_SERVER || 'https://ntfy.sh'; try { const r = await fetch(`${server}/${topic}`, { method: 'POST', headers: { 'Title': title || 'Alerta Shivao', 'Priority': priority || 'urgent', 'Tags': 'rotating_light,anchor,warning', 'Click': text.match(/https:\/\/maps[^\s]+/)?.[0] || '' }, body: text }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return { ok: true }; } catch (e) { return { ok: false, error: e.message }; } } // ---- Email ---- let transporter = null; function getEmailTransporter() { if (transporter) return transporter; if (!env.SMTP_HOST || !env.SMTP_USER) return null; transporter = nodemailer.createTransport({ host: env.SMTP_HOST, port: parseInt(env.SMTP_PORT || '587'), secure: env.SMTP_SECURE === 'true', auth: { user: env.SMTP_USER, pass: env.SMTP_PASS } }); return transporter; } async function sendEmail(text, subject) { const t = getEmailTransporter(); const recipients = (env.SMTP_TO || '').split(',').map(s => s.trim()).filter(Boolean); if (!t || recipients.length === 0) return null; try { await t.sendMail({ from: env.SMTP_FROM || env.SMTP_USER, to: recipients.join(','), subject: subject || 'Alerta Shivao', text, html: `
${text.replace(/`
});
return { ok: true, recipients: recipients.length };
} catch (e) {
return { ok: false, error: e.message };
}
}
// ---- Twilio SMS / WhatsApp ----
async function twilioRequest(messages) {
const sid = env.TWILIO_ACCOUNT_SID;
const token = env.TWILIO_AUTH_TOKEN;
if (!sid || !token) return null;
const auth = 'Basic ' + Buffer.from(`${sid}:${token}`).toString('base64');
const url = `https://api.twilio.com/2010-04-01/Accounts/${sid}/Messages.json`;
const results = [];
for (const msg of messages) {
try {
const body = new URLSearchParams({ From: msg.from, To: msg.to, Body: msg.body });
const r = await fetch(url, { method: 'POST', headers: { 'Authorization': auth, 'Content-Type': 'application/x-www-form-urlencoded' }, body });
if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`);
results.push({ to: msg.to, ok: true });
} catch (e) {
results.push({ to: msg.to, ok: false, error: e.message });
}
}
return results;
}
async function sendSMS(text) {
const from = env.TWILIO_FROM_NUMBER;
const tos = (env.TWILIO_SMS_TO || '').split(',').map(s => s.trim()).filter(Boolean);
if (!from || tos.length === 0) return null;
const messages = tos.map(to => ({ from, to, body: text }));
return twilioRequest(messages);
}
async function sendWhatsApp(text) {
const from = env.TWILIO_WHATSAPP_FROM;
const tos = (env.TWILIO_WHATSAPP_TO || '').split(',').map(s => s.trim()).filter(Boolean);
if (!from || tos.length === 0) return null;
const messages = tos.map(to => ({
from: from.startsWith('whatsapp:') ? from : `whatsapp:${from}`,
to: to.startsWith('whatsapp:') ? to : `whatsapp:${to}`,
body: text
}));
return twilioRequest(messages);
}
// ---- Generic webhook ----
async function sendWebhook(payload) {
const url = env.WEBHOOK_URL;
if (!url) return null;
try {
const r = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return { ok: true };
} catch (e) {
return { ok: false, error: e.message };
}
}
// ---- Build message ----
export function buildAlarmMessage(p) {
const lat = p.lat?.toFixed(6) ?? '?';
const lng = p.lng?.toFixed(6) ?? '?';
const mapsUrl = (p.lat && p.lng) ? `https://maps.google.com/?q=${lat},${lng}` : '';
const dist = Math.round(p.distance || 0);
const time = new Date(p.ts || Date.now()).toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' });
const boat = p.boat || 'Veleiro';
const reason = p.reason === 'heartbeat_lost' ?
'Servidor perdeu contato com o celular do barco' :
`Saiu ${dist}m da posição de fundeio (raio ${p.radius || '?'}m)`;
return `🚨 ALERTA — ${boat}
${reason}.
📍 Posição: ${lat}, ${lng}
${mapsUrl ? `🗺️ ${mapsUrl}\n` : ''}🕐 ${time}
Esta é uma mensagem automática do sistema de vigia de fundeio.`;
}
// ---- Main dispatcher ----
export async function dispatchAlarm(payload) {
const text = buildAlarmMessage(payload);
const subject = `🚨 ${payload.boat || 'Veleiro'} — ALERTA de fundeio`;
const tasks = [
['telegram', sendTelegram(text)],
['ntfy', sendNtfy(text, subject, 'urgent')],
['email', sendEmail(text, subject)],
['sms', sendSMS(text)],
['whatsapp', sendWhatsApp(text)],
['webhook', sendWebhook({ ...payload, message: text })]
];
const sent = [];
const failed = [];
for (const [name, promise] of tasks) {
const result = await promise;
if (result === null) continue; // not configured
if (Array.isArray(result)) {
for (const r of result) {
if (r.ok) sent.push(name);
else failed.push({ channel: name, ...r });
}
} else {
if (result.ok) sent.push(name);
else failed.push({ channel: name, ...result });
}
}
return { sent, failed };
}
// Quick test ping (no urgency)
export async function dispatchTest() {
const text = `✅ Teste — Sistema Shivao Cloud operacional.\nHora: ${new Date().toLocaleString('pt-BR')}`;
const sent = [];
const failed = [];
const r1 = await sendTelegram(text);
if (r1) r1.forEach(r => r.ok ? sent.push('telegram') : failed.push({ channel: 'telegram', ...r }));
const r2 = await sendNtfy(text, 'Teste Shivao', 'default');
if (r2) r2.ok ? sent.push('ntfy') : failed.push({ channel: 'ntfy', ...r2 });
const r3 = await sendEmail(text, 'Teste Shivao');
if (r3) r3.ok ? sent.push('email') : failed.push({ channel: 'email', ...r3 });
return { sent, failed };
}
export function listConfiguredChannels() {
const channels = [];
if (env.TELEGRAM_BOT_TOKEN && env.TELEGRAM_CHAT_IDS) channels.push('telegram');
if (env.NTFY_TOPIC) channels.push('ntfy');
if (env.SMTP_HOST && env.SMTP_TO) channels.push('email');
if (env.TWILIO_ACCOUNT_SID && env.TWILIO_SMS_TO) channels.push('sms');
if (env.TWILIO_ACCOUNT_SID && env.TWILIO_WHATSAPP_TO) channels.push('whatsapp');
if (env.WEBHOOK_URL) channels.push('webhook');
return channels;
}