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

@rizzclub/channels

v0.0.35

Published

Multi-channel messaging library with grammy-compatible API for Telegram, Webchat, WhatsApp, and SMS

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/channels

Quick 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
});

⚠️ 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:

  1. On the first request, createBotHandler initializes the bot and registers all handlers
  2. On subsequent requests, the same bot instance is reused (handlers already registered)
  3. 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/regex
  • bot.callbackQuery(data, handler) - Handle inline button callbacks
  • bot.use(middleware) - Add middleware
  • bot.filter(filter, handler) - Handle messages matching custom filter
  • bot.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 object
  • ctx.chat - The chat object
  • ctx.from - The user object
  • ctx.text - Message text
  • ctx.callbackQuery - Callback query object
  • ctx.callbackData - Callback query data
  • ctx.channel - Current channel type

Methods

  • ctx.reply(text, options?) - Reply to the message
  • ctx.send(text, options?) - Send without replying
  • ctx.editMessageText(text, options?) - Edit the message
  • ctx.deleteMessage() - Delete the message
  • ctx.answerCallbackQuery(options?) - Answer callback query
  • ctx.answerAlert(text) - Answer with alert popup
  • ctx.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 @BotFather
  • bot? - 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 requests
  • callbackUrl? - URL to POST messages to
  • callbackHeaders? - Headers for callback requests
  • messageStore? - 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 | ❌ | ✅ | | WhatsApp | ❌ | 🚧 Coming soon | | SMS | ❌ | 🚧 Coming soon | | API compatibility | - | 100% | | TypeScript | ✅ | ✅ | | Cloudflare Workers | ✅ | ✅ |

License

MIT

Contributing

Contributions welcome! Please open an issue or PR.

Links