Files
whatsappNucleo/server/services/baileys/manager.ts
josedario87 67b54d4ad9
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
Feat: Sincronizar contactos y grupos, mejorar nombres en MCP
- Agregar listeners contacts.upsert y contacts.update
- Agregar listeners groups.upsert y groups.update
- Guardar contactos del historial en messaging-history.set
- Modificar MCP para hacer JOIN con contacts y group_metadata
- Ahora los chats muestran nombres reales en lugar de números/IDs
2025-12-04 14:51:09 -06:00

915 lines
31 KiB
TypeScript

/**
* 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 }
'message.reaction': { instanceId: string; reaction: any }
'presence.update': { instanceId: string; jid: string; presences: Record<string, { lastKnownPresence: string; lastSeen?: number }> }
}
const logger = pino({ level: 'warn' })
class BaileysManager extends EventEmitter {
private instances: Map<string, ManagedInstance> = 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<void> {
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<void> {
// 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: true,
markOnlineOnConnect: false,
// Sync history messages from the last 30 days
// IMPORTANTE: Si no hay messageTimestamp, retornar true para permitir el check inicial de Baileys
shouldSyncHistoryMessage: (msg) => {
if (!msg.messageTimestamp) return true
const thirtyDaysAgo = Date.now() / 1000 - (30 * 24 * 60 * 60)
return msg.messageTimestamp >= thirtyDaysAgo
},
// getMessage for resending/decrypting messages
getMessage: async (key) => {
try {
const result = await query<{ raw_message: any }>(
`SELECT raw_message FROM messages WHERE instance_id = $1 AND message_id = $2`,
[instanceId, key.id]
)
if (result.rows.length > 0) {
return result.rows[0].raw_message?.message
}
} catch (err) {
console.error('[BaileysManager] getMessage error:', err)
}
return undefined
}
})
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<void>,
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)
})
}
}
})
// Chat upsert - new chats
socket.ev.on('chats.upsert', async (chats) => {
console.log(`[BaileysManager] chats.upsert: ${chats.length} chats`)
for (const chat of chats) {
try {
await query(
`INSERT INTO chats (instance_id, jid, name, is_group, unread_count)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (instance_id, jid) DO UPDATE SET
name = COALESCE(EXCLUDED.name, chats.name),
updated_at = NOW()`,
[instanceId, chat.id, chat.name || chat.id?.split('@')[0], chat.id?.includes('@g.us'), chat.unreadCount || 0]
)
} catch (err) {
console.error(`[BaileysManager] Error upserting chat:`, err)
}
}
})
// Chat update - changes to existing chats
socket.ev.on('chats.update', async (updates) => {
for (const update of updates) {
try {
const fields: string[] = []
const values: any[] = [instanceId, update.id]
let paramIdx = 3
if (update.unreadCount !== undefined) {
fields.push(`unread_count = $${paramIdx++}`)
values.push(update.unreadCount)
}
if (update.name) {
fields.push(`name = $${paramIdx++}`)
values.push(update.name)
}
if (fields.length > 0) {
await query(
`UPDATE chats SET ${fields.join(', ')}, updated_at = NOW() WHERE instance_id = $1 AND jid = $2`,
values
)
}
} catch (err) {
console.error(`[BaileysManager] Error updating chat:`, err)
}
}
})
// Presence update
socket.ev.on('presence.update', async (update) => {
console.log(`[BaileysManager] presence.update:`, JSON.stringify(update))
this.emit('presence.update', {
instanceId,
jid: update.id,
presences: update.presences
})
// Cache presence in database
for (const [participantJid, presence] of Object.entries(update.presences)) {
try {
await query(
`INSERT INTO presence_cache (instance_id, jid, presence, last_seen)
VALUES ($1, $2, $3, $4)
ON CONFLICT (instance_id, jid) DO UPDATE SET
presence = EXCLUDED.presence,
last_seen = COALESCE(EXCLUDED.last_seen, presence_cache.last_seen)`,
[
instanceId,
participantJid,
presence.lastKnownPresence,
presence.lastSeen ? new Date(presence.lastSeen * 1000) : null
]
)
} catch (err) {
console.error(`[BaileysManager] Error caching presence:`, err)
}
}
})
// Contacts upsert - save/update contacts
socket.ev.on('contacts.upsert', async (contacts) => {
console.log(`[BaileysManager] contacts.upsert: ${contacts.length} contacts`)
for (const contact of contacts) {
try {
await query(
`INSERT INTO contacts (instance_id, jid, name, push_name)
VALUES ($1, $2, $3, $4)
ON CONFLICT (instance_id, jid) DO UPDATE SET
name = COALESCE(NULLIF(EXCLUDED.name, ''), contacts.name),
push_name = COALESCE(NULLIF(EXCLUDED.push_name, ''), contacts.push_name),
updated_at = NOW()`,
[instanceId, contact.id, contact.name || null, contact.notify || null]
)
} catch (err) {
console.error(`[BaileysManager] Error saving contact:`, err)
}
}
})
// Contacts update - partial updates
socket.ev.on('contacts.update', async (updates) => {
console.log(`[BaileysManager] contacts.update: ${updates.length} updates`)
for (const update of updates) {
try {
const fields: string[] = []
const values: any[] = [instanceId, update.id]
let paramIndex = 3
if (update.name !== undefined) {
fields.push(`name = $${paramIndex}`)
values.push(update.name)
paramIndex++
}
if (update.notify !== undefined) {
fields.push(`push_name = $${paramIndex}`)
values.push(update.notify)
paramIndex++
}
if (fields.length > 0) {
fields.push('updated_at = NOW()')
await query(
`UPDATE contacts SET ${fields.join(', ')} WHERE instance_id = $1 AND jid = $2`,
values
)
}
} catch (err) {
console.error(`[BaileysManager] Error updating contact:`, err)
}
}
})
// Groups upsert - save/update group metadata
socket.ev.on('groups.upsert', async (groups) => {
console.log(`[BaileysManager] groups.upsert: ${groups.length} groups`)
for (const group of groups) {
try {
await query(
`INSERT INTO group_metadata (instance_id, jid, subject, description, owner_jid, participants)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (instance_id, jid) DO UPDATE SET
subject = COALESCE(NULLIF(EXCLUDED.subject, ''), group_metadata.subject),
description = COALESCE(EXCLUDED.description, group_metadata.description),
owner_jid = COALESCE(EXCLUDED.owner_jid, group_metadata.owner_jid),
participants = COALESCE(EXCLUDED.participants, group_metadata.participants),
updated_at = NOW()`,
[
instanceId,
group.id,
group.subject || null,
group.desc || null,
group.owner || null,
JSON.stringify(group.participants || [])
]
)
} catch (err) {
console.error(`[BaileysManager] Error saving group:`, err)
}
}
})
// Groups update - partial updates
socket.ev.on('groups.update', async (updates) => {
console.log(`[BaileysManager] groups.update: ${updates.length} updates`)
for (const update of updates) {
try {
const fields: string[] = []
const values: any[] = [instanceId, update.id]
let paramIndex = 3
if (update.subject !== undefined) {
fields.push(`subject = $${paramIndex}`)
values.push(update.subject)
paramIndex++
}
if (update.desc !== undefined) {
fields.push(`description = $${paramIndex}`)
values.push(update.desc)
paramIndex++
}
if (fields.length > 0) {
fields.push('updated_at = NOW()')
await query(
`UPDATE group_metadata SET ${fields.join(', ')} WHERE instance_id = $1 AND jid = $2`,
values
)
}
} catch (err) {
console.error(`[BaileysManager] Error updating group:`, err)
}
}
})
// Message reactions
socket.ev.on('messages.reaction', async (reactions) => {
for (const reaction of reactions) {
console.log(`[BaileysManager] message.reaction:`, JSON.stringify(reaction))
this.emit('message.reaction', { instanceId, reaction })
// Save reaction to database
try {
const { key, reaction: reactionData } = reaction
if (reactionData.text) {
// Add reaction
await query(
`INSERT INTO message_reactions (message_id, reactor_jid, emoji)
SELECT m.id, $2, $3
FROM messages m
WHERE m.instance_id = $1 AND m.message_id = $4
ON CONFLICT (message_id, reactor_jid) DO UPDATE SET
emoji = EXCLUDED.emoji,
timestamp = NOW()`,
[instanceId, key.participant || key.fromMe ? 'me' : key.remoteJid, reactionData.text, reactionData.key.id]
)
} else {
// Remove reaction (empty text)
await query(
`DELETE FROM message_reactions
WHERE message_id IN (
SELECT id FROM messages WHERE instance_id = $1 AND message_id = $2
) AND reactor_jid = $3`,
[instanceId, reactionData.key.id, key.participant || key.fromMe ? 'me' : key.remoteJid]
)
}
} catch (err) {
console.error(`[BaileysManager] Error saving reaction:`, err)
}
}
})
// History sync - save chats and messages from history
socket.ev.on('messaging-history.set', async ({ chats, contacts, messages, syncType }) => {
console.log(`[BaileysManager] History sync received: ${chats?.length || 0} chats, ${contacts?.length || 0} contacts, ${messages?.length || 0} messages, type: ${syncType}`)
// Save chats
if (chats?.length) {
for (const chat of chats) {
try {
await query(
`INSERT INTO chats (instance_id, jid, name, is_group, unread_count)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (instance_id, jid) DO UPDATE SET
name = COALESCE(EXCLUDED.name, chats.name),
unread_count = EXCLUDED.unread_count,
updated_at = NOW()`,
[instanceId, chat.id, chat.name || chat.id?.split('@')[0], chat.id?.includes('@g.us'), chat.unreadCount || 0]
)
} catch (err) {
console.error(`[BaileysManager] Error saving chat:`, err)
}
}
}
// Save contacts from history
if (contacts?.length) {
console.log(`[BaileysManager] Saving ${contacts.length} contacts from history`)
for (const contact of contacts) {
try {
await query(
`INSERT INTO contacts (instance_id, jid, name, push_name)
VALUES ($1, $2, $3, $4)
ON CONFLICT (instance_id, jid) DO UPDATE SET
name = COALESCE(NULLIF(EXCLUDED.name, ''), contacts.name),
push_name = COALESCE(NULLIF(EXCLUDED.push_name, ''), contacts.push_name),
updated_at = NOW()`,
[instanceId, contact.id, contact.name || null, contact.notify || null]
)
} catch (err) {
console.error(`[BaileysManager] Error saving contact from history:`, err)
}
}
}
// Save messages
if (messages?.length) {
for (const msg of messages) {
await this.saveMessage(instanceId, msg, false)
}
}
console.log(`[BaileysManager] History sync processed`)
// Emitir evento SSE al frontend para notificar que llegó el historial
this.emit('history.synced', {
instanceId,
chatsCount: chats?.length || 0,
contactsCount: contacts?.length || 0,
messagesCount: messages?.length || 0,
syncType
})
})
}
/**
* Disconnect an instance
*/
async disconnect(instanceId: string): Promise<void> {
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, options?: any): Promise<any> {
const managed = this.instances.get(instanceId)
if (!managed?.socket) {
throw new Error('Instance not connected')
}
const result = await managed.socket.sendMessage(jid, content, options)
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())
}
/**
* Get the raw socket for an instance (for debug purposes)
* Returns null if instance is not connected
*/
getSocket(instanceId: string): WASocket | null {
const managed = this.instances.get(instanceId)
if (!managed || managed.status !== 'connected') {
return null
}
return managed.socket
}
/**
* Subscribe to presence updates for a JID
*/
async subscribeToPresence(instanceId: string, jid: string): Promise<void> {
const managed = this.instances.get(instanceId)
if (!managed?.socket) {
throw new Error('Instance not connected')
}
await managed.socket.presenceSubscribe(jid)
console.log(`[BaileysManager] Subscribed to presence: ${jid}`)
}
/**
* Send presence update (composing, recording, available, unavailable, paused)
*/
async sendPresence(instanceId: string, jid: string, presence: 'composing' | 'recording' | 'available' | 'unavailable' | 'paused'): Promise<void> {
const managed = this.instances.get(instanceId)
if (!managed?.socket) {
throw new Error('Instance not connected')
}
await managed.socket.sendPresenceUpdate(presence, jid)
console.log(`[BaileysManager] Sent presence ${presence} to ${jid}`)
}
/**
* Send a reaction to a message
*/
async sendReaction(instanceId: string, jid: string, messageId: string, emoji: string, fromMe: boolean = false): Promise<void> {
const managed = this.instances.get(instanceId)
if (!managed?.socket) {
throw new Error('Instance not connected')
}
await managed.socket.sendMessage(jid, {
react: {
text: emoji,
key: {
remoteJid: jid,
id: messageId,
fromMe: fromMe
}
}
})
console.log(`[BaileysManager] Sent reaction ${emoji} to message ${messageId} (fromMe: ${fromMe})`)
}
/**
* Update instance status in database
*/
private async updateInstanceStatus(
instanceId: string,
status: string,
extra: Record<string, any> = {}
): Promise<void> {
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<void> {
try {
const jid = msg.key.remoteJid
if (!jid) return
// Skip protocol messages (internal WhatsApp messages)
if (msg.message?.protocolMessage) {
console.log(`[BaileysManager] Skipping protocol message: ${msg.message.protocolMessage.type}`)
return
}
// Skip if no actual message content
if (!msg.message) {
console.log(`[BaileysManager] Skipping message with no content`)
return
}
// Get message content
const messageType = this.getMessageType(msg.message)
// Extract content based on message type
let content = ''
if (messageType === 'poll') {
// For polls, store the poll data as JSON
const pollMsg = msg.message?.pollCreationMessage || msg.message?.pollCreationMessageV3
if (pollMsg) {
content = JSON.stringify({
name: pollMsg.name,
options: pollMsg.options?.map((o: any) => o.optionName) || [],
selectableCount: pollMsg.selectableOptionsCount || 1
})
}
} else if (messageType === 'event') {
// For events, store the event data as JSON
const eventMsg = msg.message?.eventMessage
if (eventMsg) {
content = JSON.stringify({
name: eventMsg.name,
startDate: eventMsg.startTime ? new Date(eventMsg.startTime * 1000).toISOString() : null,
endDate: eventMsg.endTime ? new Date(eventMsg.endTime * 1000).toISOString() : null,
description: eventMsg.description,
location: eventMsg.location ? {
name: eventMsg.location.name,
address: eventMsg.location.address,
latitude: eventMsg.location.degreesLatitude,
longitude: eventMsg.location.degreesLongitude
} : null
})
}
} else {
// Default content extraction
content = msg.message?.conversation ||
msg.message?.extendedTextMessage?.text ||
msg.message?.imageMessage?.caption ||
msg.message?.videoMessage?.caption ||
msg.message?.documentMessage?.caption ||
''
}
// Skip unknown message types with no content (likely internal messages)
if (messageType === 'unknown' && !content) {
console.log(`[BaileysManager] Skipping unknown message type with no content`)
return
}
// Ensure chat exists and update name if pushName is available
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
name = COALESCE(NULLIF(EXCLUDED.name, chats.jid), chats.name),
updated_at = NOW()
RETURNING id`,
[instanceId, jid, msg.pushName || jid.split('@')[0], jid.includes('@g.us')]
)
const chatId = chatResult.rows[0].id
// 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 || message.contactsArrayMessage) return 'contact'
if (message.locationMessage) return 'location'
if (message.pollCreationMessage || message.pollCreationMessageV3) return 'poll'
if (message.eventMessage) return 'event'
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()