Canales (providers)
El motor del bot (@botmesh/core) no conoce ningún transporte. Un provider lo conecta a un canal: recibe los eventos del canal, los convierte en InboundEvent neutros y envía OutboundContent de vuelta. Los mismos flujos, triggers y plugins funcionan en todos los canales; solo cambia el constructor.
import { Bot } from '@botmesh/core'
const bot = new Bot({ provider }) // cualquiera de los providers de abajoEsta página recoge todos los providers del repositorio, cómo recibe eventos cada uno y qué puede y qué no puede hacer.
Estado: públicos y experimentales
| Paquete | Directorio | Estado | Transporte |
|---|---|---|---|
@botmesh/baileys | baileys-provider | Público | WhatsApp Web (socket de Baileys) |
@botmesh/meta | meta-provider | Público | WhatsApp Cloud API (oficial) |
@botmesh/telegram | telegram-provider | Público | Telegram Bot API |
@botmesh/messenger | messenger-provider | Experimental | Facebook Messenger (Send API) |
@botmesh/instagram | instagram-provider | Experimental | DMs de Instagram (Messenger API for Instagram) |
@botmesh/discord | discord-provider | Experimental | Discord (gateway + REST) |
@botmesh/evolution | evolution-provider | Experimental | Evolution API v2 (gateway de WhatsApp self-hosted) |
@botmesh/wppconnect | wppconnect-provider | Experimental | WPPConnect Server (gateway de WhatsApp self-hosted) |
@botmesh/twilio | twilio-provider | Experimental | Twilio Programmable Messaging (WhatsApp + SMS/MMS) |
@botmesh/gohighlevel | gohighlevel-provider | Experimental | HighLevel (LeadConnector) Conversations API |
@botmesh/gupshup | gupshup-provider | Experimental | Gupshup WhatsApp API |
@botmesh/email | email-provider | Experimental | Email (IMAP IDLE + SMTP) |
@botmesh/tiktok | tiktok-provider | Experimental | TikTok API for Business (comentarios + Business Messaging) |
Los paquetes públicos se instalan con pnpm add. Los experimentales llevan "private": true en su package.json: están implementados y con tests, pero no se publican en npm y su API puede cambiar. Úsalos desde este monorepo ("workspace:*").
Elegir un canal
WhatsApp tiene varias opciones:
@botmesh/baileys: vincula el bot como un dispositivo de WhatsApp Web (QR o código de emparejamiento) y abre su propio socket. Sin servidor público ni webhook, con grupos y encuestas, sin coste por mensaje. Es un cliente no oficial: automatizar una cuenta puede infringir los términos de WhatsApp y acabar en el bloqueo del número. Deja activado el rate limiter.@botmesh/meta: la WhatsApp Cloud API oficial. Botones, listas, plantillas y formularios con WhatsApp Flows nativos, sin riesgo de baneo. Necesita un webhook HTTPS público, no tiene grupos y los mensajes libres solo funcionan dentro de la ventana de atención de 24 horas.@botmesh/evolution/@botmesh/wppconnect: ya tienes desplegado un Evolution API o un WPPConnect Server. El bot habla con ese gateway por REST y recibe sus webhooks. Ambos gateways manejan WhatsApp Web, así que valen las mismas advertencias que con Baileys (no oficial, sin botones nativos fiables). Lo común (cliente REST, media, reconciliación LID/PN) vive en el paquete privado@botmesh/wa-gateway-kit; se instala el canal, nunca el kit.@botmesh/twilio/@botmesh/gupshup: envías WhatsApp a través de la cuenta de ese proveedor. Admiten plantillas; Twilio cubre además SMS/MMS con la misma clase.@botmesh/gohighlevel: el bot responde a los contactos del CRM HighLevel por el canal que usaron (SMS, WhatsApp, Email, Facebook, Instagram, Live chat...).
Otros canales: @botmesh/telegram (long polling por defecto, webhook opcional), @botmesh/discord (socket del gateway), @botmesh/messenger e @botmesh/instagram (webhooks de Meta), @botmesh/email (cada hilo de correo es un chat) y @botmesh/tiktok (comentarios en tus propios videos y DMs).
Socket, polling o webhook
- Socket / polling (el provider abre la conexión): Baileys, Discord, Telegram en modo polling, Email (IMAP IDLE). No hay nada que exponer a internet.
- Webhook (
capabilities.pushIngress: true): Cloud API, Messenger, Instagram, Telegram en modo webhook, Evolution, WPPConnect, Twilio, HighLevel, Gupshup, TikTok. Tu servidor HTTP recibe el POST y le pasa el body crudo y las cabeceras aprovider.ingest(), que verifica la firma o el token y devuelve el status HTTP con el que responder.@botmesh/serverresuelve ese cableado (body crudo, límites de tamaño, timeouts, endpoints de salud) paranode:http, Express, Fastify y Hono.
El constructor de todo provider por webhook (Telegram, solo en modo webhook) lanza un error si falta su credencial de webhook (app secret, auth token o token compartido), salvo que pases allowUnsignedWebhooks: true, pensado solo para desarrollo local aislado. HighLevel es la excepción: su clave pública de firma viene incluida.
Matriz de capacidades
Los valores salen de la declaración capabilities de cada provider. Una función marcada "no" se degrada, no se rompe: los botones y las listas pasan a un menú de texto numerado, ctx.edit envía un mensaje nuevo, ctx.react resuelve { skipped: true } y ctx.typing no hace nada (ver Mensajes y media).
| Provider | Entrada | Botones | Listas | Encuestas | Enviar reacciones | Editar | Escribiendo | Confirmación de lectura | Grupos | Plantillas | ctx.stream | Formularios |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Baileys | socket | no | no | sí | sí | no | sí | sí | sí | no | fragmentos | campo a campo |
Cloud API (meta) | webhook | sí | sí | no | sí | no | no | sí | no | sí | fragmentos | nativo (WhatsApp Flows) |
| Telegram | polling o webhook | sí | no (teclado inline) | sí | sí | sí | sí | no | sí | no | borrador | campo a campo |
| Messenger | webhook | sí | sí | no | no | no | sí | sí | no | no | fragmentos | campo a campo |
| webhook | sí (quick replies) | no | no | no | no | sí | sí | no | no | fragmentos | campo a campo | |
| Discord | socket | sí | sí | sí | sí | sí | sí | no | sí | no | edición | campo a campo |
| Evolution | webhook | no | no | sí | sí | sí | sí | sí | sí | no | fragmentos | campo a campo |
| WPPConnect | webhook | no | no | sí | sí | sí | sí | sí | sí | no | fragmentos | campo a campo |
| Twilio | webhook | no | no | no | no | no | sí (solo WhatsApp) | no | no | sí | fragmentos | campo a campo |
| HighLevel | webhook | no | no | no | no | no | no | sí | no | sí | fragmentos | campo a campo |
| Gupshup | webhook | sí | sí | no | no | no | no | no | no | sí | fragmentos | campo a campo |
IMAP IDLE (o ingest()) | no | no | no | no | no | no | no | no | no | fragmentos | campo a campo | |
| TikTok | webhook | sí (de 1 a 3, DMs; más → texto numerado) | sí (hasta 3 filas, DMs; más → texto numerado) | no | no | no | sí (DMs) | sí (DMs) | no | no | fragmentos | campo a campo |
- Enviar reacciones solo describe el envío. Varios providers reciben igualmente reacciones como eventos
reaction(Messenger, Instagram, Gupshup, TikTok...). ctx.stream:borrador= vistas previas nativas en borrador y un mensaje final (chats privados de Telegram; ediciones en el resto),edición= un mensaje que se edita a medida que crece el texto,fragmentos= sin vista previa en vivo, mensajes del tamaño de una frase con indicador de escritura (streaming: 'none'). Ver Respuestas en streaming.- Formularios:
nativoenvíaflow().form()como un WhatsApp Flow;campo a campopregunta cada campo con mensajes de texto. textFormat(no es columna): el formato de texto que muestra el canal, para que quien genere Markdown, como@botmesh/ai, lo convierta.'whatsapp'(*negrita*,_cursiva_,~tachado~): Baileys, Cloud API, Evolution, WPPConnect, Gupshup.'plain'(sin formato): Telegram (se envía sinparse_mode), Messenger, Instagram, TikTok.'markdown': Discord. Sin declarar: Email. Los providers que sirven varios canales deciden por chat con el método opcionaltextFormatFor(chatId), que gana acapabilities.textFormaty llega a los handlers comoctx.textFormat: Twilio devuelve'whatsapp'en los chatswhatsapp:y'plain'en SMS; HighLevel sigue el canal de respuesta del contacto ('whatsapp'en WhatsApp,'plain'en SMS, Email, FB, IG, Live chat, GMB y custom).- Plantillas es el tipo de contenido
template: plantillas de la Cloud API, Content Templates de Twilio (ContentSid),templateIdde HighLevel, plantillas de Gupshup. El resto de providers lo rechaza conUnsupportedContentError. - Todos los providers salvo Baileys aceptan el tipo de contenido
native(ctx.sendNative('<id del provider>', payload)) como vía de escape a la API propia del canal, con un payload específico de cada provider. Baileys lo rechaza conUnsupportedContentError. Discord, Messenger, Instagram, Evolution, WPPConnect, Twilio, Gupshup y HighLevel también rechazan un payload dirigido a otro id de provider en lugar de enviarlo.
Contrato común de los canales
Las mismas reglas valen en todos los canales, así un bot se comporta igual donde corra. Quien escribe un provider las comprueba con los helpers de @botmesh/core/testing (ver Testing).
- Los timestamps son milisegundos epoch.
ctx.timestamp,InboundMessage.timestamp,InboundInteraction.timestampyDeliveryStatus.timestampson ms epoch en todos los canales. Los transportes que informan segundos (WhatsApp, Telegram, la Cloud API) se multiplican por 1000; uno que no informa hora (Twilio) usa la hora de recepción, nunca0. Telegram no envía la hora de pulsación de un botón, así que un callback query lleva la hora de recepción. - El texto largo se divide. Cada canal declara su límite de texto y el core divide un envío
textmás largo en varios mensajes (ver Mensajes;splitLongText: falselo desactiva). - Las operaciones no soportadas no existen. Una operación que el transporte no puede hacer no se implementa: falta el método opcional (
typing,markRead,editMessage,deleteMessage) y su flag de capacidad esfalse, así quectx.typing(),ctx.markRead()yctx.delete()no hacen nada. Un método que existe hace la operación o rechaza; nunca es un sustituto silencioso. - La presencia es de mejor esfuerzo sin conexión.
typing()ymarkRead()resuelven sin tocar el transporte mientras el último estado de conexión del provider no es'ready'(antes de questart()conecte, durante una reconexión, después destop()): sin error, sin cola y sin reintento posterior. El bot aplica la misma regla antes de llamarlos, registra la señal omitida en debug y la cuenta enmetrics().presenceSkipped(botmesh_presence_skipped_totalentoPrometheus()). Ya conectado, un fallo del transporte sigue rechazando.assertPresenceWhileDisconnected()lo comprueba en un provider. - Demasiadas opciones rechazan. Un transporte que muestra menos botones, filas o respuestas de encuesta de las que enviaste rechaza con
UnsupportedContentError(Cloud API, Messenger, Instagram, Discord), salvo que tenga una degradación documentada (texto numerado en Baileys, Evolution, WPPConnect, Gupshup, TikTok). - Una ruta local de media que no se puede leer rechaza con
UnsupportedContentError(el error de fs comocause), nunca con unENOENTcrudo. - Las esperas tienen tope. Cada llamada REST tiene timeout (
requestTimeoutMsen la Cloud API, Telegram, Discord, Messenger e Instagram) y unRateLimitedErrornunca pide más de 24 h. - Una edición conserva el id original. Donde el transporte informa de ediciones de texto (Baileys,
edited_messagede Telegram,MESSAGE_UPDATEde Discord), la edición llega como mensaje con el texto nuevo y elref.idoriginal; cada uno de esos paquetes exportaisEditedMessage(ctx)y una opcióneditedMessages: 'ignore'. El dedupe del core deja pasar la edición porque su contenido cambió y descarta una re-entrega de la misma edición. - Los votos de encuesta tienen una sola forma. Baileys y Telegram emiten cada voto como evento
'poll'(bot.on('poll', (vote) => …)):vote.refes el mensaje de la encuesta,vote.senderIdquien votó yvote.optionsel recuento actual de cada opción con susvoters. - Un solo esquema de nombres para los helpers de bajo nivel. El mapeador de errores de cada provider es
to<Canal>Error(toTelegramError,toDiscordError,toTwilioError…; los canales de Meta compartentoGraphErrorde@botmesh/graph-core); los nombres antiguostoProviderErrorquedan como alias deprecados.ingest()devuelve el estado HTTP con el que responder (unnumber); WPPConnect lo devuelve comoPromise<number>(comprueba el token de forma asíncrona) y elingest()de email recibe un mensaje crudo, no un webhook, y devuelve unEmailIngestResult.
| Provider | maxTextLength | maxTextBytes |
|---|---|---|
| Baileys, Evolution, WPPConnect | 65 536 | |
Cloud API (meta), Gupshup | 4096 | |
| Telegram | 4096 | |
| Discord, Messenger | 2000 | |
| 1000 | 1000 | |
| Twilio | 1600 | |
| TikTok | 6000 (DMs; las respuestas a comentarios admiten 150) | |
| HighLevel, Email | sin declarar (no se divide) |
Providers
@botmesh/baileys
WhatsApp Web a través de Baileys. Se vincula con un código QR (o un código de emparejamiento, pairingPhoneNumber) y guarda la sesión en sessionsDir.
import { BaileysProvider } from '@botmesh/baileys'
const provider = new BaileysProvider({ session: 'my-bot' })- Socket propio del provider, con reconexión y backoff (
reconnect: { maxAttempts, baseMs, capMs }lo ajusta,falselo desactiva). - Las formas LID y número de teléfono de un usuario se unen en un único
senderIdestable. - Sin botones ni listas nativos (WhatsApp Web los retiró): usa menús
choose()octx.poll(). - El rate limiting anti-baneo de envíos está activado por defecto (
rateLimit). - Las reacciones y los borrados llegan como eventos, nunca como mensajes, así que un handler comodín o un
askabierto no los toman como texto. Una edición de texto llega como mensaje con el texto nuevo y elref.idoriginal, como en Telegram:isEditedMessage(ctx)(de@botmesh/baileys) la distingue yeditedMessages: 'ignore'descarta las ediciones. Solo el autor de un mensaje que el proceso ha visto puede editarlo: la edición de un mensaje ajeno, de uno del bot o de un original que ya no está en caché se descarta. ctx.voice()yctx.sticker()aceptan URLs y streams (descargados con los mismos controles anti-SSRF que el resto de la media);ctx.poll()necesita de 2 a 12 opciones distintas.
@botmesh/meta (WhatsApp Cloud API)
La API oficial. Funciona por webhook: handshake GET con verifyChallenge() y eventos POST con ingest().
import { CloudAPIProvider } from '@botmesh/meta'
const provider = new CloudAPIProvider({
accessToken: process.env.META_TOKEN!, // system-user token
phoneNumberId: process.env.META_PHONE_ID!,
verifyToken: process.env.META_VERIFY_TOKEN!, // the one you set in Meta
appSecret: process.env.META_APP_SECRET!, // verifies X-Hub-Signature-256
})- Botones de respuesta nativos (máximo 3: con más rechaza con
UnsupportedContentError, envía una lista), listas, plantillas y formularios con WhatsApp Flows. Un botón conurlsale como botón de enlacecta_url(uno por mensaje: varios enlaces, o un enlace mezclado con botones de respuesta, rechazan). Unacardconserva su imagen (como media o como cabecera del mensaje interactivo) y sus botones. Los límites de Graph se comprueban antes de enviar: sin botones, ids o títulos de botón repetidos (comparados tras el corte a 20 caracteres), una lista sin filas, con más de 10 filas en total o con ids de fila repetidos rechazan conUnsupportedContentError. Las etiquetas se cortan por grafemas, así que un emoji se conserva entero o se quita, nunca se parte. - Si falta
accessToken,phoneNumberIdoverifyToken, el constructor falla con un error que nombra el campo (phoneNumberIdtambién puede ser un entero seguro). Cada petición a Graph tiene un límite de tiempo (requestTimeoutMs, 60 s por defecto); las subidas de media tienen el suyo (uploadTimeoutMs, 5 min por defecto). - Una respuesta interactiva que no es la pulsación de un botón o una lista (por ejemplo
call_permission_reply) llega como mensajeunknown, con el payload crudo enctx.ref.native. - Versión de la Graph API:
apiVersiontoma por defectoDEFAULT_GRAPH_API_VERSIONde@botmesh/graph-core(v26.0, publicada el 29-07-2026; Messenger e Instagram usan el mismo valor). Meta retira cada versión unos dos años después de publicarla, así que el valor por defecto sigue a la actual y cambia en una versión menor, tras revisar los payloads contra el changelog de Meta. FijaapiVersion(por ejemplo'v24.0') para quedarte en una versión hasta migrar. Un estadoplayed(se escuchó una nota de voz) llega comoreadconplatformData.played: true. - Los usuarios con username de WhatsApp pueden llegar con un BSUID en lugar del teléfono; el provider mantiene un id estable por persona (
identityMap). - Fuera de la ventana de 24 h los envíos libres rechazan con
OutsideSessionWindowError. El gestor opcionalsessionWindowreabre el chat con una plantilla y reintenta: ver Ventana de 24 h y costos. - La API no tiene grupos, indicador de escritura ni encuestas, ni borrado para todos: el provider no tiene
typing()nideleteMessage(), así quectx.typing()yctx.delete()no hacen nada (ver el contrato común de los canales). - El core divide en varios mensajes el texto de más de 4096 caracteres.
publishForm()fija en el Flow la etiqueta de envío por defecto de un formulario en ellangdel provider (por defecto'en'; ponle elBotConfig.langde tu bot), o enpublishForm(def, { lang })para un Flow. ElsubmitLabeldel propio formulario siempre gana.
@botmesh/telegram
Telegram Bot API. Long polling por defecto; polling: false cambia a modo webhook, donde webhookSecret es obligatorio.
import { TelegramProvider } from '@botmesh/telegram'
const provider = new TelegramProvider({ token: process.env.TELEGRAM_TOKEN! })
// Webhook mode: the same secret goes to setWebhook (secret_token)
const hooked = new TelegramProvider({
token: process.env.TELEGRAM_TOKEN!,
polling: false,
webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET!,
})- Teclados inline, encuestas nativas, reacciones y edición de mensajes.
ctx.stream()usa borradores nativos en chats privados y ediciones en el resto.- En modo webhook
ingest(body, headers)acepta los bytes crudos, el string JSON o el objeto ya parseado, y comprueba la cabeceraX-Telegram-Bot-Api-Secret-Token(en cualquier capitalización, objeto plano oHeadersde Fetch). Con@botmesh/serverno hace falta ninguna opción. Trasstop()responde503y no emite nada, así que Telegram reenvía esos Updates cuando el bot vuelve. - Los comandos de grupo dirigidos al bot (
/menu@MiBot) llegan abot.command('menu'), también en el pie de una foto o un archivo: el provider quita el sufijo cuando nombra a este bot (usuario degetMe, o la opciónusernamesi pasasselfId). - Cada petición a la Bot API tiene timeout de cliente (
requestTimeoutMs, 120 s por defecto; el long poll usapollTimeoutMs(30 s por defecto; el deprecadopollTimeoutva en segundos) + 15 s; las subidas de archivos,uploadTimeoutMs, 5 min por defecto), los errores nunca llevan el token del bot, unretry_afterde 429 se respeta hasta 24 h ybot.stop()corta al momento las esperas de reintento del bucle de polling. - Mensajes editados: llegan al bot como mensajes con el texto nuevo y el
ref.idoriginal (respuestas, reacciones y ediciones apuntan al mensaje editado).isEditedMessage(ctx)los distingue; el dedupe del core deja pasar la edición porque su contenido cambió.editedMessages: 'ignore'los desactiva. - Votos de encuesta: las encuestas se envían no anónimas (
is_anonymous: false), así que cada voto llega como updatepoll_answery se convierte en un evento'poll'con el recuento, como en WhatsApp. Los miembros del chat ven quién votó;anonymousPolls: trueenvía encuestas anónimas, cuyos votos nunca llegan al bot. Se cuentan los votos de las encuestas que envió este proceso (en memoria, las últimas 500): un voto en una encuesta enviada antes de un reinicio se descarta. Los canales no admiten encuestas con votantes públicos: una encuesta a un canal se reintenta una vez como anónima (sin votos) y ese canal recibe encuestas anónimas desde entonces. - Servidor Bot API propio (self-hosted): apunta
baseUrla él. Si va por HTTP plano o en una red privada, las descargas de media necesitan ademásmediaDownload: { allowHttp: true, allowPrivateHosts: true }(y unmaxBytesmayor). Arráncalo sin--local: en ese modo las rutas de archivo no se sirven por HTTP.
import { TelegramProvider, isEditedMessage } from '@botmesh/telegram'
const selfHosted = new TelegramProvider({
token: process.env.TELEGRAM_TOKEN!,
baseUrl: 'http://telegram-bot-api:8081',
mediaDownload: { allowHttp: true, allowPrivateHosts: true, maxBytes: 200 * 1024 * 1024 },
})
bot.before('message', async (ctx, next) => { if (!isEditedMessage(ctx)) await next() }) // ignora ediciones@botmesh/messenger (experimental)
Facebook Messenger a través del Send API de Graph, con la misma verificación de webhook que la Cloud API.
import { MessengerProvider } from '@botmesh/messenger'
const provider = new MessengerProvider({
accessToken: process.env.FB_PAGE_TOKEN!, // Page access token (pages_messaging)
verifyToken: process.env.META_VERIFY_TOKEN!,
appSecret: process.env.META_APP_SECRET!,
})- Botones y listas nativos; las reacciones llegan pero no se pueden enviar (suscribe el webhook a
message_reactions;emojies el carácter yremovedse marca conunreact). - El texto se limita a 2000 caracteres (
maxTextLength): el texto más largo se divide en varios mensajes, y los trozos dectx.stream()caben en la Send API. - Más de 3 botones (plantilla de botones) o de 10 filas de lista (plantilla genérica) rechazan con
UnsupportedContentErroren vez de descartar opciones. - Cada llamada a la Send API vence tras
requestTimeoutMs(60 s por defecto).
@botmesh/instagram (experimental)
DMs de una cuenta profesional de Instagram conectada a una Página de Facebook (Messenger API for Instagram).
import { InstagramProvider } from '@botmesh/instagram'
const provider = new InstagramProvider({
accessToken: process.env.FB_PAGE_TOKEN!, // token of the connected Page
verifyToken: process.env.META_VERIFY_TOKEN!,
appSecret: process.env.META_APP_SECRET!,
})- Los botones salen como quick replies (como mucho 13; con más rechaza con
UnsupportedContentError); las listas se degradan a texto numerado. Cada petición a la Send API caduca trasrequestTimeoutMs(60 s). - Solo DMs (sin grupos); las reacciones llegan pero no se pueden enviar.
- El texto se limita a 1000 bytes UTF-8 (
maxTextBytes, además demaxTextLength1000), así que el texto no ASCII cabe menos: el texto más largo se divide en varios mensajes por bytes, y los trozos dectx.stream()también caben. - Más de 13 quick replies rechazan con
UnsupportedContentErroren vez de descartar opciones. - Cada llamada a la Send API vence tras
requestTimeoutMs(60 s por defecto).
@botmesh/discord (experimental)
Gateway de Discord (WebSocket) para los eventos y REST para enviar.
import { DiscordProvider, GatewayIntents } from '@botmesh/discord'
const provider = new DiscordProvider({ token: process.env.DISCORD_TOKEN! })
// Default intents: Guilds | GuildMessages | MessageContent.
// Add GatewayIntents.DirectMessages to the `intents` bitmask to receive DMs.- Botones, menús de selección, encuestas, reacciones y ediciones nativos;
ctx.stream()edita un mensaje a medida que crece el texto. - El bot mantiene una reacción por mensaje, como en WhatsApp/Telegram: un nuevo
ctx.react(emoji)reemplaza la anterior yctx.react('')la quita. Discord borra reacciones por emoji, así que el provider recuerda el que puso (en memoria,reactionMemory, 1000 mensajes por defecto); si no lo conoce (se puso antes de reiniciar o salió del LRU) rechaza conUnsupportedContentError. - Las interacciones tienen un plazo estricto de confirmación (
interactionsRequireAck), así que el core confirma cada una exactamente una vez por ti: una confirmación diferida antes del primer envío del handler, poco antes del plazo de ~3 s o al terminar el handler, lo que ocurra antes. Llama actx.acknowledge('texto')antes de responder para contestar con un texto inmediato; una vez enviada la confirmación automática,ctx.acknowledge('texto')envíatextocomo respuesta normal yctx.acknowledge()no hace nada. (Hasta2.0.0-alpha.2el core no confirmaba por su cuenta; los handlers que ya llaman primero actx.acknowledge()siguen funcionando igual.) ctx.interaction.ides el id de la interacción a secas: su token no sale del provider (la forma"<id>:<token>"de las alphas se sigue aceptando). Como red de seguridad, el provider también difiere cualquier interacción que siga sin respuesta trasautoAcknowledgeMs(2000 ms por defecto;autoAckMses un alias obsoleto;falselo desactiva). Unctx.acknowledge('texto')tardío, ya confirmada, sale como followup, conappIdo, si falta, con el id de aplicación que Discord manda en READY y en cada interacción; si no se conoce ninguno, la llamada rechaza conProviderErroren vez de descartar el texto.- Más de 25 botones (5 filas de 5), 25 opciones de menú o 10 respuestas de encuesta rechazan con
UnsupportedContentErroren vez de descartar opciones; el texto de más de 2000 caracteres se divide en varios mensajes. - Cada llamada REST vence tras
requestTimeoutMs(60 s por defecto); las subidas de archivos, trasuploadTimeoutMs(5 min). - Los mensajes de sistema (entradas al servidor, fijados, boosts...) y los de bots no se enrutan.
- Un mensaje reenviado trae el texto y la media reenviados (de su snapshot), no una respuesta citada; si no tiene nada legible llega como
unknown. Una encuesta nativa llega comotype: 'poll'con la pregunta como cuerpo. - Mensajes editados (
MESSAGE_UPDATEconedited_timestamp) llegan como mensajes con el texto nuevo y elref.idoriginal;isEditedMessage(ctx)de@botmesh/discordlos distingue yeditedMessages: 'ignore'los descarta. Las demás actualizaciones (previsualizaciones de enlaces, fijados, flags) no se enrutan: el provider entrega una edición una sola vez poredited_timestamp(recuerda el último de los 1000 mensajes editados más recientes), así que fijar un mensaje editado no lo vuelve a entregar.
@botmesh/evolution (experimental)
Evolution API v2, un gateway REST de WhatsApp self-hosted construido sobre Baileys. Salida: REST con la cabecera apikey. Entrada: el webhook de la instancia, autenticado con un secreto compartido en cabecera o con el jwt_key de Evolution (JWT HS256).
import { EvolutionProvider } from '@botmesh/evolution'
const provider = new EvolutionProvider({
baseUrl: 'http://localhost:8080',
apiKey: process.env.EVOLUTION_API_KEY!,
instance: 'my-instance',
webhookSecret: process.env.EVOLUTION_WEBHOOK_SECRET!, // or webhookJwtKey
})
// POST /webhook/evolution → res.sendStatus(provider.ingest(body, headers))start()comprueba la instancia; si no está vinculada, el QR o el código de emparejamiento llega en la actualizaciónconnection(authPayload).- Los gemelos LID/PN que envía Evolution se enlazan antes de emitir; pasa
identityPersistencepara mantener los ids estables entre reinicios. - Encuestas, reacciones, edición (texto), escribiendo y confirmaciones de lectura; botones, listas y cards se degradan a texto numerado;
templatese rechaza.
@botmesh/wppconnect (experimental)
WPPConnect Server, un gateway self-hosted sobre WhatsApp Web (WA-JS). REST con token Bearer; webhooks autenticados con un token en la URL del webhook.
import { WPPConnectProvider } from '@botmesh/wppconnect'
const provider = new WPPConnectProvider({
baseUrl: 'http://localhost:21465',
session: 'my-bot',
secretKey: process.env.WPP_SECRET_KEY!, // or token: '<generated bearer>'
webhookToken: process.env.WPP_WEBHOOK_TOKEN!, // webhook URL: …/webhooks/wpp?token=<it>
})
// POST /webhooks/wpp → res.sendStatus(await provider.ingest(body, headers, req.originalUrl))ingest()es asíncrono y recibe la URL de la petición como tercer argumento, porque wppconnect-server no puede añadir cabeceras: el token viaja en la query string.- Los LID desconocidos se resuelven a número de teléfono (
contact/pn-lid) antes de emitir. markReadmarca el chat entero como leído; botones y listas se degradan a texto numerado.
@botmesh/twilio (experimental)
Twilio Programmable Messaging: WhatsApp y SMS/MMS con una sola clase. Webhooks verificados con X-Twilio-Signature, que cubre la URL pública exacta.
import { TwilioProvider } from '@botmesh/twilio'
const provider = new TwilioProvider({
accountSid: process.env.TWILIO_SID!,
authToken: process.env.TWILIO_TOKEN!, // signs webhooks + REST auth
whatsappFrom: 'whatsapp:+14155238886',
smsFrom: '+15017122661',
webhookUrl: 'https://bot.example.com/twilio/webhook', // EXACT URL set in Twilio
})chatIdes elFromde Twilio tal cual (whatsapp:+57…o+57…): la misma persona por WhatsApp y por SMS son dos chats.- Texto de hasta 1600 caracteres; media solo desde URLs públicas; botones y listas pasan a texto numerado;
templatese traduce aContentSid+ContentVariables. ingestRequest({ url, body, headers })para URLs que cambian por petición (multi-tenant).- El error 63016 se traduce a
OutsideSessionWindowError. - Un mensaje con varios adjuntos (
NumMedia> 1, hasta 10) llega como un mensaje por adjunto, en orden: el primero conserva elMessageSidy el texto (Body); el adjuntoilleva el idMessageSid:iy cuerpo vacío (ids únicos y estables ante reintentos del webhook).
@botmesh/gohighlevel (experimental)
HighLevel (GoHighLevel / LeadConnector) Conversations API. Entrada por el webhook InboundMessage (Ed25519, X-GHL-Signature), salida con POST /conversations/messages por el último canal que usó el contacto.
import { GoHighLevelProvider } from '@botmesh/gohighlevel'
const provider = new GoHighLevelProvider({
accessToken: process.env.GHL_TOKEN!, // Private Integration Token
locationId: process.env.GHL_LOCATION_ID!,
// defaultChannel: 'SMS', // until the contact writes
})chatId=contactIdde HighLevel: un chat por contacto en todos los canales; el canal de cada mensaje está enctx.chat.platformData.channel.- La clave pública de HighLevel viene incluida (
GHL_WEBHOOK_PUBLIC_KEY), así que no hace falta secreto de webhook.ingest()necesita el body crudo. - Media solo desde URLs públicas;
templatese traduce atemplateId.
@botmesh/gupshup (experimental)
Gupshup WhatsApp API: endpoints de envío v1 y callbacks v2. Gupshup no firma los callbacks, así que se autentican con un token compartido en la URL del callback (o en una cabecera).
import { GupshupProvider } from '@botmesh/gupshup'
const provider = new GupshupProvider({
apiKey: process.env.GUPSHUP_API_KEY!,
appName: 'MyBotApp',
source: '917834811114', // the app's number
webhookToken: process.env.GUPSHUP_WEBHOOK_TOKEN!, // callback: …/gupshup?token=<it>
})
// POST /gupshup → res.sendStatus(provider.ingestRequest({ url: req.originalUrl, body, headers }))- Los botones salen como quick replies (hasta 3), después como lista (hasta 10) y después como texto numerado; las listas son nativas hasta 10 filas. El botón de la lista de los botones degradados usa
listButtonText(por defecto'Opciones', máximo 20 caracteres). chatId= los dígitos del teléfono del remitente (sin+), justo lo que espera la API de envío.- Media solo desde URLs públicas. Gupshup también documenta restringir por IP: haz ambas cosas en producción.
@botmesh/email (experimental)
Email: IMAP IDLE para recibir y SMTP para responder. Cada hilo de correo es un chat, así que los flujos con ask() continúan cuando el cliente responde al email. imapflow y nodemailer son peer dependencies opcionales que se cargan de forma lazy.
import { EmailProvider } from '@botmesh/email'
const provider = new EmailProvider({
address: 'support@acme.com',
imap: { host: 'imap.example.com', user: 'support@acme.com', pass: process.env.IMAP_PASS! },
smtp: { host: 'smtp.example.com', user: 'support@acme.com', pass: process.env.SMTP_PASS! },
})threading: 'thread'(por defecto, un chat por hilo) o'sender'(un chat por dirección); persiste los hilos conthreadStore: cursorThreadStore(store). Ese store guarda también el cursor de UID del buzón, así que tras un reinicio el bot sigue después del último mensaje que procesó y no vuelve a entregar correo (ni siquiera conmarkSeen: false).- Protección contra bucles de respuestas automáticas y listas de correo, gestión de rebotes (eventos
status: 'failed', supresión de direcciones con rebote permanente). - El email no autentica
Frompor sí mismo: usarequireAuthentication: { authservId }para aceptar solo correo que tu MTA haya autenticado. - Sin IMAP,
ingest(rawMime)acepta el MIME crudo de un webhook de correo entrante y resuelve{ status, accepted, reason? }. - El transporte creado desde
smtpse cierra enstop()y se reconstruye en el siguiente envío (también con pool); unsmtpTransportinyectado es tuyo ystop()nunca lo cierra.
@botmesh/tiktok (experimental)
TikTok API for Business (v1.3): comentarios en los videos propios de la cuenta y mensajes directos (Business Messaging API, requiere aprobación). Webhooks verificados con TikTok-Signature.
import { TikTokProvider } from '@botmesh/tiktok'
const provider = new TikTokProvider({
accessToken: process.env.TIKTOK_TOKEN!, // or an async refresh function
businessId: process.env.TIKTOK_BUSINESS_ID!, // open_id of the account
appSecret: process.env.TIKTOK_APP_SECRET!, // verifies TikTok-Signature
selfUniqueIdentifier: process.env.TIKTOK_SELF_UID!, // required to receive comments
})- Chats:
dm:<conversation_id>(DM),comment:<video_id>:<comment_id>(hilo de un comentario, un chat de grupo) yhic:<comment_id>(comentario de alta intención).ctx.chat.platformData.surfacelos distingue. - DMs: texto (hasta 6000 caracteres), imágenes, tarjetas Q&A para los botones (de 1 a 3); comentarios: respuestas de texto de hasta 150 caracteres. Los botones o listas que no caben en una tarjeta Q&A (más de 3 opciones, botones con URL, títulos de más de 40 caracteres) y los menús enviados a un hilo de comentarios se degradan a un menú de texto numerado, como en Baileys.
- El bot no puede iniciar una conversación y TikTok limita los mensajes por ventana de 48 h (
OutsideSessionWindowError).
Migrar desde las alphas de 2.0
El contrato común de los canales cambió algunos comportamientos de las versiones alpha. Revisa esto al actualizar:
ctx.timestampes milisegundos epoch en todas partes. Baileys, la Cloud API yMockProviderinformaban segundos epoch. Quita cualquier* 1000(new Date(ctx.timestamp)ya es correcto). Lo mismo vale para los timestamps deInboundMessage,InboundInteractionyDeliveryStatusen hooks propios. Un provider de terceros que aún emite segundos sigue funcionando: el core convierte un valor menor que1e12paractx.timestampy el log de mensajes, pero actualízalo para emitir ms (assertInboundContractlo detecta).- Las pulsaciones de botones en Telegram llevan la hora de recepción, no la fecha del mensaje que tiene el teclado.
- Se quitaron métodos que no hacían nada:
CloudAPIProvider#typing()y#deleteMessage(),TelegramProvider#markRead()yDiscordProvider#markRead(). Usactx.typing()/ctx.delete()/ctx.markRead()(que ahí no hacen nada) oprovider.typing?.(…). - El texto largo se divide en varios mensajes en vez de rechazar con
PayloadTooLargeError.new Bot({ splitLongText: false })recupera el comportamiento anterior. Los hooksbefore('send')siguen corriendo una vez por mensaje (sobre el texto completo, antes de dividirlo); losafter('send'), una vez por parte. - Demasiadas opciones rechazan en Discord (más de 25 botones u opciones de menú, más de 10 respuestas de encuesta), Messenger (más de 3 botones, más de 10 filas de lista) e Instagram (más de 13 quick replies) con
UnsupportedContentError; antes descartaban las opciones sobrantes en silencio. - Discord difiere las interacciones sin responder tras 2 s.
autoAcknowledgeMs: falserecupera el comportamiento anterior. Unctx.acknowledge('texto')tardío sale entonces como followup; sinappIdni id de aplicación recibido de Discord, rechaza. MockProviderinformacapabilities.typingyreadReceiptscomofalsesalvo que exista un métodotyping()/markRead()(antes tampoco tenían efecto), y emite timestamps en ms epoch.RateLimitedError.retryAfterMstiene un tope de 24 h.- Las encuestas de Telegram se envían no anónimas para que sus votos lleguen al bot como eventos
'poll'.new TelegramProvider({ anonymousPolls: true })mantiene las encuestas anónimas. - Las ediciones de Discord se entregan como mensajes con el
ref.idoriginal; un handler que no deba verlas compruebaisEditedMessage(ctx)o usaeditedMessages: 'ignore'. - Helpers renombrados (los nombres antiguos son alias deprecados hasta v3):
pollTimeoutde Telegram (segundos) →pollTimeoutMs;toProviderError→toTelegramError,toDiscordError,toMessengerError,toInstagramError,toTikTokError;toEmailProviderError→toEmailError; eltoProviderErrorde@botmesh/meta→toGraphErrorde@botmesh/graph-core.
Siguientes pasos
- Servidor de webhooks: monta los providers por webhook con
@botmesh/server. - Ventana de 24 h y costos: ventana de sesión de la Cloud API y seguimiento de uso.
- Mensajes y media: qué hace cada helper en cada transporte.