// bot.js
import { Bot, defineBot, isMainModule, slashCommandsPlugin } from '@botmesh/core'
import { BaileysProvider } from '@botmesh/baileys'
// Quién atiende los chats derivados: sus ids de usuario, tal como los muestra /id
const AGENTS = (process.env.AGENTS ?? 'agente').split(',')
// Devuelve el chat al bot si ningún agente escribe en este tiempo
const AGENT_TIMEOUT = '10m'
const createBot = defineBot((provider) => {
// 1. `/bot` sigue funcionando mientras una persona atiende el chat
const bot = new Bot({ provider, lang: 'es', handoff: { allowCommands: ['/bot'] } })
const tickets = new Map() // número de ticket → id del chat
let lastTicket = 0
const timers = new Map() // id del chat → temporizador de inactividad
const isAgent = (ctx) => AGENTS.includes(ctx.sender)
const ticketOf = (chatId) => [...tickets].find(([, id]) => id === chatId)?.[0]
// Reinicia la cuenta atrás que devuelve el chat al bot
const armTimer = (chatId) => {
timers.get(chatId)?.cancel()
timers.set(chatId, bot.delay(AGENT_TIMEOUT, async () => { await bot.resume(chatId, { reason: 'timeout' }) }))
}
bot.plug(slashCommandsPlugin, {
greeting: '👋 Soy el bot de soporte. Pregúntame el "horario" o envía "humano" para hablar con una persona.',
})
bot.when('horario', (ctx) => ctx.reply('🕘 Abrimos de lunes a viernes, de 9:00 a 18:00.'))
// 2. El usuario pide una persona: se deriva el chat
bot.when(['humano', 'agente', 'hablar con una persona'], async (ctx) => {
await ctx.reply('🙋 Te paso con una persona. Envía /bot para volver conmigo.')
await ctx.handoff({ reason: 'user request' })
})
// 3. Avisa a los agentes y arranca la cuenta atrás
bot.on('handoff', async ({ chatId }) => {
const ticket = ++lastTicket
tickets.set(ticket, chatId)
armTimer(chatId)
for (const agent of AGENTS) {
await bot.send(agent, `🔔 El ticket #${ticket} necesita una persona. Responde con /reply ${ticket} <texto> y ciérralo con /done ${ticket}.`)
}
})
// 4. Mientras está derivado el bot calla, pero cada mensaje sigue emitiendo 'message'
bot.on('message', async (ctx) => {
const ticket = ticketOf(ctx.from)
if (!bot.isHandedOff(ctx.from) || ticket === undefined) return
for (const agent of AGENTS) await bot.send(agent, `💬 #${ticket}: ${ctx.body}`)
})
// 5. Comandos de agente (solo desde los ids de los agentes)
bot.command('reply', async (ctx, [ticket, ...words]) => {
if (!isAgent(ctx)) return
const chatId = tickets.get(Number(ticket))
if (!chatId || !bot.isHandedOff(chatId)) return ctx.reply(`El ticket #${ticket} no está abierto.`)
armTimer(chatId)
await bot.send(chatId, `👩💼 ${words.join(' ')}`)
})
bot.command('done', async (ctx, [ticket]) => {
if (!isAgent(ctx)) return
const chatId = tickets.get(Number(ticket))
if (chatId && (await bot.resume(chatId, { reason: 'agent' }))) await ctx.reply(`✅ Ticket #${ticket} cerrado.`)
})
// 6. El usuario llama de vuelta al bot
bot.command('bot', (ctx) => bot.resume(ctx.from, { reason: 'user' }))
// 7. De vuelta al bot, sea cual sea el motivo
bot.on('resume', async ({ chatId, reason }) => {
timers.get(chatId)?.cancel()
timers.delete(chatId)
for (const [ticket, id] of tickets) if (id === chatId) tickets.delete(ticket)
await bot.send(
chatId,
reason === 'timeout'
? '⏰ Ahora mismo no hay nadie libre, así que vuelvo yo. Prueba "humano" más tarde.'
: '🤖 Ya estoy de vuelta. Pregúntame el "horario" o envía "humano".',
)
})
bot.onMessage((ctx) => ctx.reply('Pregúntame el "horario" o envía "humano" para hablar con una persona.'))
return bot
})
export default createBot
// `node bot.js` se conecta a WhatsApp; `npx botmesh dev bot.js` se salta este bloque
if (isMainModule(import.meta)) {
const bot = await createBot(new BaileysProvider({ session: 'bot-soporte' }))
bot.on('ready', () => console.log('✅ Conectado'))
await bot.start()
}