Files
echo-core/bridge/whatsapp/index.js
MoltBot Service 80502b7931 stage-13: WhatsApp bridge with Baileys + Python adapter
Node.js bridge (bridge/whatsapp/): Baileys client with Express HTTP API
on localhost:8098 — QR code linking, message queue, reconnection logic.

Python adapter (src/adapters/whatsapp.py): polls bridge every 2s, routes
through router.py, separate whatsapp.owner/admins auth, security logging.

Integrated in main.py alongside Discord + Telegram via asyncio.gather.
CLI: echo whatsapp status/qr. 442 tests pass (32 new, zero failures).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 21:41:16 +00:00

185 lines
5.2 KiB
JavaScript

// NOTE: auth/ directory is in .gitignore — do not commit session data
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } = require('@whiskeysockets/baileys');
const express = require('express');
const pino = require('pino');
const QRCode = require('qrcode');
const path = require('path');
const PORT = 8098;
const HOST = '127.0.0.1';
const AUTH_DIR = path.join(__dirname, 'auth');
const MAX_RECONNECT_ATTEMPTS = 5;
const logger = pino({ level: 'warn' });
let sock = null;
let connected = false;
let phoneNumber = null;
let currentQR = null;
let reconnectAttempts = 0;
let messageQueue = [];
let shuttingDown = false;
// --- WhatsApp connection ---
async function startConnection() {
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
const { version } = await fetchLatestBaileysVersion();
sock = makeWASocket({
version,
auth: state,
logger,
printQRInTerminal: false,
defaultQueryTimeoutMs: 60000,
});
sock.ev.on('creds.update', saveCreds);
sock.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
try {
currentQR = await QRCode.toDataURL(qr);
console.log('[whatsapp] QR code generated — scan with WhatsApp to link');
} catch (err) {
console.error('[whatsapp] Failed to generate QR code:', err.message);
}
}
if (connection === 'open') {
connected = true;
currentQR = null;
reconnectAttempts = 0;
phoneNumber = sock.user?.id?.split(':')[0] || sock.user?.id?.split('@')[0] || null;
console.log(`[whatsapp] Connected as ${phoneNumber}`);
}
if (connection === 'close') {
connected = false;
phoneNumber = null;
const statusCode = lastDisconnect?.error?.output?.statusCode;
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
console.log(`[whatsapp] Disconnected (status: ${statusCode})`);
if (shouldReconnect && !shuttingDown) {
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
console.log(`[whatsapp] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
setTimeout(startConnection, delay);
} else {
console.error(`[whatsapp] Max reconnect attempts reached (${MAX_RECONNECT_ATTEMPTS})`);
}
} else if (statusCode === DisconnectReason.loggedOut) {
console.log('[whatsapp] Logged out — delete auth/ and restart to re-link');
}
}
});
sock.ev.on('messages.upsert', ({ messages, type }) => {
if (type !== 'notify') return;
for (const msg of messages) {
// Skip own messages
if (msg.key.fromMe) continue;
// Skip status broadcasts
if (msg.key.remoteJid === 'status@broadcast') continue;
// Only text messages
const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text;
if (!text) continue;
const isGroup = msg.key.remoteJid.endsWith('@g.us');
messageQueue.push({
from: msg.key.remoteJid,
pushName: msg.pushName || null,
text,
timestamp: msg.messageTimestamp,
id: msg.key.id,
isGroup,
});
console.log(`[whatsapp] Message from ${msg.pushName || msg.key.remoteJid}: ${text.substring(0, 80)}`);
}
});
}
// --- Express API ---
const app = express();
app.use(express.json());
app.get('/status', (_req, res) => {
res.json({
connected,
phone: phoneNumber,
qr: connected ? null : currentQR,
});
});
app.get('/qr', (_req, res) => {
if (connected) {
return res.json({ error: 'already connected' });
}
if (!currentQR) {
return res.json({ error: 'no QR code available yet' });
}
res.json({ qr: currentQR });
});
app.post('/send', async (req, res) => {
const { to, text } = req.body || {};
if (!to || !text) {
return res.status(400).json({ ok: false, error: 'missing "to" or "text" in body' });
}
if (!connected || !sock) {
return res.status(503).json({ ok: false, error: 'not connected to WhatsApp' });
}
try {
const result = await sock.sendMessage(to, { text });
res.json({ ok: true, id: result.key.id });
} catch (err) {
console.error('[whatsapp] Send failed:', err.message);
res.status(500).json({ ok: false, error: err.message });
}
});
app.get('/messages', (_req, res) => {
const messages = messageQueue.splice(0);
res.json({ messages });
});
// --- Startup ---
const server = app.listen(PORT, HOST, () => {
console.log(`[whatsapp] Bridge API listening on http://${HOST}:${PORT}`);
startConnection().catch((err) => {
console.error('[whatsapp] Failed to start connection:', err.message);
});
});
// --- Graceful shutdown ---
function shutdown(signal) {
console.log(`[whatsapp] Received ${signal}, shutting down...`);
shuttingDown = true;
if (sock) {
sock.end(undefined);
}
server.close(() => {
console.log('[whatsapp] HTTP server closed');
process.exit(0);
});
// Force exit after 5s
setTimeout(() => process.exit(1), 5000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));