
Research
/Security News
Shai Hulud Strikes Again (v2)
Another wave of Shai-Hulud campaign has hit npm with more than 500 packages and 700+ versions affected.
@rizzclub/channels
Advanced tools
Multi-channel messaging library with grammy-compatible API for Telegram, Webchat, WhatsApp, and SMS
Multi-channel messaging library with a grammy-compatible API for Telegram, Webchat, WhatsApp, and SMS.
IMPORTANT: This library uses camelCase for all property names and object keys:
callbackQuery, messageId, firstName, inlineKeyboardcallback_query, message_id, first_name, inline_keyboardThis applies to:
Update, Message, CallbackQuery, etc.)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)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.
npm install @rizzclub/channels
โ ๏ธ 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
});
โ ๏ธ 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);
}
};
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
})
});
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:
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:
createBotHandler initializes the bot and registers all handlersOptions:
getToken: (env) => string - Extract bot token from env (for TelegramAdapter)createAdapter: (env) => ChannelAdapter - Create custom adapter (for other adapters)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.
The Bot class is the main entry point, providing a grammy-compatible API.
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'message' - Any message'message:text' - Text messages only'callback_query' - Inline button clicks'edited_message' - Message editsThe Context object provides convenient access to update data and reply methods.
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 typectx.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 commandWraps 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 usageCustom 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 messagesWebhook 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
}
}
Placeholder for WhatsApp Business API integration.
Placeholder for SMS integration via Twilio/Vonage.
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);
}
};
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 });
}
};
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 โ
');
});
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
}
});
});
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();
});
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());
}
});
@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 | โ | โ |
MIT
Contributions welcome! Please open an issue or PR.
FAQs
Multi-channel messaging library with grammy-compatible API for Telegram, Webchat, WhatsApp, and SMS
The npm package @rizzclub/channels receives a total of 945 weekly downloads. As such, @rizzclub/channels popularity was classified as not popular.
We found that @rizzclub/channels demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.ย It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Another wave of Shai-Hulud campaign has hit npm with more than 500 packages and 700+ versions affected.

Product
Add real-time Socket webhook events to your workflows to automatically receive software supply chain alert changes in real time.

Security News
ENISA has become a CVE Program Root, giving the EU a central authority for coordinating vulnerability reporting, disclosure, and cross-border response.