All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
El campo quoted debe ir en el tercer parámetro (opciones) de sendMessage, no dentro del objeto content. Esto corrige el envío de respuestas.
734 lines
24 KiB
TypeScript
734 lines
24 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
|
|
shouldSyncHistoryMessage: (msg) => {
|
|
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)
|
|
}
|
|
}
|
|
})
|
|
|
|
// 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 messages
|
|
if (messages?.length) {
|
|
for (const msg of messages) {
|
|
await this.saveMessage(instanceId, msg, false)
|
|
}
|
|
}
|
|
|
|
console.log(`[BaileysManager] History sync processed`)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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): 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
|
|
}
|
|
}
|
|
})
|
|
console.log(`[BaileysManager] Sent reaction ${emoji} to message ${messageId}`)
|
|
}
|
|
|
|
/**
|
|
* 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 content = msg.message?.conversation ||
|
|
msg.message?.extendedTextMessage?.text ||
|
|
msg.message?.imageMessage?.caption ||
|
|
msg.message?.videoMessage?.caption ||
|
|
msg.message?.documentMessage?.caption ||
|
|
''
|
|
|
|
const messageType = this.getMessageType(msg.message)
|
|
|
|
// 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) 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()
|