Skip to content

Callback Queries for Inline Keyboards

Overview

Callback queries in the NestKit grammY integration provide a structured way to handle user interactions with inline keyboard buttons. This system allows you to create reusable callback query handlers that respond to button clicks, leveraging our extended context system.

Understanding Inline Keyboards

Inline keyboards display buttons directly under a message in a Telegram chat. When a user taps a button, a callback query is sent to your bot with the associated callback data. Our system makes processing these interactions straightforward and type-safe.

For detailed information about inline keyboards, refer to the official grammY documentation on keyboards.

Creating Callback Queries

To handle button clicks, create a class that extends BaseCallbackQuery:

typescript
import { GrammyContext } from '@deep/nest-kit/dist/grammy-kit';
import { BaseCallbackQuery } from '@deep/nest-kit/dist/grammy-kit/callback-queries';
import { Injectable } from '@nestjs/common';
import { CallbackQueryContext } from 'grammy';

@Injectable()
export class ExampleCallbackQuery extends BaseCallbackQuery {
  // Define the trigger pattern(s) that will match callback data
  protected trigger = 'example'; // Can also be a regex or array of strings/regexes

  async run(data: string, ctx: CallbackQueryContext<GrammyContext>): Promise<void> {
    // Handle the callback query
    await ctx.answerCallbackQuery('You clicked the example button!');
    
    // You can also update the message, send a new message, etc.
    await ctx.editMessageText('Button was clicked!');
  }
}

After creating a callback query class, remember to add it to your module's providers:

typescript
@Module({
  providers: [
    ExampleCallbackQuery,
    // Other providers...
  ],
})
export class BotModule {}

Callback Data with Arguments

The BaseCallbackQuery system supports passing arguments in the callback data using a pipe separator (|). This allows buttons to transmit additional information:

typescript
// When creating an inline keyboard
const keyboard = new InlineKeyboard()
  .text('Delete Item', `delete|${itemId}`)

In your callback query handler, use getArgs() to extract these arguments:

typescript
@Injectable()
export class DeleteItemCallbackQuery extends BaseCallbackQuery {
  protected trigger = 'delete';

  async run(data: string, ctx: CallbackQueryContext<GrammyContext>): Promise<void> {
    const [itemId] = this.getArgs(data);
    
    this.logger.info(`User ${ctx.user.fullName} requested to delete item ${itemId}`);
    await this.handleDelete(itemId, ctx);
    await ctx.answerCallbackQuery();
  }
  
  private async handleDelete(itemId: string, ctx: CallbackQueryContext<GrammyContext>): Promise<void> {
    // Delete item logic
  }
}

Accessing User and Context

Like other parts of our system, callback queries have access to the authenticated user through our extended context:

typescript
async run(data: string, ctx: CallbackQueryContext<GrammyContext>): Promise<void> {
  if (ctx.isGuest()) {
    await ctx.answerCallbackQuery('Please register first');
    return;
  }
  
  // Only authenticated users reach this point
  this.logger.info(`User ${ctx.user.id} (${ctx.user.fullName}) clicked a button`);
  
  if (ctx.isGranted('items.delete')) {
    // User has permission to delete items
    await this.processDelete(data, ctx);
  } else {
    await ctx.answerCallbackQuery('You do not have permission for this action');
  }
}

Best Practices

  1. Always Answer Callback Queries: Call ctx.answerCallbackQuery() in every callback query handler to remove the loading state from the button.

  2. Use Descriptive Naming: Name your callback query classes according to their function (e.g., DeleteItemCallbackQuery, AnswerTestCallbackQuery).

  3. Organize Related Callback Queries: Group related callback queries in the same directory or module.

  4. Handle Errors: Implement proper error handling to prevent callback queries from hanging.

  5. Keep Callback Data Small: Telegram has a size limit for callback data. Consider using IDs or short codes instead of full objects.

Example: Implementing Pagination with Callback Queries

Here's a complete example of implementing pagination for a list of items:

typescript
// Creating the keyboard
const buildPaginationKeyboard = (currentPage: number, totalPages: number): InlineKeyboard => {
  const keyboard = new InlineKeyboard();
  
  if (currentPage > 1) {
    keyboard.text('⬅️ Previous', `pagination|prev|${currentPage - 1}`);
  }
  
  keyboard.text(`Page ${currentPage}/${totalPages}`, `pagination|info`);
  
  if (currentPage < totalPages) {
    keyboard.text('Next ➡️', `pagination|next|${currentPage + 1}`);
  }
  
  return keyboard;
};

// The callback query handler
@Injectable()
export class PaginationCallbackQuery extends BaseCallbackQuery {
  protected trigger = /^pagination|/;
  
  constructor(private readonly itemsService: ItemsService) {
    super();
  }

  async run(data: string, ctx: CallbackQueryContext<GrammyContext>): Promise<void> {
    const [pageStr] = this.getArgs(data);
    
    const page = parseInt(pageStr, 10);
    const [items, meta] = await this.itemsService.getMany({
      filter: {},
    }, ctx);
    
    const text = this.formatItems(items);
    const keyboard = buildPaginationKeyboard(page, meta.total);
    
    await ctx.editMessageText(text, { reply_markup: keyboard });
    await ctx.answerCallbackQuery();
  }
  
  private formatItems(items: Item[]): string {
    // Format items as text
    return items.map(item => `- ${item.name}`).join('\n');
  }
}

Conclusion

The BaseCallbackQuery system provides a structured and type-safe way to handle user interactions with inline keyboards. By extending the BaseCallbackQuery class, you can create reusable callback query handlers that leverage our extended context system with user authentication, permissions, and logging capabilities.

Created by DeepVision Software.