A powerful, flexible, and modern Telegram Bot SDK built with TypeScript. This SDK provides:
- JWT authentication (fully or partially enforced)
- Admin approval/authentication (for public or semi-public bots)
- Full session management (CRUD helpers for custom variables)
- Easy integration with Redis for session and approval state
- Modern, type-safe API and middleware support
- Professional Logging System (development and production-ready)
- ⚡ Robust Callback Query Handling:
- Automatic callback query timeout prevention
- Built-in timeout protection for long-running operations
- Graceful error handling that prevents bot crashes
- Support for image generation and other time-consuming tasks
- 🛡️ JWT Authentication: Secure your bot with JWT tokens. Enforce authentication on all routes (
fully
) or only on selected routes (partially
). - 👥 Admin Approval Layer: Add an extra layer of admin approval for new users. Great for public or semi-public bots, clubs, or organizations.
- 🗃️ Session CRUD Helpers: Easily manage custom session variables for each user, with built-in helpers for set/get/update/delete.
- 💾 Redis-backed Session & Approval: All session and approval state is stored in Redis for performance and reliability.
- 📝 Type-safe, Modern API: Built with TypeScript, with clear types and extensibility.
- 📝 Professional Logging System:
- Colorized, detailed logs in development
- Optimized, minimal logs in production
- Automatic context logging
- Multiple log levels (debug, info, warn, error)
- Timestamp and request tracking
- Built with Pino for performance
- Node.js (v14 or higher)
- Redis
- Telegram Bot Token (from @BotFather)
npm install @wasserstoff/mangi-tg-bot
import { Bot, AppConfig, CustomContext, logger } from '@wasserstoff/mangi-tg-bot';
const configWithJwtAuth: AppConfig = {
botToken: 'YOUR_BOT_TOKEN',
botMode: 'polling',
botAllowedUpdates: ['message', 'callback_query'],
redisUrl: 'YOUR_REDIS_URL',
isDev: true,
useAuth: 'fully', // All routes require JWT authentication
jwtSecret: 'your_jwt_secret_here',
};
async function createJwtAuthBot() {
logger.info('Starting bot with JWT authentication:', configWithJwtAuth);
const bot = new Bot(configWithJwtAuth);
await bot.initialize();
const botManager = bot.getBotManager();
botManager.handleCommand('start', async (ctx: CustomContext) => {
await ctx.api.sendMessage(
ctx.chat.id,
'Welcome! You are authenticated with JWT.'
);
});
botManager.handleCommand('whoami', async (ctx: CustomContext) => {
await ctx.api.sendMessage(
ctx.chat.id,
`Your chat ID: <code>${ctx.from.id}</code>`,
{ parse_mode: 'HTML' }
);
});
}
createJwtAuthBot().catch(console.error);
import { Bot, AppConfig, CustomContext, logger } from '@wasserstoff/mangi-tg-bot';
const configWithAdminAuth: AppConfig = {
botToken: 'YOUR_BOT_TOKEN',
botMode: 'polling',
botAllowedUpdates: ['message', 'callback_query'],
redisUrl: 'YOUR_REDIS_URL',
isDev: true,
useAuth: 'none',
adminAuthentication: true, // Enable admin approval system
adminChatIds: [123456789, 987654321], // Replace with your admin Telegram chat IDs
};
async function createAdminAuthBot() {
logger.info('Starting bot with admin authentication:', configWithAdminAuth);
const bot = new Bot(configWithAdminAuth);
await bot.initialize();
const botManager = bot.getBotManager();
botManager.handleCommand('start', async (ctx: CustomContext) => {
await ctx.api.sendMessage(
ctx.chat.id,
'Welcome! If you see this, you are approved by an admin.'
);
});
botManager.handleCommand('whoami', async (ctx: CustomContext) => {
await ctx.api.sendMessage(
ctx.chat.id,
`Your chat ID: <code>${ctx.from.id}</code>`,
{ parse_mode: 'HTML' }
);
});
botManager.handleCommand('secret', async (ctx: CustomContext) => {
await ctx.api.sendMessage(
ctx.chat.id,
'This is a secret command only for approved users!'
);
});
}
createAdminAuthBot().catch(console.error);
import { Bot, AppConfig, CustomContext, logger } from '@wasserstoff/mangi-tg-bot';
const configWithSessionCrud: AppConfig = {
botToken: 'YOUR_BOT_TOKEN',
botMode: 'polling',
botAllowedUpdates: ['message', 'callback_query'],
redisUrl: 'YOUR_REDIS_URL',
isDev: true,
useAuth: 'none',
};
async function createSessionCrudBot() {
logger.info('Starting bot with session CRUD example:', configWithSessionCrud);
const bot = new Bot(configWithSessionCrud);
await bot.initialize();
const botManager = bot.getBotManager();
botManager.handleCommand('setvar', async (ctx: CustomContext) => {
ctx.session.setCustom('foo', 'bar');
const foo = ctx.session.getCustom('foo');
ctx.session.updateCustom({ hello: 'world', count: 1 });
ctx.session.deleteCustom('count');
await ctx.api.sendMessage(
ctx.chat.id,
`Session custom variable 'foo' set to '${foo}'. Updated and deleted 'count'.`
);
});
botManager.handleCommand('getvar', async (ctx: CustomContext) => {
const foo = ctx.session.getCustom('foo');
await ctx.api.sendMessage(ctx.chat.id, `Current value of 'foo': ${foo}`);
});
}
createSessionCrudBot().catch(console.error);
import { Bot, AppConfig, CustomContext, logger } from '@wasserstoff/mangi-tg-bot';
const configCombined: AppConfig = {
botToken: 'YOUR_BOT_TOKEN',
botMode: 'polling',
botAllowedUpdates: ['message', 'callback_query'],
redisUrl: 'YOUR_REDIS_URL',
isDev: true,
useAuth: 'fully', // JWT auth required for all routes
jwtSecret: 'your_jwt_secret_here',
adminAuthentication: true, // Enable admin approval system
adminChatIds: [123456789], // Replace with your admin Telegram chat IDs
};
async function createCombinedBot() {
logger.info(
'Starting combined bot with JWT, admin auth, and session CRUD:',
configCombined
);
const bot = new Bot(configCombined);
await bot.initialize();
const botManager = bot.getBotManager();
// Set up command menu
botManager.setMyCommands([
{ command: 'start', description: 'Start the bot' },
{ command: 'whoami', description: 'Get your chat ID' },
{ command: 'setvar', description: 'Set session variables' },
]);
// Only accessible if JWT is valid AND user is approved by admin
botManager.handleCommand('start', async (ctx: CustomContext) => {
await ctx.api.sendMessage(
ctx.chat.id,
'Welcome! You are authenticated and approved by an admin.'
);
});
// Session CRUD helpers
botManager.handleCommand('setvar', async (ctx: CustomContext) => {
ctx.session.setCustom('foo', 'bar');
const foo = ctx.session.getCustom('foo');
ctx.session.updateCustom({ hello: 'world', count: 1 });
ctx.session.deleteCustom('count');
await ctx.api.sendMessage(
ctx.chat.id,
`Session custom variable 'foo' set to '${foo}'. Updated and deleted 'count'.`
);
});
botManager.handleCommand('getvar', async (ctx: CustomContext) => {
const foo = ctx.session.getCustom('foo');
await ctx.api.sendMessage(ctx.chat.id, `Current value of 'foo': ${foo}`);
});
// Show user their chat ID (useful for admin setup)
botManager.handleCommand('whoami', async (ctx: CustomContext) => {
await ctx.api.sendMessage(
ctx.chat.id,
`Your chat ID: <code>${ctx.from.id}</code>`,
{ parse_mode: 'HTML' }
);
});
// Example: Only approved users with valid JWT can access this command
botManager.handleCommand('secret', async (ctx: CustomContext) => {
await ctx.api.sendMessage(
ctx.chat.id,
'This is a secret command only for authenticated and approved users!'
);
});
}
createCombinedBot().catch(console.error);
The SDK now includes robust handling for callback queries and long-running operations. Here are the key improvements:
Callback queries are automatically answered immediately when received, preventing Telegram's 30-second timeout:
botManager.handleCallback((ctx) => ctx.callbackQuery.data === "generate_image", async (ctx: CustomContext) => {
try {
// Send a "processing" message first
const processingMsg = await ctx.api.sendMessage(
ctx.chat.id,
"🔄 Generating your image... Please wait."
);
// Your long-running operation (e.g., image generation API call)
const imageUrl = await generateImageFromAPI(prompt);
// Send the result
await ctx.api.sendPhoto(ctx.chat.id, imageUrl, {
caption: "Here's your generated image! 🎨"
});
// Clean up processing message
await ctx.api.deleteMessage(ctx.chat.id, processingMsg.message_id);
} catch (error) {
await ctx.api.sendMessage(
ctx.chat.id,
"❌ Sorry, there was an error. Please try again."
);
}
});
All handlers have built-in timeout protection:
- Callback queries: 25 seconds
- Commands: 30 seconds
- Messages: 30 seconds
The SDK includes comprehensive error handling that prevents bot crashes:
// Errors are automatically caught and logged
// Users receive appropriate error messages
// The bot continues running even if individual operations fail
- Always answer callback queries immediately (handled automatically by the SDK)
- Send a processing message to keep users informed
- Use try-catch blocks for error handling
- Clean up temporary messages after completion
- Provide retry options when operations fail
botManager.handleMessage((ctx) => ctx.message.text === "generate", async (ctx: CustomContext) => {
await ctx.api.sendMessage(
ctx.chat.id,
"🎨 Image Generation Demo\n\nClick the button below to generate an image:",
{
reply_markup: {
inline_keyboard: [[{ text: "🖼️ Generate Image", callback_data: "generate_image" }]]
}
}
);
});
botManager.handleCallback((ctx) => ctx.callbackQuery.data === "generate_image", async (ctx: CustomContext) => {
try {
// Send processing message
const processingMsg = await ctx.api.sendMessage(
ctx.chat.id,
"🔄 Generating your image... Please wait."
);
// Simulate long-running operation
await new Promise(resolve => setTimeout(resolve, 5000));
// Send result
await ctx.api.sendPhoto(
ctx.chat.id,
"https://via.placeholder.com/400x300/FF0000/FFFFFF?text=Generated+Image",
{
caption: "Here's your generated image! 🎨",
reply_markup: {
inline_keyboard: [[
{ text: "Generate Another", callback_data: "generate_image" },
{ text: "Done", callback_data: "done" }
]]
}
}
);
// Clean up
await ctx.api.deleteMessage(ctx.chat.id, processingMsg.message_id);
} catch (error) {
await ctx.api.sendMessage(
ctx.chat.id,
"❌ Sorry, there was an error generating your image. Please try again.",
{
reply_markup: {
inline_keyboard: [[{ text: "Try Again", callback_data: "generate_image" }]]
}
}
);
}
});
The SDK provides easy CRUD helpers for managing session variables in ctx.session.custom
.
-
ctx.session.setCustom(key, value)
— Set a variable insession.custom
-
ctx.session.getCustom(key)
— Get a variable fromsession.custom
-
ctx.session.updateCustom({ ... })
— Update multiple variables insession.custom
-
ctx.session.deleteCustom(key)
— Delete a variable fromsession.custom
-
ctx.session.save(callback)
— Persist the session to Redis immediately (optional, usually auto-saved)
botManager.handleCommand('setvar', async (ctx: CustomContext) => {
// Set a simple variable
ctx.session.setCustom('foo', 'bar');
// Set a nested variable
ctx.session.setCustom('profile.name', 'Alice');
// Get a variable
const foo = ctx.session.getCustom('foo');
const name = ctx.session.getCustom('profile.name');
// Update multiple variables (including nested)
ctx.session.updateCustom({ 'hello': 'world', 'profile.age': 30 });
// Delete a variable
ctx.session.deleteCustom('profile.name');
// Save session if available (optional)
if (typeof ctx.session.save === 'function') {
ctx.session.save(() => {});
}
await ctx.reply(`Session custom variable 'foo' set to '${foo}', name: '${name}'. Updated and deleted 'profile.name'.`);
});
The SDK provides a convenient way to set up and manage your bot's command menu using the setMyCommands
method. This allows you to define a list of commands that will appear in the bot's menu interface.
botManager.setMyCommands([
{ command: 'start', description: 'Start the bot' },
{ command: 'help', description: 'Show help information' },
{ command: 'settings', description: 'Configure bot settings' }
]);
The command menu will be displayed to users when they open the bot's chat interface, making it easier for them to discover and use available commands.
The SDK includes a professional logging system built with Pino that automatically adapts to your environment:
- In development mode (
isDev: true
), you get detailed, colorized logs - In production mode (
isDev: false
), logs are minimized to essential information
- 🎨 Colorized Output: Development logs are colorized for better readability
- ⏰ Timestamp Information: Each log includes precise timestamp
- 🔍 Debug Mode: Extensive debugging information in development
- 🎯 Production Ready: Optimized, minimal logging in production
- 📊 Log Levels: Supports multiple log levels (debug, info, warn, error)
import { createSdkLogger } from '@wasserstoff/mangi-tg-bot';
// Create a logger instance
const logger = createSdkLogger(config.isDev);
// Usage examples
logger.info('Bot initialized successfully');
logger.debug('Processing update:', update);
logger.warn('Rate limit approaching');
logger.error('Connection failed:', error);
The SDK automatically includes logging in the bot context:
botManager.handleCommand('example', async (ctx: CustomContext) => {
// Logs are automatically controlled by isDev setting
ctx.logger.info('Processing example command');
ctx.logger.debug('Session state:', ctx.session);
await ctx.reply('Command processed!');
});
-
Development Mode (
isDev: true
):- Detailed debug information
- Session state logging
- Command processing details
- Redis operations logging
- Colorized, formatted output
-
Production Mode (
isDev: false
):- Critical errors only
- Important state changes
- Minimal operational logs
- Optimized for performance
To switch between modes, simply set isDev
in your configuration:
const config: AppConfig = {
// ... other config options ...
isDev: process.env.NODE_ENV !== 'production'
};
Add an extra layer of admin approval for new users. This is ideal for public or semi-public bots, clubs, or organizations where you want to control who can use the bot.
-
New users are set to
pending
in Redis and cannot use the bot until approved. - Admins receive approval requests and can approve/deny users via inline buttons.
-
Only approved users (status
member
oradmin
) can interact with the bot.
- When a new user interacts with the bot, their status is set to
pending
in Redis. - All admins (specified in
adminChatIds
) receive a message with Approve/Deny buttons. - When an admin approves, the user's status is set to
member
and they are notified. - Only users with status
member
oradmin
can use the bot; others are blocked until approved.
const configWithAdminAuth: AppConfig = {
botToken: 'YOUR_BOT_TOKEN',
botMode: 'polling',
botAllowedUpdates: ['message', 'callback_query'],
redisUrl: 'YOUR_REDIS_URL',
isDev: true,
useAuth: 'none',
adminAuthentication: true,
adminChatIds: [123456789, 987654321], // Replace with your admin Telegram chat IDs
};
const bot = new Bot(configWithAdminAuth);
const botManager = bot.getBotManager();
botManager.handleCommand('start', async (ctx: CustomContext) => {
await ctx.reply('Welcome! If you see this, you are approved by an admin.');
});
botManager.handleCommand('whoami', async (ctx: CustomContext) => {
await ctx.reply(`Your chat ID: <code>${ctx.from?.id}</code>`, { parse_mode: 'HTML' });
});
botManager.handleCommand('secret', async (ctx: CustomContext) => {
await ctx.reply('This is a secret command only for approved users!');
});
The SDK now ensures that ctx.session
, ctx.chat
, and ctx.from
are always present in your handlers. You can safely use ctx.session.whatever
, ctx.chat.id
, etc., without needing to write ctx.session!
or add type guards.
This is handled automatically by the SDK's internal middleware and does not require any code changes for existing users.
Example:
botManager.handleCommand('start', async (ctx: CustomContext) => {
// No need for ctx.session! or ctx.chat!
ctx.session.setCustom('foo', 'bar');
await ctx.api.sendMessage(ctx.chat.id, 'Welcome!');
});
ISC
This project is available on GitHub: https://github.com/AmanUpadhyay1609/-wasserstoff-mangi-tg-bot
Issues, feature requests, and contributions are welcome!
Never use:
const botInstance = botManager.getBot();
botInstance.on("chat_member", (ctx) => { /* ... */ }); // ❌ This will cause errors!
- The grammY framework (and this SDK) throw a runtime error if you try to add event listeners after the bot has started, or from within other listeners.
- This can cause a memory leak and eventually crash your bot. The error message will look like:
Error: It looks like you are registering more listeners on your bot from within other listeners! ...
Always use:
botManager.handleEvent("chat_member", async (ctx) => {
// Your logic here
});
- This ensures all listeners are registered before the bot starts, using the SDK's internal Composer.
- Works for any event type supported by grammY (e.g.,
chat_member
,my_chat_member
, etc.).
botManager.handleEvent("chat_member", async (ctx) => {
console.log("A user joined or left the group:", ctx);
});
Summary:
- ❌ Do NOT use
botInstance.on(...)
directly. - ✅ Use
botManager.handleEvent(...)
for all event listeners, including group events. - See
src/example.ts
for a working example.
The handleEvent
method uses grammY's built-in FilterQuery
type for the event name. This means:
-
You get autocompletion and type safety in your editor for all valid event names (like
"message"
,"chat_member"
,"message:text"
, etc.). - You can't accidentally use an invalid event name—TypeScript will warn you.
Example:
botManager.handleEvent("chat_member", async (ctx) => { /* ... */ }); // ✅ autocompleted, type-checked
Tip: Start typing inside the quotes and your editor (VS Code, WebStorm, etc.) will suggest all valid grammY event names. You can also use arrays for multiple events.
-
FilterQuery
is a type from grammY that represents all valid event filter strings. - See grammY filter queries documentation for more info and examples.