Las piezas
Bot y provider
Bot (de @botmesh/core) es el motor agnóstico de transporte: ejecuta el
dispatcher, los hooks, los flujos, las conversaciones y el scheduler. El
provider es el transporte: @botmesh/baileys (WhatsApp Web),
@botmesh/meta (WhatsApp Cloud API), @botmesh/telegram y los
demás canales entregan al bot los mismos eventos normalizados, así
que el código que registras funciona en cualquiera de ellos. Todas las opciones
de new Bot() están en la referencia de configuración.
when: respuestas simples
'hola' coincide con “Hola” y ” hóla ”, pero no con
“/hola” ni con “hola amigo”. Los triggers admiten otras formas:
bot.command('ayuda', handler) registra un comando /ayuda (el prefijo es
commandPrefix), y bot.onMessage(handler) es el catch-all para lo que nada
más manejó.
flow y conversation: conversaciones de varios pasos
c.state guarda la respuesta y el flujo sigue,
con validación, reintentos, menús y timeouts. Compila al mismo runtime que
bot.conversation(nombre, async (c, ctx) => { ... }), que escribes a mano
cuando necesitas bucles o lógica arbitraria. Ver
Flujos conversacionales.
Hooks: middleware
before corren antes del enrutado y pueden detener o reescribir un
mensaje; los after corren cuando ya se manejó; los onError cuando un
handler lanza un error. Los hooks de 'send' ven cada mensaje saliente. Ver
Hooks.
Plugins: funciones empaquetadas
bot.plug() devuelve una
función unplug() que deshace lo que registró el plugin. Un name de plugin
se puede enchufar una vez por bot, salvo que su spec ponga multiple: true.
Ver Plugins.
El objeto ctx
Todo handler recibe un MessageContext. Lo que te dice:
Lo que hace:
Todos los helpers de envío están en Mensajes y media.
Ids de chat y remitente
ctx.from (chat) y ctx.sender (usuario) son strings opacos que elige el
provider; el core nunca los interpreta. Guárdalos y compáralos tal como
llegan. En WhatsApp (Baileys) son JIDs:
Telegram usa ids numéricos, la Cloud API números de teléfono, etc. El provider
también resuelve los alias: la misma persona llega siempre con el mismo
ctx.sender, aunque WhatsApp alterne entre su número y un LID.
El pipeline de entrada
Todo mensaje entrante pasa por los mismos pasos, en este orden. Los cuatro primeros corren en cuanto el provider emite el evento; el resto corre en una cola FIFO por chat, así que dos mensajes del mismo chat nunca se procesan a la vez. El primer handler que consume el mensaje detiene el enrutado: Antes del paso 1 el bot descarta sus propios mensajes (fromMe) y, con
echoFilter activo, los ecos de sus envíos.
- Blacklist / whitelist: un remitente o chat bloqueado se descarta en
silencio. No se guarda, no corre ningún hook, no se emite
'message'y no se marca como visto, así que una reentrega tras desbloquearlo se procesa. Ver Handoff y listas de acceso. - Dedupe de entrada: un mensaje reenviado (mismo
chatId+ id, por un reintento de webhook o una reconexión) se descarta. Ver Dedupe de entrada. Con el procesamiento durable activo, el evento se escribe después en el journal de entrada, así que tras una caída se reproduce desde el paso 1. - Persistencia: el chat, el contacto y el mensaje se guardan en el store.
- Filtro
groupMode: un mensaje de grupo que el bot debe ignorar se detiene aquí (se guarda igual). - Idioma y después hooks
before('message'): con i18n activo, primero se resuelvectx.lang, así que hooks y handlers ya lo ven. Luego corren los hooks, que pueden hacerabort()del mensaje. Si un hook reescribióctx.body(la transcripción de@botmesh/voice, por ejemplo) y el idioma salió dedetecto del valor por defecto, se resuelve otra vez sobre el texto nuevo. - Puerta de handoff: mientras un chat está
en manos de un humano, se saltan los pasos 7–11 (salvo
handoff.allowCommands). El mensaje se guarda igual, los hooks corren y'message'se emite. - Comandos (
/reset,/help…): siempre ganan, incluso a una conversación activa. Por eso/resetsirve para salir de un flujo atascado. - Conversación activa: si este participante está en un flujo o en una
bot.conversationesperando respuesta, el mensaje va ahí. - Reanudación dormida: un flujo
.persistent()con checkpoint en el store (por ejemplo antes de un reinicio) se reanuda en el paso guardado. - Matchers registrados con
bot.when()ybot.onType(), por especificidad:WELCOME(solo el primer contacto), luego texto exacto,startsWith,endsWith, regex,contains, predicados, tipo único (IMAGE) y triggers multi-tipo (MEDIA). A igual especificidad gana el registrado primero. Ver Precedencia. bot.onMessage(): el catch-all (aiPlugines uno). Corre solo si nada de lo anterior consumió el mensaje.- Final: hooks
after('message')y después el evento'message'(aquí se rearman los timers debot.onIdle).
onError('message') y el error
se emite en bot.on('error', ({ error, ctx }) => ...), haya o no un hook de
error.
Los handlers consumen por defecto
En cuanto corre un handler, el enrutado se detiene ahí. Llama actx.pass()
para entregar el mensaje al siguiente handler que coincida:
El evento 'message' vs onMessage()
bot.on('message', ctx => ...)se dispara con cada mensaje que el dispatcher terminó, incluidos los que consumió un comando, una conversación o unwhen. Úsalo para logs e integraciones de bandeja de entrada.bot.onMessage(ctx => ...)es el handler catch-all: solo corre cuando nada más consumió el mensaje.
Llamadas, bot.trigger() y réplicas
- Las llamadas siguen un camino más corto: las listas de acceso y la
puerta de handoff van primero y bloquean la llamada por completo (sin
hooks
'call', evento ni triggerCALL; cuenta enmetrics().dropped). Una llamada admitida corre los hooksbefore/after('call')y el evento'call', y después solo los triggersCALL: sin hooks de mensaje, comandos ni evento'message'. bot.trigger(nombre, { chatId })inicia un flujo sin mensaje entrante (ver Iniciar un flujo desde código). Comprueba las listas de acceso, espera el turno FIFO del chat, comprueba la puerta de handoff, resuelve el idioma e inicia la conversación. Sin dedupe, persistencia, hooks de mensaje, matchers ni evento'message'.- Con
coordination(varias réplicas) hay un paso más entre el 4 y el 5: el mensaje espera el turno de su chat, así que el FIFO por chat se mantiene entre réplicas. Un mensaje que no consigue su turno se descarta y se reporta en'error'(su id de dedupe se libera, así que una reentrega se procesa).
Las conversaciones de grupo son por participante
En un grupo, dos participantes que hablan con el bot tienen conversaciones independientes por defecto: uno puede estar a mitad de un registro mientras el otro pide el menú. UsaconversationScope: 'chat' para tener una sola
conversación compartida por grupo:
groupMode y el evento 'message' siguen siendo por chat en
ambos casos.
Dedupe de entrada
Los transportes por webhook (Cloud API, Messenger, Instagram, webhook de Telegram) reintentan una entrega cuando no reciben un 2xx a tiempo, y Baileys puede reenviar mensajes recientes tras reconectar. Sin protección, el mismo mensaje arrancaría un flujo dos veces, o se tomaría como respuesta a la siguiente pregunta. El bot descarta las reentregas antes de que las vea ningún handler. Está activado por defecto:- Un mensaje se identifica por
chatId+ref.id(algunos transportes solo garantizan ids únicos por chat). Un toque en un botón o lista se identifica por suinteractionId, así que dos toques en el mismo menú son dos eventos. - Ediciones: el mismo id con un cuerpo distinto (una edición de Telegram) se procesa; un reintento idéntico se descarta. Solo se guarda un hash del cuerpo.
- Los eventos con id vacío nunca se deduplican.
- Un mensaje bloqueado nunca se registra, así que el mismo id reenviado tras desbloquear se procesa.
- Si la persistencia falla antes de los handlers (store caído), o el mensaje
sigue en cola cuando corre
bot.stop(), la clave se libera para que una reentrega se procese. sizelimita un LRU;ttl(una duración) cuenta desde la primera aceptación.- Límite: vive en la memoria del proceso, así que no deduplica entre réplicas ni tras un reinicio. Eso lo cubre el procesamiento durable.
'' cuando el transporte no pueda
garantizarlo (el contrato de ids de entrada de IProvider).
Filtro de ecos
Todo provider marca los mensajes del propio bot confromMe, y el core los
descarta primero. Algunos transportes solo conocen su propio id tras un
handshake, y un buzón o gateway puede devolver un mensaje que el bot acaba de
enviar sin esa marca. echoFilter añade una segunda protección: el bot
recuerda las refs de sus envíos correctos y descarta un mensaje entrante que
traiga una de ellas, antes de las listas de acceso, el dedupe y el journal.
metrics().dropped.echo (Prometheus: botmesh_inbound_dropped_total{reason="echo"}).
La memoria es por proceso, y cada ref recordada se consume con el primer eco
que coincide.
Entradas del paquete
Siguientes pasos
- Referencia de configuración: todas las opciones de
new Bot(), eventos, logs y errores - Mensajes y media: todos los tipos de respuesta
- Flujos conversacionales: conversaciones de varios pasos
- Triggers especiales: WELCOME, tipos de mensaje y CALL
- Hooks: middleware
- Ejemplos: recetas prácticas