npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@znxki_/tgbot-lib

v2.0.3

Published

Feature-rich TypeScript library for node-telegram-bot-api

Readme

🤖 tgbot-lib

A feature-rich, modular TypeScript framework for building Telegram bots with node-telegram-bot-api — inspired by the command architecture of Bukkit/Spigot for Minecraft.

TypeScript Node License Build PRs Welcome


📋 Table of Contents


✨ Features

| Feature | Description | |---|---| | 🧱 Command System | Abstract Command class, registry, aliases, per-command cooldowns, permissions and scope guards | | 🔗 Middleware Pipeline | Koa-style async/await middleware chain with next() | | 💬 Conversations | Multi-step wizard/form flows with validation, transformation and cancel support | | ⌨️ Keyboard Builders | Fluent builders for InlineKeyboard, ReplyKeyboard and ForceReply | | 📄 Pagination | Auto-paginated message lists with inline navigation buttons | | ⏰ Scheduler | Cron expressions, fixed intervals and one-shot Date-based jobs | | 🌍 i18n | Per-user locale management with {{variable}} interpolation | | 🚦 Rate Limiter | Per-key, per-user sliding window rate limiting | | 📦 Session Storage | In-memory and file-based JSON session stores with optional TTL | | 📡 Callback Router | Path-style routing for callback_query data ("user/:id/action") | | 🛠️ Message Utils | Broadcast, split long messages, safe edit, delete many, HTML/MD escape | | 📝 Logger | Coloured, levelled, child-scope logger | | 🔒 Guards | Scope (private/group), owner-only, user whitelist, member status |


📦 Installation

npm i @znxki_/tgbot-lib 

🚀 Quick Start

import { BotClient, Command, CommandContext, CommandMeta } from "./src";

class PingCommand extends Command {
  readonly meta: CommandMeta = {
    name: "ping",
    description: "Check if the bot is alive",
    cooldown: 5,
  };

  async execute({ reply }: CommandContext): Promise<boolean> {
    await reply("🏓 Pong!");
    return true;
  }
}

const client = new BotClient({
  token: process.env.BOT_TOKEN!,
  ownerIds: [123456789],
  logLevel: "info",
});

await client.registerAndSync(new PingCommand());
await client.start();

🗂️ Project Structure

src/
├── index.ts                  ← Barrel export (public API)
├── BotClient.ts              ← Main orchestrator class
├── types/
│   └── types.ts              ← All shared interfaces and types
├── commands/
│   └── Command.ts            ← Command, CommandRegistry, CommandExecutor
├── middleware/
│   └── Middleware.ts         ← MiddlewarePipeline + built-in factories
├── session/
│   └── SessionManager.ts     ← SessionManager, MemoryStore, FileStore
├── conversation/
│   └── ConversationManager.ts← Multi-step wizard flows
├── keyboard/
│   └── KeyboardBuilder.ts    ← InlineKeyboardBuilder, ReplyKeyboardBuilder
├── pagination/
│   └── PaginationManager.ts  ← Paginated message lists
├── scheduler/
│   └── Scheduler.ts          ← Cron, interval, one-shot jobs
├── i18n/
│   └── I18n.ts               ← Translations and locale management
├── ratelimit/
│   └── RateLimiter.ts        ← Per-key sliding window limiter
├── utils/
│   ├── MessageUtils.ts       ← Static message helpers
│   └── CallbackRouter.ts     ← callback_query pattern router
└── logger/
    └── Logger.ts             ← Coloured levelled logger

⚙️ Configuration

BotClient accepts a BotConfig object:

const client = new BotClient({
  token: "YOUR_BOT_TOKEN",           // Required. Telegram bot token.
  ownerIds: [123456789],             // Optional. User IDs treated as owners.
  defaultLocale: "en",               // Optional. Default i18n locale. Default: "en"
  logLevel: "debug",                 // Optional. "debug"|"info"|"warn"|"error"|"none"
  polling: { interval: 300 },        // Optional. Polling options (node-telegram-bot-api)
  webhook: {                         // Optional. Mutually exclusive with polling.
    url: "https://example.com/hook",
    port: 443,
    secretToken: "my-secret",
    tlsOptions: { key: "...", cert: "..." },
  },
  session: new FileSessionStore(),   // Optional. Custom session store. Default: MemorySessionStore
});

📚 Wiki


BotClient

src/BotClient.ts

The central orchestrator that wires together all subsystems. Create a single instance per bot.

Constructor

new BotClient(config: BotConfig)

On construction, BotClient:

  1. Instantiates TelegramBot in polling or webhook mode.
  2. Creates all subsystem instances.
  3. Attaches a single "message" listener that runs the middleware pipeline, checks for active conversations, and dispatches commands.

Properties

| Property | Type | Description | |---|---|---| | bot | TelegramBot | The raw node-telegram-bot-api instance. | | log | Logger | Logger scoped to "BotClient". | | session | SessionManager | Session storage manager. | | registry | CommandRegistry | Command registry (lookup, listing). | | executor | CommandExecutor | Runs guards and executes commands. | | pipeline | MiddlewarePipeline<Message> | Middleware chain for all incoming messages. | | conversations | ConversationManager | Multi-step conversation flows. | | pagination | PaginationManager | Paginated list messages. | | scheduler | Scheduler | Job scheduler. | | i18n | I18n | Translation manager. | | rateLimiter | RateLimiter | Per-key rate limiting. | | callbackRouter | CallbackRouter | callback_query routing. |

Methods


register(...commands: Command[]): this

Register one or more commands into the registry without syncing Telegram's command list.

client.register(new StartCommand(), new HelpCommand());

Returns this for chaining.


registerAndSync(...commands: Command[]): Promise<this>

Register commands and push the visible command list to Telegram via setMyCommands(). This is what users see when they type / in the chat.

await client.registerAndSync(new StartCommand(), new HelpCommand());

Returns Promise<this> for chaining.


use(...fns: MiddlewareFn[]): this

Add one or more middleware functions to the message pipeline. Middleware runs in registration order before any command is dispatched.

client.use(loggerMiddleware(), typingMiddleware(client.bot));

start(): Promise<void>

Fetches bot info via getMe(), logs it, and starts the scheduler. Call after registering commands.

await client.start();

stop(): Promise<void>

Stops the scheduler and gracefully stops polling. Safe to call on SIGINT/SIGTERM.

process.once("SIGINT", () => client.stop());

Command System

src/commands/Command.ts


Command (abstract class)

The base class for all bot commands. Inspired by Bukkit's CommandExecutor. Extend it, define meta, implement execute().

class MyCommand extends Command {
  readonly meta: CommandMeta = {
    name: "mycommand",
    description: "Does something",
  };

  async execute(ctx: CommandContext): Promise<boolean> {
    await ctx.reply("Hello!");
    return true;
  }
}
Abstract members

| Member | Type | Description | |---|---|---| | meta | CommandMeta | Command configuration. Must be defined. | | execute(ctx) | Promise<boolean> \| boolean | Main handler. Return false to auto-send the usage hint (if meta.usage is defined). |

Optional lifecycle hooks

| Hook | Signature | Description | |---|---|---| | onRegister | (bot: TelegramBot) => void | Called once when the command is registered. Useful for setup. | | onInvoke | (ctx: CommandContext) => void | Called on every invocation, before execute. Useful for logging. |


CommandMeta interface

Describes the behaviour and constraints of a command.

| Field | Type | Default | Description | |---|---|---|---| | name | string | required | The trigger without /. Must be lowercase and unique. | | description | string | required | Shown in the Telegram command list. | | usage | string | undefined | Hint sent when execute() returns false. Example: "/echo <text>". | | aliases | string[] | [] | Alternative triggers. E.g. ["s", "begin"] for /start. | | category | string | "General" | Groups commands in registry.byCategory(). | | hidden | boolean | false | Hides the command from toBotCommands() and visible(). | | permissions | CommandPermission | undefined | Access control rules. If omitted, everyone can use it. | | scope | ChatScope | "all" | Restricts where the command works. | | cooldown | number | 0 | Per-user cooldown in seconds. 0 = no cooldown. | | deleteAfter | number | undefined | Auto-deletes the bot's usage-hint reply after N milliseconds. |


CommandContext interface

The context object passed to execute(). Provides the bot instance, message, parsed arguments and convenience reply helpers.

| Field | Type | Description | |---|---|---| | bot | TelegramBot | Raw bot instance. | | msg | Message | The incoming Telegram message. | | args | string[] | Arguments split by whitespace, filtered empty. | | rawArgs | string | Everything after the command trigger, unsplit. | | matchedTrigger | string | The exact trigger used (could be an alias). | | session | Record<string, unknown> | Persistent per-user/chat data. Mutate freely; it is saved automatically. | | reply(text, opts?) | Promise<Message> | Send a reply to the original message. | | replyHTML(text) | Promise<Message> | Reply with parse_mode: "HTML". | | replyMD(text) | Promise<Message> | Reply with parse_mode: "Markdown". |


CommandPermission interface

| Field | Type | Description | |---|---|---| | ownerOnly | boolean | If true, only users in BotConfig.ownerIds can run this command. | | allowedUsers | number[] | Whitelist of Telegram user IDs. | | allowedStatuses | ChatMemberStatus[] | Allowed chat member statuses (e.g. ["administrator", "creator"]). Only checked in group chats. |


CommandRegistry

Manages the command store. Accessed via client.registry.

register(cmd, bot): void

Stores the command under its name and all aliases. Calls cmd.onRegister(bot).

unregister(nameOrAlias: string): boolean

Removes a command and all its aliases. Returns true if found and removed.

find(trigger: string): Command | undefined

Looks up a command by trigger (case-insensitive). Returns undefined if not found.

all(): Command[]

Returns every registered command (including hidden ones).

visible(): Command[]

Returns commands where meta.hidden !== true.

byCategory(): Map<string, Command[]>

Groups visible commands by meta.category. Useful for generating structured help menus.

const categories = client.registry.byCategory();
for (const [cat, cmds] of categories) {
  console.log(cat, cmds.map(c => c.meta.name));
}
toBotCommands(): TelegramBot.BotCommand[]

Returns the array expected by bot.setMyCommands().


CommandExecutor

Runs the guard chain and invokes a command. Accessed via client.executor. You rarely need to use this directly.

run(bot, msg, cmd): Promise<void>
  1. Runs guardScope — checks meta.scope against the chat type.
  2. Runs guardPermissions — checks owner, user whitelist, member status.
  3. Runs guardCooldown — enforces per-user cooldown.
  4. Builds CommandContext.
  5. Calls cmd.onInvoke(ctx).
  6. Calls cmd.execute(ctx).
  7. If execute returns false, sends the usage hint (and schedules deletion if deleteAfter is set).
  8. Saves session data.

Middleware

src/middleware/Middleware.ts


MiddlewarePipeline<T>

A generic Koa-style pipeline. The default type parameter is TelegramBot.Message.

const pipeline = new MiddlewarePipeline<TelegramBot.Message>();
pipeline.use(async (msg, next) => {
  console.log("Before");
  await next();
  console.log("After");
});
use(...fns: MiddlewareFn<T>[]): this

Appends one or more middleware functions. Returns this for chaining.

execute(ctx: T): Promise<void>

Runs the full pipeline for the given context. If any middleware throws, the error is logged and re-thrown, stopping subsequent middleware.


Built-in Middleware Factories

These are factory functions that return MiddlewareFn<TelegramBot.Message>.


loggerMiddleware(log?: Logger)

Logs every incoming message with the user's display name, chat ID and text. Uses debug level.

client.use(loggerMiddleware(client.log));

blacklistMiddleware(bannedIds: number[], message?: string)

Silently drops messages from banned user IDs. The message parameter is reserved but currently the drop is silent.

client.use(blacklistMiddleware([111111, 222222]));

allowlistMiddleware(allowedChatIds: number[])

Only allows messages from the specified chat IDs. All others are silently dropped.

client.use(allowlistMiddleware([-1001234567890]));

maintenanceMiddleware(bot, enabled, message?)

When enabled() returns true, replies with a maintenance message and stops the pipeline.

let maintenance = false;
client.use(maintenanceMiddleware(client.bot, () => maintenance));

// To enable maintenance mode:
maintenance = true;

| Parameter | Type | Description | |---|---|---| | bot | TelegramBot | Bot instance used to send the reply. | | enabled | () => boolean | Function checked on every message. | | message | string | Custom maintenance message. Default: "🛠️ Bot is under maintenance..." |


typingMiddleware(bot)

Sends a "typing" chat action before passing control to the next middleware, giving users visual feedback.

client.use(typingMiddleware(client.bot));

globalRateLimitMiddleware(bot, maxPerWindow, windowMs)

Limits each user to maxPerWindow messages per windowMs milliseconds across all messages (not per-command).

// Max 30 messages per minute per user
client.use(globalRateLimitMiddleware(client.bot, 30, 60_000));

Session Management

src/session/SessionManager.ts


SessionManager

Manages per-user/chat session data. Backed by any SessionStore implementation.

const sessions = new SessionManager(new FileSessionStore("./data/sessions.json"));
Constructor
new SessionManager(store?: SessionStore, ttlMs?: number)

| Parameter | Default | Description | |---|---|---| | store | MemorySessionStore | Storage backend. | | ttlMs | 0 | Session time-to-live in milliseconds. 0 = never expires. |

getOrCreate(userId, chatId): Promise<SessionData>

Returns the existing session or creates a new empty one. Automatically deletes expired sessions when TTL is set.

get(userId, chatId): Promise<SessionData | undefined>

Returns the session if it exists, otherwise undefined. Does not create.

set(userId, chatId, data): Promise<void>

Merges data into the existing session (Object.assign semantics) and updates updatedAt.

await sessions.set(msg.from.id, msg.chat.id, { points: 42, level: "gold" });
delete(userId, chatId): Promise<void>

Deletes a session completely.

clearAll(): Promise<void>

Wipes all sessions from the store.

purgeExpired(): Promise<number>

Removes all sessions older than ttlMs. Returns the number of sessions removed. No-op when ttlMs is 0.


MemorySessionStore

Default in-memory store. Data is lost when the process restarts.

new MemorySessionStore()

Implements all SessionStore methods: get, set, delete, clear, all.


FileSessionStore

Persists sessions as a JSON file. Writes to disk in the background every flushEveryMs milliseconds.

new FileSessionStore(filePath?: string, flushEveryMs?: number)

| Parameter | Default | Description | |---|---|---| | filePath | "./sessions.json" | Path to the JSON file. | | flushEveryMs | 5000 | Flush interval in milliseconds. |

close(): Promise<void>

Clears the flush interval and performs an immediate final write. Call during graceful shutdown.


Conversations

src/conversation/ConversationManager.ts

Guided multi-step input flows (wizards/forms). While a conversation is active for a user, the main command dispatcher is bypassed for that user.


ConversationManager

isActive(userId, chatId): boolean

Returns true if a conversation is currently in progress for the given user+chat pair.

start(bot, userId, chatId, steps, options?): Promise<ConversationResult>

Starts a wizard. Returns a Promise that resolves when all steps are completed or the user cancels.

const result = await client.conversations.start(
  client.bot,
  msg.from.id,
  msg.chat.id,
  [
    {
      key: "name",
      prompt: "What is your name?",
      validate: (v) => v.length >= 2 || "Name too short.",
    },
    {
      key: "age",
      prompt: "How old are you?",
      transform: (v) => parseInt(v),
      validate: (v) => !isNaN(Number(v)) || "Must be a number.",
    },
  ],
  {
    cancelKeywords: ["/cancel", "stop"],
    onCancel: (data) => console.log("Cancelled with data:", data),
  }
);

if (result.completed) {
  console.log(result.data); // { name: "Alice", age: 30 }
}

| Parameter | Type | Description | |---|---|---| | bot | TelegramBot | Bot instance. | | userId | number | Telegram user ID. | | chatId | number | Chat ID. | | steps | ConversationStep[] | Ordered list of steps. | | options.cancelKeywords | string[] | Triggers that cancel the flow. Default: ["/cancel", "cancel", "annulla"] | | options.onCancel | (data) => void | Called with collected data when the flow is cancelled. |

abort(userId, chatId): void

Programmatically terminates a running conversation without sending any message.


ConversationStep interface

| Field | Type | Description | |---|---|---| | key | string | Key under which the value is stored in result.data. | | prompt | string \| (data) => string | Message sent to the user. Can be a function receiving previously collected data. | | validate | (input, data) => boolean \| string | Return true to accept, false for a generic error, or a string for a custom error message. | | transform | (input) => unknown | Transform the raw string before storing. E.g. parseInt. | | parseMode | ParseMode | Parse mode for the prompt message. Default: "HTML". |

ConversationResult interface

| Field | Type | Description | |---|---|---| | completed | boolean | true if all steps were answered. | | data | Record<string, unknown> | Collected values keyed by ConversationStep.key. | | cancelledAt | string \| undefined | The key of the step at which the user cancelled. |


Keyboard Builders

src/keyboard/KeyboardBuilder.ts


InlineKeyboardBuilder

Fluent builder for InlineKeyboardMarkup.

const kb = new InlineKeyboardBuilder()
  .button("✅ Accept", "action:accept")
  .button("❌ Decline", "action:decline")
  .row()
  .url("📖 Docs", "https://example.com")
  .build();
Instance methods

| Method | Returns | Description | |---|---|---| | button(text, callbackData) | this | Add a callback button to the current row. | | url(text, url) | this | Add a URL button. | | switchInline(text, query?) | this | Add a switch-inline button (opens inline query in any chat). | | switchInlineCurrent(text, query?) | this | Add a switch-inline-current-chat button. | | webApp(text, webAppUrl) | this | Add a WebApp button. | | row() | this | Finalise the current row and start a new one. | | build() | InlineKeyboardMarkup | Build and return the markup object. Automatically commits the last row. |

Static presets

| Method | Description | |---|---| | InlineKeyboardBuilder.yesNo(yesData?, noData?) | ✅ Yes / ❌ No confirmation keyboard. | | InlineKeyboardBuilder.pagination(page, totalPages, prefix?) | Navigation keyboard: ◀ Prev, n/N, Next ▶. | | InlineKeyboardBuilder.back(data?) | Single ← Back button. |


ReplyKeyboardBuilder

Fluent builder for ReplyKeyboardMarkup.

const kb = new ReplyKeyboardBuilder()
  .button("Option A").button("Option B").row()
  .button("Option C")
  .resize()
  .oneTime()
  .build();
Instance methods

| Method | Returns | Description | |---|---|---| | button(text) | this | Add a plain text button. | | requestContact(text) | this | Add a button that requests the user's contact. | | requestLocation(text) | this | Add a button that requests the user's location. | | webApp(text, url) | this | Add a WebApp button. | | row() | this | Start a new row. | | resize(v?) | this | Set resize_keyboard. Default true. | | oneTime(v?) | this | Set one_time_keyboard. Default true. | | selective(v?) | this | Set selective. Default true. | | inputPlaceholder(text) | this | Set input_field_placeholder. | | build() | ReplyKeyboardMarkup | Build and return the markup. |

Static methods

| Method | Description | |---|---| | ReplyKeyboardBuilder.remove() | Returns { remove_keyboard: true } to dismiss the keyboard. | | ReplyKeyboardBuilder.fromList(labels, columns?) | Quick-build from a flat array of strings. Default columns = 2. |


forceReply(placeholder?)

Returns a ForceReply markup object.

await bot.sendMessage(chatId, "What is your name?", {
  reply_markup: forceReply("Type your name…"),
});

Pagination

src/pagination/PaginationManager.ts


PaginationManager

Sends a message with a paginated list and handles navigation via inline buttons.

await client.pagination.send("products", {
  chatId: msg.chat.id,
  items: productList,
  pageSize: 8,
  title: "🛒 Products",
  renderItem: (item, i) => `${i + 1}. <b>${item.name}</b> — €${item.price}`,
  parseMode: "HTML",
});
Constructor
new PaginationManager(bot: TelegramBot)

Automatically listens for callback_query events matching its internal prefix pattern.

send<T>(tag, options): Promise<Message>

| Parameter | Type | Description | |---|---|---| | tag | string | Unique identifier for this pagination session within a chat. | | options.chatId | number | Chat to send the message to. | | options.items | T[] | Full array of items. | | options.pageSize | number | Items per page. Default: 10. | | options.renderItem | (item, index) => string | Converts an item to a display string. index is the global index. | | options.title | string | Optional bold header above the list. | | options.parseMode | ParseMode | Default: "HTML". | | options.messageId | number | If set, edits an existing message instead of sending a new one. |

destroy(chatId, tag): void

Removes the pagination session, freeing memory. Call when the paginated list is no longer needed.


Scheduler

src/scheduler/Scheduler.ts


Scheduler

Supports three job types: cron expressions (minute precision), fixed intervals, and one-shot Date-triggered jobs.

client.scheduler
  .cron("morning-msg", "0 9 * * *", async () => {
    await client.bot.sendMessage(CHANNEL_ID, "Good morning! ☀️");
  })
  .interval("heartbeat", 30_000, () => console.log("ping"))
  .once("launch-event", new Date("2025-12-31T23:59:00Z"), () => {
    console.log("🎉 Happy New Year!");
  });
cron(id, expression, task): this

Register a cron job. expression follows the "min hour dom mon dow" format. Supports *, /step, -range, ,list.

interval(id, ms, task): this

Register a job that fires every ms milliseconds.

once(id, runAt, task): this

Register a job that fires once at the given Date. The job is automatically removed after execution.

start(): void

Starts all registered jobs. For cron jobs, aligns to the next whole minute. For interval jobs, starts immediately.

stop(): void

Clears all timers. Jobs can be restarted by calling start() again.

trigger(id): Promise<void>

Manually run a job immediately by its ID, regardless of its schedule.

await client.scheduler.trigger("morning-msg");
remove(id): boolean

Remove and stop a job by ID. Returns true if found.

list(): ScheduledJob[]

Returns an array of all registered jobs, including their lastRun timestamps.


ScheduledJob interface

| Field | Type | Description | |---|---|---| | id | string | Unique job identifier. | | cron | string \| undefined | Cron expression. | | interval | number \| undefined | Interval in milliseconds. | | runAt | Date \| undefined | One-shot execution time. | | task | () => Promise<void> \| void | The function to execute. | | running | boolean | true while the task is executing (prevents overlap). | | lastRun | Date | Timestamp of the last execution. |


Internationalization (i18n)

src/i18n/I18n.ts


I18n

Per-user locale management with {{variable}} interpolation.

client.i18n
  .addLocale("en", {
    welcome: "Hello, {{name}}!",
    bye: "Goodbye!",
  })
  .addLocale("it", {
    welcome: "Ciao, {{name}}!",
    bye: "Arrivederci!",
  });

client.i18n.setUserLocale(msg.from.id, "it");
const text = client.i18n.t("welcome", msg.from.id, { name: "Mario" });
// → "Ciao, Mario!"
addLocale(locale, translations): this

Register or merge translations for a locale. Returns this for chaining.

addBundle(bundle): this

Register multiple locales at once from a { [locale]: TranslationMap } object.

setUserLocale(userId, locale): void

Override the locale for a specific user. Persists for the process lifetime (combine with sessions for permanent storage).

getUserLocale(userId): Locale

Returns the user's locale or defaultLocale if not set.

t(key, localeOrUserId, vars?): string

Translate key for a locale string or a user ID. Interpolates {{varName}} placeholders.

| Parameter | Description | |---|---| | key | Translation key. Falls back to defaultLocale, then to the key itself. | | localeOrUserId | A locale string ("it") or a Telegram user ID. | | vars | Optional Record<string, string \| number> for interpolation. |

for(localeOrUserId): (key, vars?) => string

Returns a bound translator function — useful inside a command to avoid repeating the locale.

const t = client.i18n.for(msg.from.id);
await ctx.replyHTML(t("welcome", { name: "Alice" }));
locales(): Locale[]

Returns an array of all registered locale strings.

hasLocale(locale): boolean

Returns true if the locale has been registered.


Rate Limiter

src/ratelimit/RateLimiter.ts


RateLimiter

Sliding window rate limiter, keyed by an arbitrary string and a user ID.

client.rateLimiter
  .addRule({ key: "search", maxRequests: 5, windowMs: 10_000 });

// In a command:
const result = client.rateLimiter.check("search", msg.from.id);
if (!result.allowed) {
  await ctx.reply(`⏳ Slow down! Try again in ${Math.ceil(result.retryAfterMs / 1000)}s.`);
  return true;
}
addRule(rule): this

Register a rule. Rules are matched by rule.key.

interface RateLimitRule {
  key: string;          // Arbitrary identifier, e.g. "search", "vote"
  maxRequests: number;  // Max allowed requests per window
  windowMs: number;     // Window size in milliseconds
}
check(key, userId): { allowed: true } | { allowed: false, retryAfterMs: number }

Consumes one request from the user's bucket. Returns the result synchronously.

reset(key, userId): void

Clears a specific user's bucket for a key (e.g. after a manual admin reset).

resetAll(key): void

Clears all user buckets for a key.

remaining(key, userId): number

Returns how many requests the user has left in the current window. Returns Infinity if no rule is registered for the key.


Utilities


MessageUtils

src/utils/MessageUtils.ts

All methods are static.


sendTemp(bot, chatId, text, delayMs, options?): Promise<Message>

Send a message and schedule its deletion after delayMs milliseconds.

await MessageUtils.sendTemp(bot, chatId, "✅ Done!", 5000);

sendLong(bot, chatId, text, options?, maxLength?): Promise<Message[]>

Split a long string into chunks of maxLength characters (default: 4096) and send them sequentially.


broadcast(bot, chatIds, text, options?, delayBetweenMs?): Promise<{chatId, success}[]>

Send a message to multiple chats. Errors are caught per-chat and returned in the result array. delayBetweenMs (default 50ms) prevents hitting Telegram rate limits.

const results = await MessageUtils.broadcast(bot, [111, 222, 333], "📢 Announcement!");

safeEdit(bot, chatId, messageId, text, options?): Promise<boolean>

Edit a message text. Returns true on success or if the message was already identical. Returns false on other errors.


deleteMany(bot, chatId, messageIds): Promise<void>

Delete multiple messages in parallel using Promise.allSettled. Ignores individual failures.


pin(bot, chatId, messageId, disableNotification?): Promise<boolean>

Pin a message. Returns true on success.


typeFor(bot, chatId, ms): Promise<void>

Send "typing" action and then wait ms milliseconds. Useful for simulating processing time.

await MessageUtils.typeFor(bot, chatId, 1500);
await bot.sendMessage(chatId, "Here is your result...");

escapeHTML(text): string

Escapes &, <, >, " for safe use in parse_mode: "HTML" messages.


escapeMD(text): string

Escapes all MarkdownV2 special characters.


formatBytes(bytes): string

Formats a byte count to a human-readable string: "1.4 KB", "3.2 MB", etc.


formatDuration(seconds): string

Formats a duration in seconds to "hh:mm:ss".


getMentionedUserIds(msg): number[]

Extracts user IDs from text_mention entities in a message.


isReply(msg): boolean

Returns true if the message is a reply to another message.


isFromBot(msg): boolean

Returns true if msg.from.is_bot is true.


displayName(user): string

Returns @username if available, otherwise the full name.


fullName(user): string

Concatenates first_name and last_name.


mentionHTML(user): string

Returns an HTML mention link: <a href="tg://user?id=123">First Last</a>.


CallbackRouter

src/utils/CallbackRouter.ts

Routes callback_query data to handler functions using path-style patterns.

client.callbackRouter
  .on("confirm", async (query) => {
    await client.bot.answerCallbackQuery(query.id, { text: "Confirmed!" });
  })
  .on("user/:id/ban", async (query, params) => {
    console.log("Banning user", params.id);
    await client.bot.answerCallbackQuery(query.id);
  })
  .on(/^delete:(\d+)$/, async (query, params) => {
    // Raw RegExp also supported; params will be empty for RegExp patterns
  })
  .otherwise(async (query) => {
    await client.bot.answerCallbackQuery(query.id, { text: "Unknown action" });
  });
Constructor
new CallbackRouter(bot: TelegramBot, log?: Logger)

Automatically attaches to bot.on("callback_query", ...).

on(pattern, handler, autoAnswer?): this

| Parameter | Type | Description | |---|---|---| | pattern | string \| RegExp | String patterns support :paramName segments. RegExp are used directly. | | handler | CallbackHandler | (query, params) => Promise<void> | | autoAnswer | { text?: string, alert?: boolean } | If provided, calls answerCallbackQuery automatically after the handler. |

otherwise(handler): this

Sets a fallback handler for unmatched callback data.


Logger

src/logger/Logger.ts


Logger

Coloured, levelled, child-scoped console logger.

const log = new Logger("MyModule", "debug");
log.debug("Detailed info");
log.info("Bot started");
log.warn("Config missing, using default");
log.error("Unhandled exception", err);
Constructor
new Logger(prefix?: string, level?: LogLevel)

| Parameter | Default | Description | |---|---|---| | prefix | "TgBot" | Shown as [PREFIX] in every log line. | | level | "info" | Minimum level to emit. |

setLevel(level): void

Change the log level at runtime.

debug / info / warn / error(msg, ...args): void

Emit a log at the respective level. Additional args are passed to console.log.

child(prefix): Logger

Create a child logger that prepends "Parent:Child" to the prefix. Used internally by every subsystem.

const log = new Logger("Bot");
const cmdLog = log.child("Commands"); // prefix: "Bot:Commands"
Log levels (in ascending priority order)

| Level | Numeric | Description | |---|---|---| | debug | 0 | Verbose development output. | | info | 1 | General operational messages. | | warn | 2 | Non-fatal issues. | | error | 3 | Errors and exceptions. | | none | 99 | Suppress all output. |

globalLogger

A pre-created Logger instance exported for convenience.


🗃️ Types Reference

All types are exported from src/types/types.ts.

// Chat scope restriction for commands
type ChatScope = "all" | "private" | "group" | "supergroup" | "channel";

// Log level
type LogLevel = "debug" | "info" | "warn" | "error" | "none";

// Middleware function signature
type MiddlewareFn<T> = (ctx: T, next: () => Promise<void> | void) => Promise<void> | void;

// Locale string
type Locale = string;

// Translation map { key: "translated string" }
type TranslationMap = Record<string, string>;

// Full bundle { locale: TranslationMap }
type TranslationBundle = Record<Locale, TranslationMap>;

// Session data object
interface SessionData {
  userId: number;
  chatId: number;
  data: Record<string, unknown>;
  createdAt: number;
  updatedAt: number;
}

// Webhook configuration
interface WebhookConfig {
  url: string;
  port?: number;
  secretToken?: string;
  tlsOptions?: { key: string; cert: string };
}

💡 Examples

Full minimal bot

import { BotClient, Command, CommandContext, CommandMeta, loggerMiddleware } from "./src";

class PingCommand extends Command {
  readonly meta: CommandMeta = { name: "ping", description: "Pong!" };
  async execute({ reply }: CommandContext) {
    await reply("🏓 Pong!");
    return true;
  }
}

const client = new BotClient({ token: process.env.BOT_TOKEN! });
client.use(loggerMiddleware());
await client.registerAndSync(new PingCommand());
await client.start();

Wizard registration form

bot.on("message", async (msg) => {
  if (msg.text !== "/register") return;

  const result = await client.conversations.start(
    client.bot, msg.from!.id, msg.chat.id,
    [
      { key: "name", prompt: "Enter your name:" },
      { key: "email", prompt: "Enter your email:", validate: v => v.includes("@") || "Invalid email" },
    ]
  );

  if (result.completed) {
    await client.bot.sendMessage(msg.chat.id,
      `✅ Welcome, ${result.data.name}! Confirmation sent to ${result.data.email}.`
    );
  }
});

Rate-limited command

client.rateLimiter.addRule({ key: "search", maxRequests: 3, windowMs: 10_000 });

class SearchCommand extends Command {
  readonly meta: CommandMeta = { name: "search", description: "Search something" };
  async execute({ msg, args, reply }: CommandContext) {
    const check = client.rateLimiter.check("search", msg.from!.id);
    if (!check.allowed) {
      await reply(`⏳ Try again in ${Math.ceil(check.retryAfterMs / 1000)}s`);
      return true;
    }
    await reply(`🔍 Results for: ${args.join(" ")}`);
    return true;
  }
}

Scheduled broadcast

client.scheduler.cron("daily-tip", "0 10 * * *", async () => {
  const chatIds = await db.getAllChatIds();
  await MessageUtils.broadcast(client.bot, chatIds, "💡 Tip of the day: Stay curious!");
});

🤝 Contributing

  1. Fork the repository.
  2. Create a feature branch: git checkout -b feat/my-feature.
  3. Commit your changes: git commit -m "feat: add my feature".
  4. Push and open a Pull Request.

Please follow the existing code style (no semicolons in types, 2-space indent, descriptive names).


📄 License

Apache-2.0 © 2026 — see LICENSE for details.