Package Exports
- @rizzclub/channels
- @rizzclub/channels/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@rizzclub/channels) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@rizzclub/channels
Multi-channel messaging library with a grammy-compatible API for Telegram, Webchat, WhatsApp, and SMS.
Features
- 🎯 Grammy-compatible API - Drop-in replacement for grammy with the same interface
- 🔌 Multiple Channels - Telegram, Webchat, WhatsApp (coming soon), SMS (coming soon)
- 🚀 Cloudflare Workers - Optimized for serverless deployment
- 📦 TypeScript - Full type safety and autocompletion
- 🎨 Unified Interface - Write once, deploy to multiple channels
Code Style: camelCase Standard
IMPORTANT: This library uses camelCase for all property names and object keys:
- ✅
callbackQuery,messageId,firstName,inlineKeyboard - ❌
callback_query,message_id,first_name,inline_keyboard
This applies to:
- All TypeScript interfaces (
Update,Message,CallbackQuery, etc.) - All internal data structures
- Bot API methods and context properties
Exception - Grammy Filter Strings: Event filter strings remain snake_case for Grammy compatibility:
- ✅
bot.on('callback_query:data', handler)- Filter string (snake_case) - ✅
ctx.callbackQuery.data- Data access (camelCase) - This maintains Grammy API compatibility while using TypeScript conventions internally
Adapters handle conversion: Each adapter (TelegramAdapter, WhatsAppAdapter, etc.) converts from the external API's format (usually snake_case) to our camelCase standard at the boundary.
Rationale: This library is platform-agnostic and follows TypeScript/JavaScript naming conventions, not Telegram-specific conventions. Filter strings are kept as Grammy DSL for API compatibility.
Installation
npm install @rizzclub/channelsQuick Start
Telegram Bot (Recommended Pattern)
⚠️ IMPORTANT: Always use createBotHandler to prevent handler accumulation. This ensures handlers are registered only once, not on every request.
import { createBotHandler } from '@rizzclub/channels';
interface Env {
BOT_TOKEN: string;
}
export default createBotHandler<Env>((bot, env) => {
// Handlers are registered ONCE on first request
bot.command('start', (ctx) => {
return ctx.reply('Hello! I am your bot.');
});
bot.on('message:text', (ctx) => {
return ctx.reply(`You said: ${ctx.text}`);
});
}, {
getToken: (env) => env.BOT_TOKEN
});Alternative: Manual Bot Creation (Not Recommended)
⚠️ Warning: This pattern re-registers all handlers on every request, causing handler accumulation and performance issues.
import { Bot, TelegramAdapter } from '@rizzclub/channels';
const adapter = new TelegramAdapter({
token: process.env.TELEGRAM_TOKEN
});
const bot = new Bot(adapter);
bot.command('start', (ctx) => {
return ctx.reply('Hello! I am your bot.');
});
bot.on('message:text', (ctx) => {
return ctx.reply(`You said: ${ctx.text}`);
});
// Cloudflare Worker
export default {
async fetch(request: Request, env: Env) {
return bot.handleWebhook(request);
}
};Webchat Bot
import { createBotHandler, WebchatAdapter } from '@rizzclub/channels';
interface Env {
WEBHOOK_SECRET: string;
CALLBACK_URL: string;
}
export default createBotHandler<Env>((bot, env) => {
bot.on('message:text', async (ctx) => {
await ctx.reply(`Echo: ${ctx.text}`);
});
bot.callbackQuery(/^button:/, async (ctx) => {
await ctx.answerAlert('Button clicked!');
});
}, {
createAdapter: (env) => new WebchatAdapter({
webhookSecret: env.WEBHOOK_SECRET,
callbackUrl: env.CALLBACK_URL
})
});Cloudflare Workers Best Practices
Using createBotHandler (Recommended)
Why it's important: In Cloudflare Workers, the fetch() function is called on every request. If you create a new Bot instance and register handlers inside fetch(), you'll re-register all handlers on every request, causing:
- 📈 Handler accumulation - Middleware array grows unbounded (300 handlers × N requests)
- 🐌 Performance degradation - Each request gets slower as middleware grows
- 💸 Increased costs - More CPU time per request
Solution: Use createBotHandler to register handlers exactly once:
import { createBotHandler } from '@rizzclub/channels';
interface Env {
BOT_TOKEN: string;
}
// ✅ CORRECT - Handlers registered ONCE on first request
export default createBotHandler<Env>((bot, env) => {
bot.command('start', (ctx) => ctx.reply('Hello!'));
bot.on('message:text', (ctx) => ctx.reply(`Echo: ${ctx.text}`));
// ... all your handlers
}, {
getToken: (env) => env.BOT_TOKEN
});Compare with the problematic pattern:
// ❌ WRONG - Handlers re-registered on EVERY request
export default {
async fetch(request: Request, env: Env) {
const bot = new Bot(new TelegramAdapter({ token: env.BOT_TOKEN }));
// These handlers are added to middleware array on EVERY request
bot.command('start', (ctx) => ctx.reply('Hello!'));
bot.on('message:text', (ctx) => ctx.reply(`Echo: ${ctx.text}`));
return bot.handleWebhook(request);
}
}How it works:
- On the first request,
createBotHandlerinitializes the bot and registers all handlers - On subsequent requests, the same bot instance is reused (handlers already registered)
- Bot instance persists across requests in the same Worker instance
Options:
getToken: (env) => string- Extract bot token from env (for TelegramAdapter)createAdapter: (env) => ChannelAdapter- Create custom adapter (for other adapters)
API Reference
createBotHandler
function createBotHandler<Env = any>(
registerHandlers: (bot: Bot, env: Env) => void | Promise<void>,
options: {
getToken?: (env: Env) => string;
createAdapter?: (env: Env) => ChannelAdapter;
}
): { fetch: (request: Request, env: Env, ctx: ExecutionContext) => Promise<Response> }Creates a Cloudflare Workers handler with lazy bot initialization. See Cloudflare Workers Best Practices for details.
Bot
The Bot class is the main entry point, providing a grammy-compatible API.
Methods
bot.command(command, handler)- Handle commands (e.g.,/start)bot.on(event, handler)- Handle events (e.g.,'message:text')bot.hears(trigger, handler)- Handle messages matching text/regexbot.callbackQuery(data, handler)- Handle inline button callbacksbot.use(middleware)- Add middlewarebot.filter(filter, handler)- Handle messages matching custom filterbot.handleWebhook(request)- Process incoming webhook requests
Events
'message'- Any message'message:text'- Text messages only'callback_query'- Inline button clicks'edited_message'- Message edits
Context
The Context object provides convenient access to update data and reply methods.
Properties
ctx.message- The message objectctx.chat- The chat objectctx.from- The user objectctx.text- Message textctx.callbackQuery- Callback query objectctx.callbackData- Callback query datactx.channel- Current channel type
Methods
ctx.reply(text, options?)- Reply to the messagectx.send(text, options?)- Send without replyingctx.editMessageText(text, options?)- Edit the messagectx.deleteMessage()- Delete the messagectx.answerCallbackQuery(options?)- Answer callback queryctx.answerAlert(text)- Answer with alert popupctx.hasCommand(command?)- Check if message is a command
Adapters
TelegramAdapter
Wraps grammy for Telegram integration.
import { TelegramAdapter } from '@rizzclub/channels';
const adapter = new TelegramAdapter({
token: 'YOUR_BOT_TOKEN'
});Options:
token- Telegram bot token from @BotFatherbot?- Optional grammy Bot instance for advanced usage
WebchatAdapter
Custom adapter for web-based chat interfaces.
import { WebchatAdapter, InMemoryMessageStore } from '@rizzclub/channels';
const adapter = new WebchatAdapter({
webhookSecret: 'your-secret',
callbackUrl: 'https://your-app.com/api/send',
callbackHeaders: {
'Authorization': 'Bearer token'
},
messageStore: new InMemoryMessageStore()
});Options:
webhookSecret?- Secret for validating webhook requestscallbackUrl?- URL to POST messages tocallbackHeaders?- Headers for callback requestsmessageStore?- Store for tracking messages
Webhook Payload Format:
{
type: 'message' | 'callback_query',
sessionId: string,
userId: string,
userName?: string,
timestamp: number,
message?: {
id: string,
text?: string
},
callbackQuery?: {
id: string,
data: string,
messageId: string
}
}WhatsAppAdapter (Coming Soon)
Placeholder for WhatsApp Business API integration.
SMSAdapter (Coming Soon)
Placeholder for SMS integration via Twilio/Vonage.
Multi-Channel Support
Option 1: Using Router (Recommended)
Handle multiple channels with shared bot logic using the built-in Router:
import {
createRouter,
TelegramAdapter,
WebchatAdapter
} from '@rizzclub/channels';
// Shared bot setup
function setupBot(bot) {
bot.command('start', (ctx) => {
return ctx.reply(`Welcome to ${ctx.channel}!`);
});
bot.on('message:text', (ctx) => {
return ctx.reply(`[${ctx.channel}] You said: ${ctx.text}`);
});
}
// Cloudflare Worker
export default {
async fetch(request: Request, env: Env) {
const router = createRouter()
.route('/telegram/webhook', new TelegramAdapter({ token: env.TELEGRAM_TOKEN }), setupBot)
.route('/webchat/webhook', new WebchatAdapter({ webhookSecret: env.WEBHOOK_SECRET }), setupBot);
return router.handleRequest(request);
}
};Option 2: Manual Routing
import {
Bot,
TelegramAdapter,
WebchatAdapter,
type ChannelAdapter
} from '@rizzclub/channels';
// Shared bot logic
function createBot(adapter: ChannelAdapter) {
const bot = new Bot(adapter);
bot.command('start', (ctx) => {
return ctx.reply(`Welcome to ${ctx.channel}!`);
});
bot.on('message:text', (ctx) => {
return ctx.reply(`[${ctx.channel}] You said: ${ctx.text}`);
});
return bot;
}
// Cloudflare Worker
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
// Telegram webhook
if (url.pathname === '/telegram') {
const bot = createBot(
new TelegramAdapter({ token: env.TELEGRAM_TOKEN })
);
return bot.handleWebhook(request);
}
// Webchat webhook
if (url.pathname === '/webchat') {
const bot = createBot(
new WebchatAdapter({ webhookSecret: env.WEBHOOK_SECRET })
);
return bot.handleWebhook(request);
}
return new Response('Not Found', { status: 404 });
}
};Inline Keyboards
Create interactive inline keyboards:
bot.command('menu', (ctx) => {
return ctx.reply('Choose an option:', {
replyMarkup: {
inlineKeyboard: [
[
{ text: '✅ Option 1', callbackData: 'opt1' },
{ text: '❌ Option 2', callbackData: 'opt2' }
],
[
{ text: '🔗 Visit Website', url: 'https://rizz.club' }
]
]
}
});
});
bot.callbackQuery('opt1', async (ctx) => {
await ctx.answerAlert('You chose Option 1!');
await ctx.editMessageText('Option 1 selected ✅');
});Reply Keyboards
Create reply keyboards (Telegram):
bot.command('keyboard', (ctx) => {
return ctx.reply('Choose a category:', {
replyMarkup: {
keyboard: [
[{ text: '📱 Tech' }, { text: '🎮 Gaming' }],
[{ text: '🎨 Art' }, { text: '🎵 Music' }]
],
resizeKeyboard: true,
oneTimeKeyboard: true
}
});
});Middleware
Add custom middleware for logging, authentication, etc:
// Logging middleware
bot.use(async (ctx, next) => {
console.log(`Incoming from ${ctx.channel}: ${ctx.text}`);
await next();
});
// Auth middleware
bot.use(async (ctx, next) => {
const userId = ctx.from?.id;
if (!userId) return;
const isAuthorized = await checkAuth(userId);
if (!isAuthorized) {
return ctx.reply('Unauthorized');
}
await next();
});TypeScript
Full TypeScript support with type inference:
import { Bot, Context, TelegramAdapter } from '@rizzclub/channels';
const adapter = new TelegramAdapter({ token: 'token' });
const bot = new Bot(adapter);
bot.on('message:text', (ctx: Context) => {
// ctx.text is automatically typed as string | undefined
if (ctx.text) {
console.log(ctx.text.toUpperCase());
}
});Comparison with Grammy
@rizzclub/channels provides the same API as grammy while supporting multiple channels:
| Feature | Grammy | @rizzclub/channels |
|---|---|---|
| Telegram | ✅ | ✅ |
| Webchat | ❌ | ✅ |
| ❌ | 🚧 Coming soon | |
| SMS | ❌ | 🚧 Coming soon |
| API compatibility | - | 100% |
| TypeScript | ✅ | ✅ |
| Cloudflare Workers | ✅ | ✅ |
License
MIT
Contributing
Contributions welcome! Please open an issue or PR.