Appearance
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
Always Answer Callback Queries: Call
ctx.answerCallbackQuery()
in every callback query handler to remove the loading state from the button.Use Descriptive Naming: Name your callback query classes according to their function (e.g.,
DeleteItemCallbackQuery
,AnswerTestCallbackQuery
).Organize Related Callback Queries: Group related callback queries in the same directory or module.
Handle Errors: Implement proper error handling to prevent callback queries from hanging.
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.