import express from 'express'; import axios from 'axios'; import morgan from 'morgan'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc.js'; // ⬅ OJO: “.js” si usás ESM puro dayjs.extend(utc); // ─────────────────────────────────────────── // ⚙️ Variables de entorno (con defaults) const VERSION = '0.3.21'; const API_URL = process.env.BOT_API_URL ?? 'http://whatsapp-bot:8002'; const GROUP_ID = process.env.GROUP_ID ?? '120363203056794284@g.us'; const REPLY_MSG = process.env.REPLY_MSG ?? 'que pedos'; const PORT = +(process.env.PORT ?? 4000); const LOG_LEVEL = process.env.LOG_LEVEL ?? 'debug'; const RETRY_MS = +(process.env.RETRY_MS ?? 5_000); // 5 s const MAX_ATTEMPTS = +(process.env.MAX_ATTEMPTS ?? 60);// 5 min máx // ─────────────────────────────────────────── // ► Logger interno function log(level, ...args) { const levels = ['debug', 'info', 'warn', 'error']; if (levels.indexOf(level) >= levels.indexOf(LOG_LEVEL)) { console[level === 'debug' ? 'log' : level]( `[${dayjs().utc().format()}]`, level.toUpperCase(), ...args ); } } // ─────────────────────────────────────────── const app = express(); app.use(express.json({ limit: '1mb' })); app.use(morgan('[:date[iso]] :method :url :status - :response-time ms')); // ► Util para enviar texto async function sendText(to, content) { log('info', `Enviando mensaje a ${to}: "${content}"`); const { data } = await axios.post(`${API_URL}/sendText`, { args: { to, content } }); log('debug', 'Respuesta de /sendText →', data); return data; } // ► Procesar mensajes entrantes async function processIncoming(msg) { const text = msg.body ?? msg.text ?? ''; // 🧠 Extraemos info útil const info = { idMensaje: msg.id ?? msg.mId ?? null, texto: text, esDeGrupo: msg.isGroupMsg ?? false, grupoId: msg.chatId ?? null, nombreGrupo: msg.chat?.name ?? msg.chat?.formattedTitle ?? null, participantesGrupo: msg.chat?.participantsCount ?? null, esPrivado: !(msg.isGroupMsg ?? false), autorId: msg.sender?.id ?? msg.author ?? null, autorNombre: msg.sender?.name ?? null, autorPushName: msg.sender?.pushname ?? null, autorEsContacto: msg.sender?.isMyContact ?? false, reenviado: msg.isForwarded ?? false, menciones: msg.mentionedJidList ?? [], citandoMensaje: msg.isQuotedMsgAvailable ?? false, tieneReaccion: msg.hasReaction ?? false, tipo: msg.type ?? 'chat', timestamp: msg.timestamp ?? null, fecha: msg.timestamp ? dayjs.unix(msg.timestamp).format('YYYY-MM-DD HH:mm:ss') : null, }; // 📋 Logueamos limpio log('debug', '↪︎ Mensaje IN procesado', info); // 🚀 Acción si menciona al bot if (/@nucleo/i.test(info.texto)) { await sendText(GROUP_ID, REPLY_MSG); } // 🚀 Acción si pide repetir if (/@nucleoRepeti/i.test(info.texto)) { const partes = info.texto.split(/@nucleoRepeti/i); const contenido = (partes[1]?.trim() || 'vacio'); log('info', `📢 Reenviando: "${contenido}" al chat ${info.grupoId}`); await sendText(info.grupoId, contenido); } } // ──────── ENDPOINTS ───────────────────────── app.post('/webhook', async (req, res) => { const { event, data } = req.body; log('debug', `📩 Webhook event "${event}"`); if (event === 'onMessage' || event === 'onAnyMessage') await processIncoming(data); res.sendStatus(200); }); app.get('/debug/scan', async (_req, res) => { const { data } = await axios.post(`${API_URL}/loadAndGetAllMessagesInChat`, { args: { chatId: GROUP_ID, includeMe: 'true', includeNotifications: 'false' } }); const msgs = (data?.response ?? []).slice(-20); log('info', `Escaneando ${msgs.length} mensajes recientes…`); for (const m of msgs) await processIncoming(m); res.json({ ok: true, scanned: msgs.length }); }); app.get('/debug/send', async (req, res) => { const text = req.query.msg ?? REPLY_MSG; const resp = await sendText(GROUP_ID, text); res.json({ ok: true, resp }); }); app.get('/debug/version', (_req, res) => { res.json({ version: VERSION }); }); // ────────── INICIALIZACIÓN ────────────────── async function waitForGateway() { for (let i = 1; i <= MAX_ATTEMPTS; i++) { try { await axios.get(`${API_URL}/api-docs`); // endpoint de salud de open-wa log('info', '🟢 whatsapp-gateway listo'); return; } catch { log('warn', `Gateway no responde (intento ${i}/${MAX_ATTEMPTS})…`); await new Promise(r => setTimeout(r, RETRY_MS)); } } throw new Error('whatsapp-gateway no respondió a tiempo'); } async function clearWebhooks() { try { const { data } = await axios.post(`${API_URL}/listWebhooks`); const hooks = data?.response ?? []; if (!hooks.length) { log('info', 'Sin webhooks previos que limpiar'); return; } log('info', `Eliminando ${hooks.length} webhooks…`); const results = await Promise.allSettled( hooks.map(h => axios.post(`${API_URL}/removeWebhook`, { args: { webhookId: h.id } })) // ⬆️ NOTA: ahora es "webhookId", no "id" ); results.forEach((result, idx) => { const id = hooks[idx].id; if (result.status === 'fulfilled' && result.value?.data?.response === true) { log('debug', `✔️ Eliminado webhook ${id}`); } else { log('warn', `⚠️ Falló eliminar webhook ${id}`); } }); const okCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.response === true).length; log('info', `Limpieza OK (${okCount}/${hooks.length} eliminados)`); } catch (e) { log('error', 'Fallo limpiando webhooks:', e.response?.data ?? e.message); } } async function registerWebhook() { const url = `http://nucleo-bot:${PORT}/webhook`; const { data } = await axios.post(`${API_URL}/registerWebhook`, { args: { url, events: ['onAnyMessage'], id: 'nucleo-bot' } }); log('info', '✔️ Webhook registrado:', data); } async function bootstrap() { await waitForGateway(); await clearWebhooks(); await registerWebhook(); } app.listen(PORT, () => { log('info', `🪪 Versión del bot: ${VERSION}`); log('info', `🚀 nucleo‑bot escuchando en :${PORT}`); bootstrap().catch(err => log('error', 'Error en bootstrap:', err.message)); });