@botmesh/core - v2.0.0-alpha.2
    Preparing search index...

    Class Bot

    Hierarchy

    • EventEmitter
      • Bot
    Index
    logger: Logger
    ready: Promise<void>

    Resolves when the bot reaches the 'ready' connection state.

    blacklist: AccessList = ...

    Senders/chats whose inbound messages are dropped before anything else (not persisted, no hooks, no routing, no 'message' event). Ids are matched exactly against ctx.sender and ctx.from. See AccessList.

    whitelist: Whitelist = ...

    When enabled, only senders/chats on this list are processed (the blacklist still wins). Disabled by default. See Whitelist.

    • Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

      server.on('connection', (stream) => {
      console.log('someone connected!');
      });

      Returns a reference to the EventEmitter, so that calls can be chained.

      By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

      import { EventEmitter } from 'node:events';
      const myEE = new EventEmitter();
      myEE.on('foo', () => console.log('a'));
      myEE.prependListener('foo', () => console.log('b'));
      myEE.emit('foo');
      // Prints:
      // b
      // a

      Type Parameters

      Parameters

      • event: K
      • listener: (...args: BotEventArgs<K>) => void

        The callback function

      Returns this

      v0.1.101

    • Parameters

      • event: string
      • listener: (...args: any[]) => void

      Returns this

    • Adds a one-time listener function for the event named eventName. The next time eventName is triggered, this listener is removed and then invoked.

      server.once('connection', (stream) => {
      console.log('Ah, we have our first user!');
      });

      Returns a reference to the EventEmitter, so that calls can be chained.

      By default, event listeners are invoked in the order they are added. The emitter.prependOnceListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

      import { EventEmitter } from 'node:events';
      const myEE = new EventEmitter();
      myEE.once('foo', () => console.log('a'));
      myEE.prependOnceListener('foo', () => console.log('b'));
      myEE.emit('foo');
      // Prints:
      // b
      // a

      Type Parameters

      Parameters

      • event: K
      • listener: (...args: BotEventArgs<K>) => void

        The callback function

      Returns this

      v0.3.0

    • Parameters

      • event: string
      • listener: (...args: any[]) => void

      Returns this

    • Alias for emitter.removeListener().

      Type Parameters

      Parameters

      Returns this

      v10.0.0

    • Parameters

      • event: string
      • listener: (...args: any[]) => void

      Returns this

    • Synchronously calls each of the listeners registered for the event named eventName, in the order they were registered, passing the supplied arguments to each.

      Returns true if the event had listeners, false otherwise.

      import { EventEmitter } from 'node:events';
      const myEmitter = new EventEmitter();

      // First listener
      myEmitter.on('event', function firstListener() {
      console.log('Helloooo! first listener');
      });
      // Second listener
      myEmitter.on('event', function secondListener(arg1, arg2) {
      console.log(`event with parameters ${arg1}, ${arg2} in second listener`);
      });
      // Third listener
      myEmitter.on('event', function thirdListener(...args) {
      const parameters = args.join(', ');
      console.log(`event with parameters ${parameters} in third listener`);
      });

      console.log(myEmitter.listeners('event'));

      myEmitter.emit('event', 1, 2, 3, 4, 5);

      // Prints:
      // [
      // [Function: firstListener],
      // [Function: secondListener],
      // [Function: thirdListener]
      // ]
      // Helloooo! first listener
      // event with parameters 1, 2 in second listener
      // event with parameters 1, 2, 3, 4, 5 in third listener

      Type Parameters

      Parameters

      Returns boolean

      v0.1.26

    • Parameters

      • event: string
      • ...args: any[]

      Returns boolean

    • Stop the bot. By default aborts conversations and tears down immediately.

      Parameters

      • Optionalopts: StopOptions

        Options of Bot.stop.

        • Optionaldrain?: boolean

          Wait for in-flight sends to complete before stopping (default false).

        • Optionaltimeout?: string | number

          Max time to wait for the drain before forcing the stop: a duration ('5s') or ms. Default 10000.

      Returns Promise<void>

    • Durable fire-and-forget send (needs BotConfig.durability): runs the before('send') hooks, journals the send and returns its outbox key at once; the background sweeper delivers it (retries, backoff, dead-letter queue). Enqueueing an idempotencyKey that already exists is a no-op (status: 'exists') — use it for campaigns and scheduled jobs that may run twice. A text over the channel's limit is split after the hooks (BotConfig.splitLongText) into one record per part: key, key#2, ….

      Parameters

      Returns Promise<{ key: string; status: "exists" | "queued" | "skipped" }>

      await bot.enqueue(chatId, 'Tu cita es mañana', { idempotencyKey: `reminder:${citaId}` })
      
    • Send plain text to a chat. Same as send(chatId, text).

      Parameters

      • chatId: string
      • text: string

      Returns Promise<SentMessage<unknown>>

      Use send: bot.send(chatId, text) (a string is sent as text).

    • Send content (a string is sent as text) to many chats: in batches of batchSize (default 10) with concurrency parallel sends (default 1), pausing delayBetween between batches. A RateLimitedError pauses the whole broadcast for its retryAfterMs and retries that chat (up to retries times after the first send, default 3); other errors are recorded and the broadcast goes on. Abort signal to stop early — never-attempted chats come back in pending. bot.stop() cancels it the same way (with drain, stop waits for its in-flight sends), and a broadcast started on a bot that is not started (never started, or stopped) sends nothing. Duplicate ids are sent once. Each send takes the normal outbound path (send hooks, ledger, the provider's rate limiter) but not the dispatcher's per-chat queues, so inbound handling is never blocked; still, launch it with void (not await) from inside a handler so that chat's queue is not held.

      Parameters

      Returns Promise<BroadcastResult>

      const ac = new AbortController()
      const res = await bot.broadcast(chatIds, '🎉 Nueva promo!', {
      batchSize: 10, delayBetween: '2s', signal: ac.signal,
      onProgress: (done, total) => bot.logger.info(`${done}/${total}`),
      onFail: (chatId, error) => bot.logger.warn({ chatId, err: error }, 'broadcast failed'),
      })
      // → { total: 500, sent: 480, failed: 20, skipped: 0, errors: [{ chatId, error }], messages, cancelled: false, pending: [] }
    • Start a registered flow/conversation for a subject without waiting for them to write — reminders from bot.schedule(), webhooks, admin panels. data seeds c.state. The start goes through the same gates as a message (blacklist / whitelist, human handoff) and the chat's FIFO queue; by default a subject that already has a live or dormant conversation is left alone (ifActive: 'replace' cancels it). Resolves once the conversation is entered — not when it ends (listen to 'conversation:ended' for that). Rejects when name is not registered. Safe to await from a handler or command of the same chat: the trigger then runs inline in that message's queue slot instead of queuing behind it (which would deadlock the chat). Awaiting a trigger for a DIFFERENT chat whose handlers in turn await a trigger back into this one can still deadlock — launch those with void. Before-message hooks, matchers and the 'message' event do not run, and nothing is persisted for the synthetic inbound (ctx.raw is { synthetic: 'trigger', name }).

      Parameters

      Returns Promise<TriggerResult>

      bot.schedule('0 9 * * *', async () => {
      for (const cita of await db.citasDeHoy()) {
      await bot.trigger('recordatorio', { chatId: cita.chatId, data: { hora: cita.hora } })
      }
      })
    • Snapshot of the runtime counters: conversations (started/completed/cancelled/ timedOut/retriesExceeded/errored, active, mean duration) overall and per flow, messages in/out, inbound drops (duplicate/blocked/handoff), triggers, errors and uptime. In-process only. Render it for Prometheus with toPrometheus().

      Returns BotMetricsSnapshot

    • Hand chatId over to a human agent. Until resume, the Dispatcher skips every routing stage for that chat (commands, conversations, dormant resume, matchers, catch-all) except BotConfig.handoff.allowCommands; inbound messages are still persisted, hooks still run and the 'message' event still fires. Active conversations in the chat are cancelled silently and its dormant checkpoints dropped. Emits 'handoff' and persists the state on StoredChat.handoff (survives restarts with a persistent store).

      The state flips synchronously, so not awaiting is safe. Resolves true if the chat was not already handed off; never rejects for store failures (they go to the 'error' event). Throws TypeError synchronously on an empty chat id.

      Parameters

      Returns Promise<boolean>

    • Give a handed-off chat back to the bot: routing resumes immediately, 'resume' fires and the persisted state is cleared. Resolves true if the chat was handed off.

      Parameters

      Returns Promise<boolean>

    • Is chatId currently handed off to a human?

      Parameters

      • chatId: string

      Returns boolean

    • Snapshot of the chat's handoff state (since, reason, meta), or undefined.

      Parameters

      • chatId: string

      Returns HandoffState | undefined

    • Send to a chat from outside the inbound pipeline — typically a human agent's message during a handoff. Same path as send (before/after('send') hooks, outbound ledger); it is the same call as send, named for handoff code. Works whether or not the chat is handed off and never resumes the bot by itself.

      Parameters

      Returns Promise<SentMessage<unknown>>

      Use send: bot.send(chatId, content, options) (the very same call).

    • Register an async conversation, entered when opts.trigger (default: name) matches. ctx is the full MessageContext of the triggering message (ctx.tools, media helpers, react…); c offers ask/waitText/waitMedia/choose, retry(), idle()/idleClear(), end(farewell?) and goto(name).

      Parameters

      Returns () => void

      bot.conversation('ahorcado', async (c, ctx) => {
      c.idle('2m', () => c.end('⏰ Te desconecté por inactividad'))
      let letra = await c.ask('Escribe una letra')
      while (letra.length !== 1) letra = await c.retry('❌ Solo una letra')
      await ctx.react('✅')
      })
    • Enter an inline ConversationHandler for the chat (or group participant) of ctx. To start a REGISTERED conversation or flow, use trigger (the by-name form here is deprecated).

      Fire-and-forget by design: it returns immediately, so a handler can start a conversation and return, releasing the chat's FIFO queue for the user's next message (which the conversation then receives). Any conversation already active on that key is replaced. Failures (e.g. an unknown name) are reported through the 'error' event, never as an unhandled rejection.

      The returned promise settles (it never rejects) once the conversation has ended and released its key. Never await it inside a handler: that blocks the chat's queue, so the user's answer could never reach the conversation.

      Parameters

      Returns Promise<void>

      bot.when('pedido', async (ctx) => {
      bot.enterConversation(async (c) => {
      const ok = await c.ask('¿Confirmas? (sí/no)')
      await c.say(ok === 'sí' ? 'Hecho' : 'Cancelado')
      }, ctx)
      })
    • Parameters

      Returns Promise<void>

      Start a registered conversation with trigger: bot.trigger(name, { chatId: ctx.from, userId: ctx.sender, isGroup: ctx.isGroup, ifActive: 'replace' }) (safe to await from a handler of the same chat).

    • Register a /name command (wins over an active conversation). Returns an unsubscriber.

      Parameters

      Returns () => void

    • Catch-all handler: receives the messages no command, conversation or matcher took. Not to be confused with bot.on('message', …), the event that observes every inbound message. Returns an unsubscriber.

      Parameters

      Returns () => void

    • Remove a flow or conversation by id: its triggers stop matching. Conversations already running finish on the version they entered with. Returns false when unknown.

      Parameters

      • id: string

      Returns boolean

    • Whether a flow or conversation is registered under id.

      Parameters

      • id: string

      Returns boolean

    • Start flow id proactively in a chat, without an inbound message (the user's replies then flow into it as usual). A thin wrapper over trigger with ifActive: 'replace' — same access-list and handoff gates, per-chat FIFO slot, language resolution, messaging seams and metrics — so it replaces a conversation already running (or dormant) on the same key. Resolves false when the flow is unknown, the subject is refused by the access lists (blacklist / whitelist) or the chat is handed off; rejects when the bot is not started.

      Parameters

      Returns Promise<boolean>

      Use trigger: bot.trigger(id, { chatId, userId, isGroup, data, body, ifActive: 'replace' }) (it resolves a TriggerResult; result.started is this method's boolean).

    • Re-arm the WELCOME trigger so the subject's next message is treated as a first contact again (persisted in the store, so it survives restarts).

      • resetWelcome(dmChatId) — the user of that DM.
      • resetWelcome(groupId, userId) — one participant of a group.
      • resetWelcome(groupId) — every participant of that group.

      Called from a handler of the same chat it is ordered with that chat's messages (the dispatcher's FIFO); called from elsewhere it races like any last-writer-wins store write. Rejects if the store write fails (the in-process state is re-armed regardless). slashCommandsPlugin calls it on /reset.

      Parameters

      • chatId: string
      • OptionaluserId: string

      Returns Promise<void>

      bot.command('bienvenida', async (ctx) => {
      await bot.resetWelcome(ctx.from, ctx.isGroup ? ctx.sender : undefined)
      await ctx.reply('Listo: tu próximo mensaje recibirá la bienvenida otra vez.')
      })
    • True while the bot is still reacting to what it received: a provider event is being stored or dispatched to its handlers (e.g. one awaiting an AI call), or a conversation is running a step instead of waiting for the user's reply. Work a handler starts without awaiting it is not tracked. Diagnostics only — botmesh dev uses it to wait for slow replies when its input is piped.

      Returns boolean

    • Install a plugin (optionally with config). Unlike use, returns an unsubscriber (not the bot: do not chain) that first calls the plugin's uninstall() and then removes, newest first, what it registered through ctx.bot: commands, matchers, conversations/flow triggers, hooks, nested plugins, runtime flows and onDispose callbacks. Calling it again is a no-op. It does NOT undo event listeners (on/once), provide()d tools, locale() messages, schedule/every/delay jobs, onIdle handlers or a flow replaceFlow() overwrote: undo those in ctx.onDispose().

      install() should be synchronous. If it throws, whatever it registered so far is rolled back and the error is rethrown; if it returns a promise that rejects, the rejection goes to the 'error' event instead of crashing the process.

      spec.requires lists plugin names that must already be plugged (a nested plugin may require its parent); a missing one throws before install() runs. Flows registered through ctx.bot.registerFlow() (or created by ctx.bot.replaceFlow()) are unregistered on unplug.

      One instance per plugin name: plugging a name that is already plugged throws (unplug the first instance before plugging it again), unless every instance sets spec.multiple: true.

      Parameters

      • plugin: Plugin
      • Optionalconfig: Record<string, unknown>

      Returns () => void

      when a plugin named in spec.requires is not plugged, or when a plugin with the same spec.name is already plugged and spec.multiple is not set.

    • Register (deep-merge) the messages of locale code, or — with an options object — configure i18n (defaultLocale, detect, providerLanguage, userKey, locales). Either form turns on per-message language resolution: ctx.user preference → provider-reported language → detect → default, limited to the locales allow-list when one is set. Keys nest (ctx.t('menu.title')), {param} placeholders are interpolated and { one, other, zero? } leaves pick a plural form from params.count. The built-in core messages live under core.* (es and en ship) and can be overridden here. Throws TypeError on an invalid code or message tree.

      Parameters

      Returns this

      bot.locale('es', { greeting: '¡Hola, {name}!', cart: { one: '{count} producto', other: '{count} productos' } })
      bot.locale('en', { greeting: 'Hello, {name}!', cart: { one: '{count} item', other: '{count} items' } })
      bot.locale({ defaultLocale: 'es' })
      bot.when('hola', (ctx) => ctx.reply(ctx.t('greeting', { name: ctx.pushName ?? '' })))
      bot.command('lang', async (ctx, [code]) => {
      if (!code || !bot.locales().includes(code.toLowerCase())) return ctx.reply(bot.locales().join(', '))
      await ctx.setLang(code)
      })
    • Register (deep-merge) the messages of locale code, or — with an options object — configure i18n (defaultLocale, detect, providerLanguage, userKey, locales). Either form turns on per-message language resolution: ctx.user preference → provider-reported language → detect → default, limited to the locales allow-list when one is set. Keys nest (ctx.t('menu.title')), {param} placeholders are interpolated and { one, other, zero? } leaves pick a plural form from params.count. The built-in core messages live under core.* (es and en ship) and can be overridden here. Throws TypeError on an invalid code or message tree.

      Parameters

      Returns this

      bot.locale('es', { greeting: '¡Hola, {name}!', cart: { one: '{count} producto', other: '{count} productos' } })
      bot.locale('en', { greeting: 'Hello, {name}!', cart: { one: '{count} item', other: '{count} items' } })
      bot.locale({ defaultLocale: 'es' })
      bot.when('hola', (ctx) => ctx.reply(ctx.t('greeting', { name: ctx.pushName ?? '' })))
      bot.command('lang', async (ctx, [code]) => {
      if (!code || !bot.locales().includes(code.toLowerCase())) return ctx.reply(bot.locales().join(', '))
      await ctx.setLang(code)
      })
    • Translate outside a handler (scheduled jobs, broadcasts): lang defaults to the default locale. Pair it with langOf to use a user's stored preference.

      Parameters

      Returns string

    • The stored language preference of userId (ctx.setLang) when a registered locale serves it, else the default locale — the same language ctx.lang would resolve from the stored value (always the default while i18n is off). A store read failure resolves the default (logged at debug level).

      Parameters

      • userId: string

      Returns Promise<string>

    • Registered locale codes, built-in es/en included.

      Returns string[]

    • Type Parameters

      • T extends string

      Parameters

      • name: T
      • value: unknown

      Returns this

    • Delete the stored schedule of a durable task id (the bot's store namespace is applied, as on registration): an orphan logged at start (stored, no longer registered in code), a one-shot tombstone, or a registered task, which is cancelled as well.

      Parameters

      • id: string

      Returns Promise<void>

    • Run handler once when a chat stays quiet for duration ('10m', '1h', '1d', ms…) after its last processed inbound message. Every new inbound message from that chat reschedules it, and re-arms it after it fired. ctx is that last message's context, so ctx.reply('¿Sigues ahí?') answers in the right chat. Handler errors go to the bot 'error' event. Pending timers are cancelled by clearIdle, by the returned unsubscribe and by stop(). The handler never runs for a chat in human handoff: a 'handoff' event carrying { chatId } cancels that chat's timers, and its messages do not re-arm them until a 'resume' event for that chat (the bot's own isHandedOff() state is honoured too, when present). duration must be positive and at most 2^31 − 1 ms (about 24.8 days, the longest a Node timer can wait); otherwise it throws RangeError.

      Parameters

      Returns () => void

      bot.onIdle('10m', async (ctx, { idleMs }) => {
      await ctx.reply(`¿Sigues ahí? (${idleMs / 60_000} min sin respuesta)`)
      })
    • Cancel pending onIdle timers for chatId (every chat when omitted) — e.g. when a human agent takes the chat over. They re-arm on that chat's next processed inbound message.

      Parameters

      • OptionalchatId: string

      Returns void

    • Persistent profile of any subject — the same data a handler sees as ctx.user when that subject writes (scheduled jobs, webhooks, admin code). Requires a store implementing the user-data contract (every built-in one).

      Parameters

      • userId: string

      Returns UserData

      await bot.user('5491122334455').ns('billing').set('plan', 'pro')
      
    • Physically remove expired ctx.user entries store-wide and resolve with how many were removed (0 with native-TTL stores such as Redis). Writes already trigger this at most once a minute; schedule it for idle bots: bot.every('1h', () => bot.purgeExpiredUserData()).

      Returns Promise<number>

    • Delete normalized subject data in this bot namespace, including the handoff of chat userId and the subject's live or dormant conversations.

      Parameters

      • userId: string

      Returns Promise<SubjectDeletionResult>

    • Read-only snapshot of the registered flows, conversations and trigger routes, plus the bits of config the linter needs. Input of describeFlows()/lintFlows().

      Returns FlowInventory

    • Describe every registered flow/conversation as a Mermaid flowchart (default) or, with { format: 'json' }, as a JSON graph. See describeFlows.

      Parameters

      Returns FlowGraph

      writeFileSync('flows.mmd', bot.describe())        // open in mermaid.live or any Mermaid viewer
      const graph = bot.describe({ format: 'json' }) // { version, flows, groups, nodes, edges }
    • Describe every registered flow/conversation as a Mermaid flowchart (default) or, with { format: 'json' }, as a JSON graph. See describeFlows.

      Parameters

      Returns string

      writeFileSync('flows.mmd', bot.describe())        // open in mermaid.live or any Mermaid viewer
      const graph = bot.describe({ format: 'json' }) // { version, flows, groups, nodes, edges }
    • Describe every registered flow/conversation as a Mermaid flowchart (default) or, with { format: 'json' }, as a JSON graph. See describeFlows.

      Parameters

      Returns string | FlowGraph

      writeFileSync('flows.mmd', bot.describe())        // open in mermaid.live or any Mermaid viewer
      const graph = bot.describe({ format: 'json' }) // { version, flows, groups, nodes, edges }
    • Statically lint the registered flows: unreachable steps/flows, missing goto targets, empty/duplicate triggers, risky timeouts, ambiguous menus, … See lintFlows.

      Parameters

      Returns LintDiagnostic[]

      for (const d of bot.lint({ minSeverity: 'warning' })) bot.logger.warn(d, d.message)
      
    • get durability(): DurabilityControl | undefined

      Durable message processing controls — the inbox/outbox journals, operator replay/retry of dead-lettered records and counters. undefined unless BotConfig.durability is set.

      Returns DurabilityControl | undefined

      for (const r of await bot.durability!.outbox.list({ state: 'dead' })) await bot.durability!.retrySend(r.key)
      
    • get tracing(): BotTracing

      The bot's tracing (BotConfig.telemetry): start your own spans under the bot's, or check enabled. Plugins (@botmesh/ai) use it to add their spans to the same trace. Without telemetry it is a disabled no-op (BotTracing.enabled false).

      Returns BotTracing

      bot.when('pedido', async (ctx) => {
      await bot.tracing.trace('erp.lookup', {}, () => erp.find(ctx.body))
      })