grammY Inline Menu

repository·main·Indexed 18 days ago

https://github.com/edjopato/grammy-inline-menu

A library for simplifying the creation and management of Telegram inline keyboards for grammY bots. It utilizes a path-based tree structure to handle nested menus and button interactions, featuring tools for pagination, submenus, and state toggling. Successor to telegraf-inline-menu.

Tokens
4.5K
Snippets
17
Records
19
Agent score
14%

What's inside grammy-inline-menu

  1. How the menu tree and callback data work

    main

    The library manages menus using a tree-like structure of callback data paths. This allows for nested submenus.

    • Root Path: The main menu uses the path provided to MenuMiddleware (e.g., /) as its base.
    • Buttons: A button in the main menu uses a path like /my-button.
    • Submenus: A submenu is identified by a path ending in a slash, such as /my-submenu/.

    Behavior Logic:

    • If the callback data ends with a /, the library treats it as a request to show a corresponding submenu.
    • If the callback data does not end with a /, the library treats it as an interaction (an action to be executed).

    To manually send a submenu, you must provide the exact path used by the button (e.g., /my-submenu/).

  2. Debug menu callback data

    main

    You can inspect the callback data being sent by buttons using a middleware that logs ctx.callbackQuery.data. Additionally, you can inspect the internal regular expression tree used by the middleware by calling menuMiddleware.tree().

    // Log callback data to see what is being sent
    bot.use((ctx, next) => {
    	if (ctx.callbackQuery) {
    		console.log("callback data just happened", ctx.callbackQuery.data);
    	}
    	return next();
    });
    
    bot.use(menuMiddleware);
    
    // Inspect the menu structure tree
    console.log(menuMiddleware.tree());
  3. Migrate from version 8 to 9

    main

    In version 9, MenuTemplate methods were updated to accept arguments within an options object instead of as separate positional arguments. This change improves readability and allows for easier inlining of functions.

    Key changes:

    • .interact(): The text is now a property inside the options object.
    • .url(): All parameters are moved into a single options object.
    • .choose(): The choices are moved into a choices key within the options object.
    -// Version 8 style
    -menuTemplate.interact((ctx) => ctx.i18n.t('button'), 'unique', { 
    - 
    +// Version 9 style
    +menuTemplate.interact('unique', {
    +  text: (ctx) => ctx.i18n.t('button'),
    +  do: async (ctx) => { ... }
    +});
    
    -// Version 8 style
    -menuTemplate.url('Text', 'https://edjopato.de', { joinLastRow: true });
    +// Version 9 style
    +menuTemplate.url({ text: 'Text', url: 'https://edjopato.de', joinLastRow: true });
    
    -// Version 8 style
    -menuTemplate.choose('unique', ['walk', 'swim'], { 
    -
    +// Version 9 style
    +menuTemplate.choose('unique', {
    +  choices: ['walk', 'swim'],
    +  do: async (ctx, key) => { ... }
    +});
  4. Create a basic inline menu

    main

    To create a menu, define a MenuTemplate that specifies the message body (often using a function to access context), define button interactions using .interact(), and then register the MenuMiddleware with your bot. Use menuMiddleware.replyToContext(ctx) to send the menu to the user.

    import { Bot } from "grammy";
    import { MenuMiddleware, MenuTemplate } from "grammy-inline-menu";
    
    // Define the template with a function for the message body
    const menuTemplate = new MenuTemplate<MyContext>((ctx) =>
    	`Hey ${ctx.from.first_name}!`
    );
    
    // Define an interaction with a unique ID
    menuTemplate.interact("unique", {
    	text: "I am excited!",
    	do: async (ctx) => {
    		await ctx.reply("As am I!");
    		return false;
    	},
    });
    
    const bot = new Bot(process.env.BOT_TOKEN);
    
    // Initialize middleware with a root path (e.g., "/")
    const menuMiddleware = new MenuMiddleware("/", menuTemplate);
    
    // Use the middleware and trigger the menu via a command
    bot.command("start", (ctx) => menuMiddleware.replyToContext(ctx));
    bot.use(menuMiddleware);
    
    await bot.start();
  5. Toggle a single value with .toggle()

    main

    Use menuTemplate.toggle(id, options) to create a button that switches a boolean state in your session.

    • isSet: A function that returns true if the value is currently active.
    • set: A function that updates the state and returns true to trigger a menu update.
    menuTemplate.toggle("unique", {
    	text: "Text",
    	isSet: (ctx) => ctx.session.isFunny,
    	set: (ctx, newState) => {
    		ctx.session.isFunny = newState;
    		return true;
    	},
    });
  6. Use media (photos, etc.) as the menu body

    main

    A MenuTemplate can display media instead of just text. The body can be an object following Telegram's InputMedia structure. The type and media properties are passed directly to grammY.

    Supported types include photo, video, audio, etc. The media.source can be a file path or a URL.

    const menuTemplate = new MenuTemplate<MyContext>((ctx, path) => {
    	return {
    		type: "photo",
    		media: {
    			source: `./${ctx.from.id}.jpg`,
    		},
    		text: "Some *caption*",
    		parse_mode: "Markdown",
    	};
    });
  7. Select one of many values with .select()

    main

    Use menuTemplate.select(id, options) to allow users to pick one option from a list. This automatically updates the menu to show the currently selected item.

    • choices: An array of strings or a Record<string, string> (where keys are IDs and values are display text).
    • isSet: Determines if a specific key is currently selected.
    • set: Updates the state with the selected key.
    // Using an array of choices
    menuTemplate.select("unique", {
    	choices: ["human", "bird"],
    	isSet: (ctx, key) => ctx.session.choice === key,
    	set: (ctx, key) => {
    		ctx.session.choice = key;
    		return true;
    	},
    });
    
    // Using a Record for dynamic text
    const choices: Record<string, string> = {
    	a: 'Alphabet',
    	b: 'Beta',
    	c: 'Canada'
    };
    
    menuTemplate.select('unique', {
        choices,
        isSet: (ctx, key) => ctx.session.choice === key,
        set: (ctx, key) => {
            ctx.session.choice = key;
            return true;
        }
    });
  8. Send a menu manually

    main

    You can send a menu outside of the standard middleware flow using several methods:

    1. Via Middleware: Use menuMiddleware.replyToContext(ctx, [path]) to reply to a context with a specific menu or submenu.
    2. Via Helper Functions: Use replyMenuToContext(menuTemplate, ctx, path) to send a specific menu template to a context.
    3. Via External Events: Use generateSendMenuToChatFunction(bot.telegram, menu, rootPath) to create a function that can be called from anywhere (e.g., an external event handler), provided you pass the user's context.
    // 1. Reply via middleware
    const menuMiddleware = new MenuMiddleware("/", menuTemplate);
    bot.command("start", (ctx) => menuMiddleware.replyToContext(ctx));
    
    // 2. Reply via helper
    import { replyMenuToContext } from "grammy-inline-menu";
    bot.command("settings", async (ctx) => {
        await replyMenuToContext(settingsMenu, ctx, "/settings/");
    });
    
    // 3. Send from external event
    const sendMenuFunction = generateSendMenuToChatFunction(
        bot.telegram,
        menu,
        "/settings/",
    );
    
    async function externalEventOccurred() {
        await sendMenuFunction(userId, context);
    }
  9. Execute logic based on choice with .choose()

    main

    Use menuTemplate.choose(id, options) when you want to perform an action based on the user's selection rather than just updating a state. Unlike .select(), .choose() does not automatically update the menu unless you return true or a path from the do function.

    menuTemplate.choose("unique", {
    	choices: ["walk", "swim"],
    	do: async (ctx, key) => {
    		await ctx.answerCallbackQuery(`Lets ${key}`);
    		return ".."; // Go back to parent
    	},
    });
  10. Close a menu safely

    main

    To close a menu, you can use ctx.deleteMessage(). However, it is recommended to use the helper deleteMenuFromContext(context). This function attempts to delete the message; if deletion fails (e.g., message is too old), it removes the inline keyboard so the user cannot interact with a non-existent menu.

    import { deleteMenuFromContext } from "grammy-inline-menu";
    
    menuTemplate.interact("unique", {
    	text: "Delete the menu",
    	do: async (context) => {
    		await deleteMenuFromContext(context);
    		return false;
    	},
    });