@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.
📋 Table of Contents
- Features
- Installation
- Quick Start
- Project Structure
- Configuration
- Wiki
- Types Reference
- Examples
- Contributing
- License
✨ 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:
- Instantiates
TelegramBotin polling or webhook mode. - Creates all subsystem instances.
- 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>
- Runs
guardScope— checksmeta.scopeagainst the chat type. - Runs
guardPermissions— checks owner, user whitelist, member status. - Runs
guardCooldown— enforces per-user cooldown. - Builds
CommandContext. - Calls
cmd.onInvoke(ctx). - Calls
cmd.execute(ctx). - If
executereturnsfalse, sends the usage hint (and schedules deletion ifdeleteAfteris set). - 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
- Fork the repository.
- Create a feature branch:
git checkout -b feat/my-feature. - Commit your changes:
git commit -m "feat: add my feature". - 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.
