// bot.js
import { Bot, defineBot, isMainModule, slashCommandsPlugin } from '@botmesh/core'
import { BaileysProvider } from '@botmesh/baileys'
// Who answers handed-off chats: their user ids, as /id shows them
const AGENTS = (process.env.AGENTS ?? 'agent').split(',')
// Give the chat back to the bot when no agent writes for this long
const AGENT_TIMEOUT = '10m'
const createBot = defineBot((provider) => {
// 1. `/bot` keeps working while a person has the chat
const bot = new Bot({ provider, handoff: { allowCommands: ['/bot'] } })
const tickets = new Map() // ticket number → chat id
let lastTicket = 0
const timers = new Map() // chat id → inactivity timer
const isAgent = (ctx) => AGENTS.includes(ctx.sender)
const ticketOf = (chatId) => [...tickets].find(([, id]) => id === chatId)?.[0]
// Restart the countdown that returns the chat to the 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: '👋 I am the support bot. Ask me for "hours", or send "human" to talk to a person.',
})
bot.when('hours', (ctx) => ctx.reply('🕘 We are open Monday to Friday, 9:00 to 18:00.'))
// 2. The user asks for a person: hand the chat off
bot.when(['human', 'agent', 'talk to a human'], async (ctx) => {
await ctx.reply('🙋 Connecting you with a person. Send /bot to come back to me.')
await ctx.handoff({ reason: 'user request' })
})
// 3. Tell the agents, and start the countdown
bot.on('handoff', async ({ chatId }) => {
const ticket = ++lastTicket
tickets.set(ticket, chatId)
armTimer(chatId)
for (const agent of AGENTS) {
await bot.send(agent, `🔔 Ticket #${ticket} needs a person. Answer with /reply ${ticket} <text>, close it with /done ${ticket}.`)
}
})
// 4. While handed off the bot stays quiet, but every message still fires '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. Agent commands (only from the agents' ids)
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(`Ticket #${ticket} is not open.`)
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} closed.`)
})
// 6. The user calls the bot back
bot.command('bot', (ctx) => bot.resume(ctx.from, { reason: 'user' }))
// 7. Back to the bot, however it happened
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'
? '⏰ No one is free right now, so I am back. Try "human" again later.'
: '🤖 I am back. Ask me for "hours" or send "human".',
)
})
bot.onMessage((ctx) => ctx.reply('Ask me for "hours", or send "human" to talk to a person.'))
return bot
})
export default createBot
// `node bot.js` connects to WhatsApp; `npx botmesh dev bot.js` skips this block
if (isMainModule(import.meta)) {
const bot = await createBot(new BaileysProvider({ session: 'support-bot' }))
bot.on('ready', () => console.log('✅ Connected'))
await bot.start()
}