const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } = require('@whiskeysockets/baileys'); const express = require('express'); const QRCode = require('qrcode'); const pino = require('pino'); const app = express(); app.use(express.json()); // Configurable via environment variables const PORT = process.env.PORT || 21465; const API_KEY = process.env.API_KEY || 'nexus-wpp-secret-2026'; const TENANT_ID = process.env.TENANT_ID || ''; const WEBHOOK_BASE = process.env.WEBHOOK_BASE || 'http://localhost:5001/pos/api/whatsapp/webhook'; const WEBHOOK_URL = TENANT_ID ? `${WEBHOOK_BASE}?tenant_id=${TENANT_ID}` : WEBHOOK_BASE; const AUTH_DIR = process.env.AUTH_DIR || '/app/auth'; let sock = null; let qrCode = null; let connectionState = 'disconnected'; const logger = pino({ level: process.env.LOG_LEVEL || 'warn' }); async function connectWhatsApp() { const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR); const { version } = await fetchLatestBaileysVersion(); console.log(`[Tenant ${TENANT_ID}] Connecting with Baileys v` + version.join('.')); connectionState = 'connecting'; sock = makeWASocket({ version, auth: state, logger, printQRInTerminal: true, browser: ['Nexus POS', 'Chrome', '120.0'] }); sock.ev.on('creds.update', saveCreds); sock.ev.on('connection.update', async (update) => { const { connection, lastDisconnect, qr } = update; if (qr) { qrCode = await QRCode.toDataURL(qr); connectionState = 'qr'; console.log(`[Tenant ${TENANT_ID}] QR code generated!`); } if (connection === 'close') { connectionState = 'disconnected'; qrCode = null; const reason = lastDisconnect?.error?.output?.statusCode; if (reason !== DisconnectReason.loggedOut) { setTimeout(connectWhatsApp, 5000); } } if (connection === 'open') { connectionState = 'open'; qrCode = null; console.log(`[Tenant ${TENANT_ID}] Connected!`); } }); sock.ev.on('messages.upsert', async ({ messages }) => { for (const msg of messages) { if (msg.key.fromMe) continue; const phone = msg.key.remoteJid.replace('@s.whatsapp.net', ''); const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text || ''; console.log(`[Tenant ${TENANT_ID}] From ${phone}: ${text}`); try { await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event: 'messages.upsert', data: { key: msg.key, message: msg.message, messageTimestamp: msg.messageTimestamp } }) }); } catch (e) { console.log(`[Tenant ${TENANT_ID}] Webhook failed:`, e.message); } } }); } app.get('/', (req, res) => res.json({ status: 'ok', service: 'Nexus WhatsApp Bridge', tenant: TENANT_ID, state: connectionState })); app.get('/status', (req, res) => res.json({ state: connectionState, hasQr: !!qrCode })); app.get('/qr', (req, res) => { if (connectionState === 'open') return res.json({ state: 'open', message: 'Already connected' }); if (!qrCode) return res.json({ state: connectionState, qr: null, message: 'QR not ready' }); res.json({ state: 'qr', qr: qrCode }); }); app.post('/connect', async (req, res) => { if (!sock) connectWhatsApp(); res.json({ state: connectionState }); }); app.post('/send', async (req, res) => { if (connectionState !== 'open') return res.status(400).json({ error: 'Not connected' }); const { phone, message } = req.body; const jid = phone.includes('@') ? phone : phone + '@s.whatsapp.net'; try { const r = await sock.sendMessage(jid, { text: message }); res.json({ success: true, id: r.key.id }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/logout', async (req, res) => { if (sock) { await sock.logout(); sock = null; } qrCode = null; connectionState = 'disconnected'; res.json({ state: 'disconnected' }); }); app.listen(PORT, () => { console.log(`[Tenant ${TENANT_ID}] WhatsApp Bridge on port ${PORT}`); connectWhatsApp(); });