npm package: @rizzclub/channels
Description: Multi-channel messaging library with grammy-compatible API for Telegram, Webchat, WhatsApp, and SMS
Author: RizzClub
Latest version: 0.0.35

# @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

```bash
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.

```typescript
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
});
```

<details>
<summary>Alternative: Manual Bot Creation (Not Recommended)</summary>

⚠️ **Warning**: This pattern re-registers all handlers on every request, causing handler accumulation and performance issues.

```typescript
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);
  }
};
```
</details>

### Webchat Bot

```typescript
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:

```typescript
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:

```typescript
// ❌ 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

```typescript
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](#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](https://grammy.dev) for Telegram integration.

```typescript
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.

```typescript
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:**
```typescript
{
  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:

```typescript
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

```typescript
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:

```typescript
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):

```typescript
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:

```typescript
// 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:

```typescript
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

- [RizzClub Platform](https://rizz.club)
- [Documentation](https://github.com/rizzclub/platform)
- [Examples](https://github.com/rizzclub/platform/tree/main/examples)