/** * BaileysManager - Manages multiple WhatsApp instances * Singleton pattern for managing all Baileys connections */ import type { WASocket, BaileysEventMap } from '@whiskeysockets/baileys' import * as baileys from '@whiskeysockets/baileys' // Get functions from baileys module (handle both ESM and CJS) const makeWASocket = (baileys as any).default || (baileys as any).makeWASocket const DisconnectReason = (baileys as any).DisconnectReason || (baileys as any).default?.DisconnectReason const fetchLatestBaileysVersion = (baileys as any).fetchLatestBaileysVersion || (baileys as any).default?.fetchLatestBaileysVersion const makeCacheableSignalKeyStore = (baileys as any).makeCacheableSignalKeyStore || (baileys as any).default?.makeCacheableSignalKeyStore const Browsers = (baileys as any).Browsers || (baileys as any).default?.Browsers // Debug: Log what we got from baileys console.log('[Baileys] Module keys:', Object.keys(baileys)) console.log('[Baileys] makeWASocket:', typeof makeWASocket) console.log('[Baileys] DisconnectReason:', typeof DisconnectReason) console.log('[Baileys] fetchLatestBaileysVersion:', typeof fetchLatestBaileysVersion) console.log('[Baileys] makeCacheableSignalKeyStore:', typeof makeCacheableSignalKeyStore) console.log('[Baileys] Browsers:', typeof Browsers) import { Boom } from '@hapi/boom' import { EventEmitter } from 'events' import QRCode from 'qrcode' import pino from 'pino' import { usePostgresAuthState, clearAuthState } from './auth-state' import { query } from '../../utils/database' // Types export interface ManagedInstance { id: string name: string socket: WASocket | null status: 'disconnected' | 'connecting' | 'connected' | 'qr_ready' | 'pairing' phoneNumber: string | null qrCode: string | null pairingCode: string | null reconnectAttempts: number lastError: string | null } export interface InstanceEvents { 'instance.status': { instanceId: string; status: string; phoneNumber?: string } 'instance.qr': { instanceId: string; qr: string; qrDataUrl: string } 'instance.pairing': { instanceId: string; code: string } 'message.received': { instanceId: string; message: any } 'message.sent': { instanceId: string; message: any } 'message.status': { instanceId: string; messageId: string; status: string } } const logger = pino({ level: 'warn' }) class BaileysManager extends EventEmitter { private instances: Map = new Map() private static instance: BaileysManager | null = null private initialized = false private constructor() { super() } static getInstance(): BaileysManager { if (!BaileysManager.instance) { BaileysManager.instance = new BaileysManager() } return BaileysManager.instance } /** * Initialize manager and reconnect previously connected instances */ async initialize(): Promise { if (this.initialized) return console.log('[BaileysManager] Initializing...') try { // Load instances that were previously connected const result = await query<{ id: string; name: string; phone_number: string }>( `SELECT id, name, phone_number FROM instances WHERE status = 'connected'` ) for (const row of result.rows) { console.log(`[BaileysManager] Reconnecting instance: ${row.name}`) await this.connect(row.id) } this.initialized = true console.log('[BaileysManager] Initialized successfully') } catch (error) { console.error('[BaileysManager] Initialization error:', error) } } /** * Connect an instance to WhatsApp */ async connect(instanceId: string, usePairingCode = false, phoneNumber?: string): Promise { // Check if already connected const existing = this.instances.get(instanceId) if (existing?.socket) { console.log(`[BaileysManager] Instance ${instanceId} already has active socket`) return } // Load instance from DB const instanceResult = await query<{ id: string; name: string; phone_number: string }>( 'SELECT id, name, phone_number FROM instances WHERE id = $1', [instanceId] ) if (instanceResult.rows.length === 0) { throw new Error(`Instance ${instanceId} not found`) } const instanceData = instanceResult.rows[0] // Update status await this.updateInstanceStatus(instanceId, 'connecting') // Create managed instance const managed: ManagedInstance = { id: instanceId, name: instanceData.name, socket: null, status: 'connecting', phoneNumber: instanceData.phone_number || null, qrCode: null, pairingCode: null, reconnectAttempts: 0, lastError: null } this.instances.set(instanceId, managed) try { console.log(`[BaileysManager] Loading auth state for ${instanceId}...`) // Load auth state from PostgreSQL const { state, saveCreds } = await usePostgresAuthState(instanceId) console.log(`[BaileysManager] Auth state loaded, creds registered:`, state.creds?.registered) console.log(`[BaileysManager] Fetching latest Baileys version...`) const { version } = await fetchLatestBaileysVersion() console.log(`[BaileysManager] Using Baileys version:`, version) console.log(`[BaileysManager] Creating WASocket...`) // Create socket const socket = makeWASocket({ version, auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys, logger) }, browser: Browsers.ubuntu('WhatsApp Nucleo'), logger: pino({ level: 'debug' }), generateHighQualityLinkPreview: true, syncFullHistory: false, markOnlineOnConnect: false }) console.log(`[BaileysManager] WASocket created successfully`) managed.socket = socket // Setup event handlers console.log(`[BaileysManager] Setting up event handlers...`) this.setupEventHandlers(instanceId, socket, saveCreds, usePairingCode, phoneNumber) console.log(`[BaileysManager] Event handlers set up, waiting for connection events...`) } catch (error) { console.error(`[BaileysManager] Error connecting instance ${instanceId}:`, error) managed.lastError = (error as Error).message await this.updateInstanceStatus(instanceId, 'disconnected') throw error } } /** * Setup event handlers for a socket */ private setupEventHandlers( instanceId: string, socket: WASocket, saveCreds: () => Promise, usePairingCode: boolean, phoneNumber?: string ): void { const managed = this.instances.get(instanceId)! // Credentials update socket.ev.on('creds.update', saveCreds) // Connection update socket.ev.on('connection.update', async (update) => { console.log(`[BaileysManager] connection.update event:`, JSON.stringify(update, null, 2)) const { connection, lastDisconnect, qr } = update // QR Code received if (qr) { console.log(`[BaileysManager] QR code received! Length: ${qr.length}`) try { const qrDataUrl = await QRCode.toDataURL(qr) console.log(`[BaileysManager] QR DataURL generated, length: ${qrDataUrl.length}`) managed.qrCode = qrDataUrl managed.status = 'qr_ready' await this.updateInstanceStatus(instanceId, 'qr_ready', { qr_code: qrDataUrl }) console.log(`[BaileysManager] QR saved to database`) this.emit('instance.qr', { instanceId, qr, qrDataUrl }) } catch (err) { console.error(`[BaileysManager] Error generating QR:`, err) } } // Request pairing code if needed if (usePairingCode && phoneNumber && !managed.pairingCode && connection === 'connecting') { try { // Wait a bit for socket to be ready await new Promise(resolve => setTimeout(resolve, 3000)) if (!socket.authState.creds.registered) { const code = await socket.requestPairingCode(phoneNumber) managed.pairingCode = code managed.status = 'pairing' await this.updateInstanceStatus(instanceId, 'pairing', { pairing_code: code }) this.emit('instance.pairing', { instanceId, code }) } } catch (error) { console.error(`[BaileysManager] Error requesting pairing code:`, error) } } // Connection closed if (connection === 'close') { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode const shouldReconnect = statusCode !== DisconnectReason.loggedOut managed.socket = null managed.qrCode = null managed.pairingCode = null if (shouldReconnect && managed.reconnectAttempts < 5) { managed.reconnectAttempts++ console.log(`[BaileysManager] Reconnecting instance ${instanceId} (attempt ${managed.reconnectAttempts})`) // Wait before reconnecting await new Promise(resolve => setTimeout(resolve, 3000 * managed.reconnectAttempts)) await this.connect(instanceId) } else { managed.status = 'disconnected' await this.updateInstanceStatus(instanceId, 'disconnected') if (statusCode === DisconnectReason.loggedOut) { // Clear auth state if logged out await clearAuthState(instanceId) } } this.emit('instance.status', { instanceId, status: 'disconnected' }) } // Connection open if (connection === 'open') { const phoneNum = socket.user?.id.split(':')[0] || null managed.status = 'connected' managed.phoneNumber = phoneNum managed.qrCode = null managed.pairingCode = null managed.reconnectAttempts = 0 await this.updateInstanceStatus(instanceId, 'connected', { phone_number: phoneNum, qr_code: null, pairing_code: null, last_connected_at: new Date() }) this.emit('instance.status', { instanceId, status: 'connected', phoneNumber: phoneNum || undefined }) console.log(`[BaileysManager] Instance ${instanceId} connected as ${phoneNum}`) } }) // Messages received socket.ev.on('messages.upsert', async ({ messages, type }) => { for (const msg of messages) { // Emit event for webhooks this.emit('message.received', { instanceId, message: msg }) // Save message to database await this.saveMessage(instanceId, msg, type === 'notify') } }) // Message status update socket.ev.on('messages.update', async (updates) => { for (const update of updates) { if (update.update.status) { this.emit('message.status', { instanceId, messageId: update.key.id!, status: this.mapMessageStatus(update.update.status) }) } } }) } /** * Disconnect an instance */ async disconnect(instanceId: string): Promise { const managed = this.instances.get(instanceId) if (!managed?.socket) return managed.socket.end(new Error('Disconnected by user')) managed.socket = null managed.status = 'disconnected' managed.qrCode = null managed.pairingCode = null await this.updateInstanceStatus(instanceId, 'disconnected') this.emit('instance.status', { instanceId, status: 'disconnected' }) } /** * Send a message */ async sendMessage(instanceId: string, jid: string, content: any): Promise { const managed = this.instances.get(instanceId) if (!managed?.socket) { throw new Error('Instance not connected') } const result = await managed.socket.sendMessage(jid, content) this.emit('message.sent', { instanceId, message: result }) return result } /** * Get QR code for an instance */ getQRCode(instanceId: string): string | null { return this.instances.get(instanceId)?.qrCode || null } /** * Get pairing code for an instance */ getPairingCode(instanceId: string): string | null { return this.instances.get(instanceId)?.pairingCode || null } /** * Get instance status */ getStatus(instanceId: string): ManagedInstance | null { return this.instances.get(instanceId) || null } /** * Get all instances */ getAllInstances(): ManagedInstance[] { return Array.from(this.instances.values()) } /** * Update instance status in database */ private async updateInstanceStatus( instanceId: string, status: string, extra: Record = {} ): Promise { const fields = ['status = $2'] const values: any[] = [instanceId, status] let paramIndex = 3 for (const [key, value] of Object.entries(extra)) { fields.push(`${key} = $${paramIndex}`) values.push(value) paramIndex++ } await query( `UPDATE instances SET ${fields.join(', ')}, updated_at = NOW() WHERE id = $1`, values ) } /** * Save message to database */ private async saveMessage(instanceId: string, msg: any, isNew: boolean): Promise { try { const jid = msg.key.remoteJid if (!jid) return // Ensure chat exists const chatResult = await query<{ id: string }>( `INSERT INTO chats (instance_id, jid, name, is_group) VALUES ($1, $2, $3, $4) ON CONFLICT (instance_id, jid) DO UPDATE SET updated_at = NOW() RETURNING id`, [instanceId, jid, msg.pushName || jid.split('@')[0], jid.includes('@g.us')] ) const chatId = chatResult.rows[0].id // Get message content const content = msg.message?.conversation || msg.message?.extendedTextMessage?.text || msg.message?.imageMessage?.caption || '' const messageType = this.getMessageType(msg.message) // Insert message await query( `INSERT INTO messages ( instance_id, chat_id, message_id, from_jid, from_me, message_type, content, timestamp, raw_message ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (instance_id, message_id) DO NOTHING`, [ instanceId, chatId, msg.key.id, msg.key.fromMe ? 'me' : jid, msg.key.fromMe || false, messageType, content, new Date(msg.messageTimestamp! * 1000), JSON.stringify(msg) ] ) // Update chat last message if (isNew) { await query( `UPDATE chats SET last_message_at = $1, unread_count = unread_count + 1 WHERE id = $2`, [new Date(msg.messageTimestamp! * 1000), chatId] ) } } catch (error) { console.error('[BaileysManager] Error saving message:', error) } } /** * Get message type from Baileys message object */ private getMessageType(message: any): string { if (!message) return 'unknown' if (message.conversation || message.extendedTextMessage) return 'text' if (message.imageMessage) return 'image' if (message.videoMessage) return 'video' if (message.audioMessage) return 'audio' if (message.documentMessage) return 'document' if (message.stickerMessage) return 'sticker' if (message.contactMessage) return 'contact' if (message.locationMessage) return 'location' return 'unknown' } /** * Map Baileys message status to our status */ private mapMessageStatus(status: number): string { switch (status) { case 0: return 'pending' case 1: return 'sent' case 2: return 'delivered' case 3: return 'read' case 4: return 'read' default: return 'sent' } } } // Export singleton export const baileysManager = BaileysManager.getInstance()