# @wasserstoff/mangi-tg-bot SDK

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

## 🚀 Features

- 🛡️ **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

## 📋 Prerequisites

- Node.js (v14 or higher)
- Redis
- Telegram Bot Token (from [@BotFather](https://t.me/BotFather))

## 🛠️ Installation

```bash
npm install @wasserstoff/mangi-tg-bot
```

## 📖 Usage Examples

### 1. Basic Bot with JWT Authentication

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

### 2. Bot with Admin Authentication/Approval

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

### 3. Bot with Session CRUD Operations

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

### 4. Combined Example: JWT Auth + Admin Auth + Session CRUD

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

## 🔧 Callback Query Handling & Long-Running Operations

The SDK now includes robust handling for callback queries and long-running operations. Here are the key improvements:

### ✅ Automatic Timeout Prevention

Callback queries are automatically answered immediately when received, preventing Telegram's 30-second timeout:

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

### ⏱️ Built-in Timeout Protection

All handlers have built-in timeout protection:
- **Callback queries**: 25 seconds
- **Commands**: 30 seconds  
- **Messages**: 30 seconds

### 🛡️ Error Handling

The SDK includes comprehensive error handling that prevents bot crashes:

```typescript
// Errors are automatically caught and logged
// Users receive appropriate error messages
// The bot continues running even if individual operations fail
```

### 📋 Best Practices for Long-Running Operations

1. **Always answer callback queries immediately** (handled automatically by the SDK)
2. **Send a processing message** to keep users informed
3. **Use try-catch blocks** for error handling
4. **Clean up temporary messages** after completion
5. **Provide retry options** when operations fail

### 🎨 Example: Image Generation Bot

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

---

## 🗃️ Session Management (CRUD Helpers)

The SDK provides easy CRUD helpers for managing session variables in `ctx.session.custom`.

### **Session CRUD API**

- `ctx.session.setCustom(key, value)` — Set a variable in `session.custom`
- `ctx.session.getCustom(key)` — Get a variable from `session.custom`
- `ctx.session.updateCustom({ ... })` — Update multiple variables in `session.custom`
- `ctx.session.deleteCustom(key)` — Delete a variable from `session.custom`
- `ctx.session.save(callback)` — Persist the session to Redis immediately (optional, usually auto-saved)

#### **Example Usage in a Command Handler**

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

---
## 📝 Command Menu Management

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.

### **Setting Up Command Menu**

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

---

## 🔍 Professional Logging System

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

### **Logger Features**

- 🎨 **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)

### **Using the Logger**

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

### **Automatic Context Logging**

The SDK automatically includes logging in the bot context:

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

### **Production vs Development Logging**

- **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:

```typescript
const config: AppConfig = {
  // ... other config options ...
  isDev: process.env.NODE_ENV !== 'production'
};
```

---

## 👥 Admin Authentication/Approval

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` or `admin`) can interact with the bot.

### How it works
1. When a new user interacts with the bot, their status is set to `pending` in Redis.
2. All admins (specified in `adminChatIds`) receive a message with Approve/Deny buttons.
3. When an admin approves, the user's status is set to `member` and they are notified.
4. Only users with status `member` or `admin` can use the bot; others are blocked until approved.

### Example: Admin Approval
```typescript
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!');
});
```

---

## 🛡️ Automatic Context Safety (No More !)

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:**
```typescript
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!');
});
```

---

## 📄 License

ISC

## 📚 GitHub Repository

This project is available on GitHub:
[https://github.com/AmanUpadhyay1609/-wasserstoff-mangi-tg-bot](https://github.com/AmanUpadhyay1609/-wasserstoff-mangi-tg-bot)

Issues, feature requests, and contributions are welcome!

## ⚠️ Important: Registering Event Listeners Safely

### Do NOT Attach Event Listeners Directly to the Bot Instance

**Never use:**
```ts
const botInstance = botManager.getBot();
botInstance.on("chat_member", (ctx) => { /* ... */ }); // ❌ This will cause errors!
```

#### Why?
- 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! ...

### ✅ Correct Way: Use `handleEvent` on BotManager

**Always use:**
```ts
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.).

#### Example
```ts
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.

### Type-Safe Event Names with Autocompletion

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:**
```ts
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.

#### What is `FilterQuery`?
- `FilterQuery` is a type from grammY that represents all valid event filter strings.
- See [grammY filter queries documentation](https://grammy.dev/guide/filter-queries.html) for more info and examples.
