Saltar al contenido principal

Las piezas

Bot y provider

El 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

Cuando un mensaje coincide con el trigger, se ejecuta el handler. Un texto simple compara por igualdad normalizada: se ignoran mayúsculas, tildes y espacios de más, así que '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

Un flujo es la descripción encadenable de una conversación: el bot pregunta, el usuario responde, 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

Los hooks 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

Un plugin agrupa handlers, hooks y herramientas. 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.
  1. 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.
  2. 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.
  3. Persistencia: el chat, el contacto y el mensaje se guardan en el store.
  4. Filtro groupMode: un mensaje de grupo que el bot debe ignorar se detiene aquí (se guarda igual).
  5. Idioma y después hooks before('message'): con i18n activo, primero se resuelve ctx.lang, así que hooks y handlers ya lo ven. Luego corren los hooks, que pueden hacer abort() del mensaje. Si un hook reescribió ctx.body (la transcripción de @botmesh/voice, por ejemplo) y el idioma salió de detect o del valor por defecto, se resuelve otra vez sobre el texto nuevo.
  6. 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.
  7. Comandos (/reset, /help…): siempre ganan, incluso a una conversación activa. Por eso /reset sirve para salir de un flujo atascado.
  8. Conversación activa: si este participante está en un flujo o en una bot.conversation esperando respuesta, el mensaje va ahí.
  9. Reanudación dormida: un flujo .persistent() con checkpoint en el store (por ejemplo antes de un reinicio) se reanuda en el paso guardado.
  10. Matchers registrados con bot.when() y bot.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.
  11. bot.onMessage(): el catch-all (aiPlugin es uno). Corre solo si nada de lo anterior consumió el mensaje.
  12. Final: hooks after('message') y después el evento 'message' (aquí se rearman los timers de bot.onIdle).
Si un handler lanza un error, corren los hooks 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 a ctx.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 un when. Ú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 trigger CALL; cuenta en metrics().dropped). Una llamada admitida corre los hooks before/after('call') y el evento 'call', y después solo los triggers CALL: 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ú. Usa conversationScope: 'chat' para tener una sola conversación compartida por grupo:
El handoff, 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 su interactionId, 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.
  • size limita 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.
Los providers deben dar a cada evento entrante un id que nombre exactamente un evento, no cambie cuando se reenvía y sea '' 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 con fromMe, 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.
Está desactivado por defecto. Los descartes cuentan en 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