/** * WhatsApp client wrapper using Baileys. * Based on OpenClaw's working implementation. */ /* eslint-disable @typescript-eslint/no-explicit-any */ import makeWASocket, { DisconnectReason, useMultiFileAuthState, fetchLatestBaileysVersion, makeCacheableSignalKeyStore, } from '@whiskeysockets/baileys'; import { Boom } from '@hapi/boom'; import qrcode from 'qrcode-terminal'; import pino from 'pino'; const VERSION = '0.1.0'; export interface InboundMessage { id: string; sender: string; pn: string; content: string; timestamp: number; isGroup: boolean; } export interface WhatsAppClientOptions { authDir: string; onMessage: (msg: InboundMessage) => void; onQR: (qr: string) => void; onStatus: (status: string) => void; } export class WhatsAppClient { private sock: any = null; private options: WhatsAppClientOptions; private reconnecting = false; constructor(options: WhatsAppClientOptions) { this.options = options; } async connect(): Promise { const logger = pino({ level: 'silent' }); const { state, saveCreds } = await useMultiFileAuthState(this.options.authDir); const { version } = await fetchLatestBaileysVersion(); console.log(`Using Baileys version: ${version.join('.')}`); // Create socket following OpenClaw's pattern this.sock = makeWASocket({ auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys, logger), }, version, logger, printQRInTerminal: false, browser: ['nanobot', 'cli', VERSION], syncFullHistory: false, markOnlineOnConnect: false, }); // Handle WebSocket errors if (this.sock.ws && typeof this.sock.ws.on === 'function') { this.sock.ws.on('error', (err: Error) => { console.error('WebSocket error:', err.message); }); } // Handle connection updates this.sock.ev.on('connection.update', async (update: any) => { const { connection, lastDisconnect, qr } = update; if (qr) { // Display QR code in terminal console.log('\nšŸ“± Scan this QR code with WhatsApp (Linked Devices):\n'); qrcode.generate(qr, { small: true }); this.options.onQR(qr); } if (connection === 'close') { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; const shouldReconnect = statusCode !== DisconnectReason.loggedOut; console.log(`Connection closed. Status: ${statusCode}, Will reconnect: ${shouldReconnect}`); this.options.onStatus('disconnected'); if (shouldReconnect && !this.reconnecting) { this.reconnecting = true; console.log('Reconnecting in 5 seconds...'); setTimeout(() => { this.reconnecting = false; this.connect(); }, 5000); } } else if (connection === 'open') { console.log('āœ… Connected to WhatsApp'); this.options.onStatus('connected'); } }); // Save credentials on update this.sock.ev.on('creds.update', saveCreds); // Handle incoming messages this.sock.ev.on('messages.upsert', async ({ messages, type }: { messages: any[]; type: string }) => { if (type !== 'notify') return; for (const msg of messages) { // Skip own messages if (msg.key.fromMe) continue; // Skip status updates if (msg.key.remoteJid === 'status@broadcast') continue; const content = this.extractMessageContent(msg); if (!content) continue; const isGroup = msg.key.remoteJid?.endsWith('@g.us') || false; this.options.onMessage({ id: msg.key.id || '', sender: msg.key.remoteJid || '', pn: msg.key.remoteJidAlt || '', content, timestamp: msg.messageTimestamp as number, isGroup, }); } }); } private extractMessageContent(msg: any): string | null { const message = msg.message; if (!message) return null; // Text message if (message.conversation) { return message.conversation; } // Extended text (reply, link preview) if (message.extendedTextMessage?.text) { return message.extendedTextMessage.text; } // Image with caption if (message.imageMessage?.caption) { return `[Image] ${message.imageMessage.caption}`; } // Video with caption if (message.videoMessage?.caption) { return `[Video] ${message.videoMessage.caption}`; } // Document with caption if (message.documentMessage?.caption) { return `[Document] ${message.documentMessage.caption}`; } // Voice/Audio message if (message.audioMessage) { return `[Voice Message]`; } return null; } async sendMessage(to: string, text: string): Promise { if (!this.sock) { throw new Error('Not connected'); } await this.sock.sendMessage(to, { text }); } async disconnect(): Promise { if (this.sock) { this.sock.end(undefined); this.sock = null; } } }