A lightweight, type-safe menu system for grammY Telegram bots built with Deno. Create declarative inline keyboards with automatic callback routing, persistent navigation history, and media support.
- Declarative Menu Building โ Define menu builders using a chainable builder API
- Automatic Callback Routing โ Callbacks are handled internally with zero manual routing code
- Media Support โ Create menus with photos, videos, animations, audio, or documents
- Navigation History โ Built-in tracking of menu navigation per message
- Type-Safe โ Full TypeScript support with proper type inference
- Storage Flexibility โ Pluggable storage adapters for persistence (memory, Redis, etc.)
- Middleware Integration โ Seamless integration with grammY's middleware system
import { MenuBuilder, MenuRegistry } from "jsr:@your-scope/grammy-menu-message";Note: This library is currently in development and not yet published to JSR.
import { Bot } from "https://lib.deno.dev/x/grammy@v1/mod.ts";
import { MenuBuilder, MenuRegistry } from "./src/mod.ts";
const bot = new Bot(Deno.env.get("BOT_TOKEN")!);
const registry = new MenuRegistry();
// Define a menu builder
const mainMenu = new MenuBuilder("Welcome! Choose an option:")
.cb("Say Hello", async (ctx) => {
await ctx.reply("Hello! ๐");
})
.cb("Show Info", async (ctx) => {
await ctx.reply("This is a grammY menu example.");
})
.row()
.url("GitHub", "https://github.com")
.url("Documentation", "https://grammy.dev");
// Register the builder
registry.register("main", mainMenu);
// Use the registry middleware
bot.use(registry.middleware());
// Send the menu
bot.command("start", async (ctx) => {
const menu = registry.menu("main");
await ctx.reply("Loading menu...", { reply_markup: menu });
});
bot.start();A MenuBuilder is a declarative builder for defining menu structure. It supports:
- Callback buttons (
.cb()) โ Buttons with handler functions - URL buttons (
.url()) โ Direct links to websites - Web App buttons (
.webApp()) โ Open Telegram Web Apps - Inline query buttons (
.switchInline(),.switchInlineCurrent(),.switchInlineChosen()) - Special buttons โ Login, copy text, game, and payment buttons
- Row control (
.row()) โ Start a new button row
The MenuRegistry manages menu builders and handles callback routing:
- Register builders โ
registry.register("id", builder) - Render menus โ
registry.menu("id")creates a new menu instance - Middleware โ
registry.middleware()handles callbacks automatically - Storage โ Configurable storage adapters for persistence across restarts
Different menu types support different media attachments:
- MenuBuilder โ Text-only menus
- PhotoMenuBuilder โ Menus with photos
- VideoMenuBuilder โ Menus with videos
- AnimationMenuBuilder โ Menus with GIF/animations
- AudioMenuBuilder โ Menus with audio files
- DocumentMenuBuilder โ Menus with documents
You can convert between types using chainable methods like .photo(), .video(), etc.
const menu = new MenuBuilder("Choose an action:")
.cb("Option 1", async (ctx) => {
await ctx.reply("You selected option 1");
})
.cb("Option 2", async (ctx) => {
await ctx.reply("You selected option 2");
})
.row()
.cb("Back", async (ctx) => {
await ctx.reply("Going back...");
});
registry.register("basic", menu);const photoMenu = new MenuBuilder("Check out this image!")
.photo("https://picsum.photos/800/600")
.cb("Like", async (ctx) => {
await ctx.reply("Thanks for liking!");
})
.cb("Share", async (ctx) => {
await ctx.reply("Sharing...");
});
registry.register("photo", photoMenu);const mixedMenu = new MenuBuilder("Explore options:")
.cb("Settings", async (ctx) => {
await ctx.reply("Opening settings...");
})
.row()
.url("Website", "https://example.com")
.webApp("Web App", "https://app.example.com")
.row()
.switchInline("Share Bot", "check out this bot");
registry.register("mixed", mixedMenu);import { MenuRegistry } from "./src/mod.ts";
// Use custom storage adapters for persistence
const registry = new MenuRegistry({
keyPrefix: "mybot",
menuStorage: new RedisAdapter(),
navigationStorage: new RedisAdapter(),
});.cb(label, handler, payload?)โ Add callback button with handler.rawCb(label, callbackData)โ Add raw callback button (manual routing).url(text, url)โ Add URL button.webApp(text, url)โ Add Web App button.login(text, loginUrl)โ Add login button.switchInline(text, query?)โ Add inline query button.switchInlineCurrent(text, query?)โ Add inline query button (current chat).switchInlineChosen(text, query?)โ Add inline query button (chosen chat filter).copyText(text, copyText)โ Add copy text button.game(text)โ Add game button.pay(text)โ Add payment button
.row()โ Start a new button row.addText(text)โ Set or replace menu text
.photo(photo)โ Convert to PhotoMenuBuilder.video(video)โ Convert to VideoMenuBuilder.animation(animation)โ Convert to AnimationMenuBuilder.audio(audio)โ Convert to AudioMenuBuilder.document(document)โ Convert to DocumentMenuBuilder
register(templateId, builder)โ Register a menu builderget(templateId)โ Retrieve a registered builderhas(templateId)โ Check if builder existsmenu(templateId)โ Render a menu from buildermiddleware()โ Get the middleware function
This project uses Deno. Available tasks:
deno task fmt # Format code
deno task lint # Lint code
deno task test # Run tests
deno task check # Type-check code
deno task ok # Run all checks (fmt + lint + test + check)Always run deno task ok before committing.
Contributions are welcome! Please:
- Create a feature branch from
main - Make your changes with appropriate tests
- Run
deno task okto ensure all checks pass - Submit a pull request
MIT License โ see LICENSE for details.