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

grammy-callbacks

v0.2.1

Published

Callback management system for Grammy Telegram bots

Readme

Grammy Callbacks

Callback handling for Grammy Telegram bots. Eliminates callback data strings and parsing—just direct function calls with parameters.

// Direct function binding
const handlers = cbs({
  async editUser(ctx /* : YourBotContextType */, userId: number) {
    // ctx already typed
    // ...user operations

    // call handlers directly by passing ctx
    await handlers.someOtherHandler(ctx);
  },

  async someOtherHandler(ctx) {

  }
});

ctx.reply('text', {
  reply_keyboard: InlineKeyboard.from([
    // bind our handler to the button with curried parameters
    [Button.cb('Edit User', handlers.editUser(123))], // Inline button 'Edit User' will call `handlers.editUser(123)`
    [handlers.otherHandler().button('Handle other')], // other format of button binding
  ]),
});
ctx.reply('text', {
  reply_keyboard: Keyboard.from([
    [handlers.otherHandler().button('Handle other')],
  ]),
});

// when user activates the button, we intercept callback_data and call handler with the exact curried parameters

Key Benefits

No callback data management - Bind functions directly to buttons instead of creating strings like "edit_user_123" and parsing them later.

Large parameters - You're not limited by Telegram's 64-byte callback data limit. Large objects are automatically stored in session.

Restart resilient - Callbacks work the same way after bot restarts thanks to deterministic function hashing.

Type-safe - Full TypeScript support with proper parameter type checking.

Supports data of any size:

const largeUserData = { id: 123, preferences: {...}, metadata: {...} };
const button = Button.cb('Edit User', handlers.editUser(largeUserData));

Installation

npm install grammy-callbacks grammy

Requirements

⚠️ Configure your sessions if you want to work across restarts. The library stores callback data and wait states in the session (or in memory, if not configured), so you must set up Grammy's session middleware before using this package.

import { session } from 'grammy';

bot.use(session({}));

Quick Start

import { Bot, session } from 'grammy';
import { cbs, callbackMiddleware, Button, waitMiddleware } from 'grammy-callbacks';

const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);

// Setup session middleware (required for callbacks)
bot.use(session({}));

// Setup callback middleware
setupCallbacks(bot);
bot.use(waitMiddleware);

// Define callback handlers
const handlers = cbs({
  async greetUser(ctx, name: string, age: number) {
    await ctx.answerCallbackQuery();
    await ctx.reply(`Hello ${name}! You are ${age} years old.`);
  },

  async greetSomeoneSpecial(ctx) {
    // use as simple handlers by passing ctx as first parameter
    await handlers.greetUser(ctx, 'Special', 42);
    // or by currying
    await handlers.greetUser('Special', 42)(ctx);
    // or
    await handlers.greetUser('Special')(42)(ctx);
  },
});

// Use curried handlers in inline keyboard
bot.command('start', async (ctx) => {
  await ctx.reply('Choose an option:', {
    reply_markup: {
      inline_keyboard: [
        [Button.cb('Greet John (25)', handlers.greetUser('John', 25))], // using helper function
        [handlers.greetUser('Jane', 30).button('Greet Jane (30)')], // or call directly
      ],
    },
  });
});

bot.start();

Core Concepts

Curried Callbacks

Curried callbacks allow you to create reusable handlers with pre-filled parameters:

// Define handlers object
const handlers = cbs({
  async handleUserAction(ctx, action: string, userId: number, extra?: string) {
    await ctx.answerCallbackQuery(`Action: ${action} for user ${userId}`);
    if (extra) {
      await ctx.reply(`Extra info: ${extra}`);
    }
  },
});

// Create specialized versions
const editUser = handlers.handleUserAction('edit');
const deleteUser = handlers.handleUserAction('delete');
const editUserWithId = editUser(123);

// Use in buttons
const keyboard = [
  [Button.cb('Edit User 123', editUserWithId())],
  [deleteUser(456).button('Delete User 456')],
  [editUser(789, 'special').button('Edit with extra')],
];

// Pass context as first parameter and call immediately as simple callback function:
await deleteUser(ctx, 456);
await handlers.handleUserAction(ctx, 'edit', 456, 'extra');

Organizing Handlers

Organize your callbacks in nested objects for better structure:

const handlers = cbs({
  user: {
    async create(ctx, name: string) {
      await ctx.reply(`Creating user: ${name}`);
    },
    async delete(ctx, id: number) {
      await ctx.reply(`Deleting user: ${id}`);
    },
    edit: {
      async name(ctx, id: number, newName: string) {
        await ctx.reply(`Changing user ${id} name to: ${newName}`);
      },
      async email(ctx, id: number, newEmail: string) {
        await ctx.reply(`Changing user ${id} email to: ${newEmail}`);
      },
    },
  },
  admin: {
    async ban(ctx, userId: number, reason: string) {
      await ctx.reply(`Banned user ${userId}: ${reason}`);
    },
  },
});

// Use with partial application
const createJohn = handlers.user.create('John');
const editUser456Name = handlers.user.edit.name(456, 'NewName');

// Create buttons
const keyboard = [
  [Button.cb('Create John', createJohn())],
  [Button.cb('Rename User 456', editUser456Name())],
  [Button.cb('Ban Spammer', handlers.admin.ban(789, 'spam'))],
];

Wait for User Input

Prompt users for input and handle their responses:

import { cbs, wait, waitMiddleware } from 'grammy-callbacks';

// Register wait middleware
bot.use(waitMiddleware);

const handlers = cbs({
  async askForName(ctx, greeting: string) {
    await ctx.reply("What's your name?");
    // Wait for text input and pass it to handleNameInput
    wait(ctx, handlers.handleNameInput(greeting));
  },

  async handleNameInput(ctx, greeting: string, name: string) {
    await ctx.reply(`${greeting}, ${name}! Nice to meet you.`);
  },
});

bot.command('introduce', async (ctx) => {
  await handlers.askForName(ctx, 'Hello');
});

Advanced Wait Usage

// Wait with custom filters and options
const handlers = cbs({
  async handleResponse(ctx, prefix: string, userInput: string) {
    await ctx.reply(`${prefix}: ${userInput}`);
  },

  async promptWithOptions(ctx) {
    await ctx.reply('Send me a message or click a button:', {
      reply_markup: {
        inline_keyboard: [
          [
            InlineKeyboard.text('Option A', 'Option A'),
            InlineKeyboard.text('Option B', 'Option B'),
          ],
        ],
      },
    });

    // Wait for either text message or callback query
    wait(ctx, ['message:text', 'callback_query:data'], handlers.handleResponse('You sent'), {
      cancelKeyword: '/cancel',
      timeoutMs: 30000,
    });
  },
});

API Reference

Core Functions

cbs(callbacksObj)

Recursively converts nested callback objects to curried versions. This is the main function you'll use to register callbacks.

const handlers = cbs({
  user: {
    async edit(ctx, param1: string, param2: number) {
      // Handler logic
    },
  },
});

// Returns curried callbacks that can be partially applied
const partialHandler = handlers.user.edit('value1');
const fullHandler = partialHandler(42);

bindCbs()

Returns a function bound to your custom Context type that converts callback objects to curried versions with proper type safety.

// bot-context.ts – Define your custom context type
export type MyBotContext = Context & {
  user: { id: number; name: string };
  db: DatabaseConnection;
};

// Create a type-safe callback binder; pass your Context type
export const cbs = bindCbs<MyBotContext>();

// In your bot file
import { cbs } from './bot-context';
// Now all handlers will be typed with your custom context
const handlers = cbs({
  async saveUser(ctx) {
    // ctx: MyBotContext
    // ctx.user and ctx.db are fully typed here
    await ctx.db.save(ctx.user);
    await ctx.reply(`Saved user ${ctx.user.name}`);
  },

  async updateProfile(ctx, newName: string) {
    // ctx: MyBotContext
    // Full type safety with custom context
    ctx.user.name = newName;
    await ctx.db.update(ctx.user);
  },
});

executeCallback(ctx, callbackData, ...args)

Execute a callback from a callback data string.

Wait Functions

wait(ctx, handler, options?)

Wait for user input with default text message filter.

wait(ctx, filter, handler)

Wait for user input with custom filter.

wait(ctx, filter, handler, options?)

Wait for user input with custom filter and options.

waitMiddleware(ctx, next)

Middleware to handle wait responses. Must be registered with your bot.

clearWaitState(ctx)

Manually clear the wait state.

Button Helpers

Button.cb(text, callbackData)

Create an inline keyboard button with callback data.

const handlers = cbs({ myHandler });
const button = Button.cb('Click me', handlers.myHandler('param'));

Middleware

setupCallbacks(bot)

Main middleware for handling callback queries. Must be registered with your bot.

Types

CurriedCallback<R, T, Ctx>

A curried callback function type.

WaitOptions

Options for configuring wait behavior:

interface WaitOptions {
  // validator handler to pre-filter messages
  validator?: CurriedCallback<boolean | string>;
  // array of Telegram update types you want to accept in the handler:
  filter?: FilterQuery[]; // ['message:text', 'callback_query:data', 'message:picture']
  messageId?: number;
  // text to send to cancel waiting for input
  cancelKeyword?: string;
  // wait timeout in milliseconds; after this time expires, the handler will not execute
  timeoutMs?: number;
}

Examples

See the examples/ directory for complete examples:

  • simple-bot.ts - Basic usage with callbacks and wait functionality

Best Practices

  1. Always use session middleware - Callbacks require session storage
  2. Add callback middleware early - Should be one of the first middlewares
  3. Organize callbacks logically - Use nested objects for better organization
  4. Use TypeScript - Get full type safety and IntelliSense

Session Requirements

Grammy Callbacks requires session middleware to be configured:

import { session } from 'grammy';

bot.use(session({}));

Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests.

License

MIT