UNPKG

@grammyjs/conversations

Version:

Conversational interfaces for grammY

594 lines (593 loc) 25.6 kB
import { type Context, type CopyTextButton, type Filter, type InlineKeyboardButton, type InlineKeyboardMarkup, type LoginUrl, type Middleware, type SwitchInlineQueryChosenChat } from "./deps.node.js"; declare const ops: unique symbol; declare const opts: unique symbol; /** A handler function for a menu button */ export type ButtonHandler<C extends Context> = (ctx: C) => unknown | Promise<unknown>; /** Options when creating a menu */ export interface ConversationMenuOptions<C extends Context> { /** * Identifier of the parent menu. Using a `back` button will navigate to * this menu. */ parent?: string | { id: string; }; /** * Conversational menus will automatically call `ctx.answerCallbackQuery` * with no arguments. If you want to call the method yourself, for example * because you need to send custom messages, you can set `autoAnswer` to * `false` to disable this behavior. */ autoAnswer: boolean; /** * Fingerprint function that lets you generate a unique string every time a * menu is rendered. Used to determine if a menu is outdated. If specified, * replaces the built-in heuristic. * * Using this option is required if you want to enable compatibility with an * outside menu defined by the menu plugin. It is rarely useful if you * simply want to define a menu inside a conversation. * * The built-in heuristic that determines whether a menu is outdated takes * the following things into account: * - identifier of the menu * - shape of the menu * - position of the pressed button * - potential payload * - text of the pressed button * * If all of these things are identical but the menu is still outdated, you * can use this option to supply the neccessary data that lets the menu * plugin determine more accurately if the menu is outdated. Similarly, if * any of these things differ but you want to consider the menu to be up to * date, you can also use this option to signal that. * * In other words, specifying a fingerprint function will replace the above * heuristic entirely by your own implementation. */ fingerprint: DynamicString<C>; } /** * A container for many menu instances that are created during a replay of a * conversation. * * You typically do not have to construct this class yourself, but it is used * internally in order to provide `conversation.menu` inside conversations. */ export declare class ConversationMenuPool<C extends Context> { private index; private dirty; /** * Marks a menu as dirty. When an API call will be performed that edits the * specified message, the given menu will be injected into the payload. If * no such API happens while processing an update, the all dirty menus will * be updated eagerly using `editMessageReplyMarkup`. * * @param chat_id The chat identifier of the menu * @param message_id The message identifier of the menu * @param menu The menu to inject into a payload */ markMenuAsDirty(chat_id: string | number, message_id: number, menu?: ConversationMenu<C>): void; /** * Looks up a dirty menu, returns it, and marks it as clean. Returns * undefined if the given message does not have a menu that is marked as * dirty. * * @param chat_id The chat identifier of the menu * @param message_id The message identifier of the menu */ getAndClearDirtyMenu(chat_id: string | number, message_id: number): ConversationMenu<C> | undefined; /** * Creates a new conversational menu with the given identifier and options. * * If no identifier is specified, an identifier will be auto-generated. This * identifier is guaranteed not to clash with any outside menu identifiers * used by [the menu plugin](https://grammy.dev/plugins/menu). In contrast, * if an identifier is passed that coincides with the identifier of a menu * outside the conversation, menu compatibility can be achieved. * * @param id An optional menu identifier * @param options An optional options object */ create(id?: string, options?: Partial<ConversationMenuOptions<C>>): ConversationMenu<C>; /** * Looks up a menu by its identifier and returns the menu. Throws an error * if the identifier cannot be found. * * @param id The menu identifier to look up */ lookup(id: string | { id: string; }): ConversationMenu<C>; /** * Prepares a context object for supporting conversational menus. Returns a * function to handle clicks. * * @param ctx The context object to prepare */ install(ctx: C): { handleClicks: () => Promise<{ next: boolean; }>; }; } /** * Context flavor for context objects in listeners that react to conversational * menus. Provides `ctx.menu`, a control pane for the respective conversational * menu. */ export interface ConversationMenuFlavor { /** Narrows down `ctx.match` to string for menu payloads */ match?: string; /** * Control panel for the currently active conversational menu. `ctx.menu` is * only available for listeners that are passed as handlers to a * conversational menu, and it allows you to perform simple actions such as * navigating the menu, or updating or closing it. * * As an example, if you have a text button that changes its label based on * `ctx`, then you should call * * ```ts * ctx.menu.update() * ``` * * whenever you mutate some state in such a way that the label should * update. The same is true for dynamic ranges that change their layout. * * If you edit the message yourself after calling one of the functions on * `ctx.menu`, the new menu will be automatically injected into the payload. * Otherwise, a dedicated API call will be performed after your middleware * completes. */ menu: ConversationMenuControlPanel; } /** * Control panel for conversational menus. Can be used to update or close the * conversational menu, or to perform manual navigation between conversational * menus. */ export interface ConversationMenuControlPanel { /** * Call this method to update the conversational menu. For instance, if you * have a button that changes its text based on `ctx`, then you should call * this method to update it. * * Calling this method will guarantee that the conversational menu is * updated, but note that this will perform the update lazily. A new * conversational menu is injected into the payload of the request the next * time you edit the corresponding message. If you let your middleware * complete without editing the message itself again, a dedicated API call * will be performed that updates the conversational menu. * * Pass `{ immediate: true }` to perform the update eagerly instead of * lazily. A dedicated API call that updates the conversational menu is sent * immediately. In that case, the method returns a Promise that you should * `await`. Eager updating may cause flickering of the conversational menu, * and it may be slower in some cases. */ update(config: { immediate: true; }): Promise<void>; update(config?: { immediate?: false; }): void; /** * Closes the conversational menu. Removes all buttons underneath the * message. * * Calling this method will guarantee that the conversational menu is * closed, but note that this will be done lazily. A new conversational menu * is injected into the payload of the request the next time you edit the * corresponding message. If you let your middleware complete without * editing the message itself again, a dedicated API call will be performed * that closes the conversational menu. * * Pass `{ immediate: true }` to perform the update eagerly instead of * lazily. A dedicated API call that updates the conversational menu is sent * immediately. In that case, the method returns a Promise that you should * `await`. Eager closing may be slower in some cases. */ close(config: { immediate: true; }): Promise<void>; close(config?: { immediate?: false; }): void; /** * Navigates to the parent menu. By default, the parent menu is the menu on * which you called `register` when installing this menu. * * Throws an error if this menu does not have a parent menu. * * Calling this method will guarantee that the navigation is performed, but * note that this will be done lazily. A new menu is injected into the * payload of the request the next time you edit the corresponding message. * If you let your middleware complete without editing the message itself * again, a dedicated API call will be performed that performs the * navigation. * * Pass `{ immediate: true }` to navigate eagerly instead of lazily. A * dedicated API call is sent immediately. In that case, the method returns * a Promise that you should `await`. Eager navigation may cause flickering * of the menu, and it may be slower in some cases. */ back(config: { immediate: true; }): Promise<void>; back(config?: { immediate?: false; }): void; /** * Navigates to the specified conversational submenu. The given identifier * is the same string that you pass to `conversation.menu('')`. If you did * not pass a string, the identifier will be auto-generated and is * accessible via `menu.id`. If you specify the identifier of the current * conversational menu itself, this method is equivalent to * `ctx.menu.update()`. * * Calling this method will guarantee that the navigation is performed, but * note that this will be done lazily. A new conversational menu is injected * into the payload of the request the next time you edit the corresponding * message. If you let your middleware complete without editing the message * itself again, a dedicated API call will be performed that performs the * navigation. * * Pass `{ immediate: true }` to navigate eagerly instead of lazily. A * dedicated API call is sent immediately. In that case, the method returns * a Promise that you should `await`. Eager navigation may cause flickering * of the conversational menu, and it may be slower in some cases. */ nav(to: string | { id: string; }, config: { immediate: true; }): Promise<void>; nav(to: string | { id: string; }, config?: { immediate?: false; }): void; } /** * Type of context objects received by buttons handlers of conversational menus. */ export type ConversationMenuContext<C extends Context> = Filter<C, "callback_query:data"> & ConversationMenuFlavor; /** * Middleware that has access to the `ctx.menu` control panel. This is the type * of functions that are used as button handlers in conversational menus. */ export type ConversationMenuMiddleware<C extends Context> = Middleware<ConversationMenuContext<C>>; type MaybePromise<T> = T | Promise<T>; type DynamicString<C extends Context> = (ctx: C) => MaybePromise<string>; type MaybeDynamicString<C extends Context> = string | DynamicString<C>; interface TextAndPayload<C extends Context> { text: MaybeDynamicString<C>; payload?: MaybeDynamicString<C>; } /** A dynamic string, or an object with a text and a payload */ export type MaybePayloadString<C extends Context> = MaybeDynamicString<C> | TextAndPayload<C>; type Cb<C extends Context> = Omit<InlineKeyboardButton.CallbackButton, "callback_data"> & { middleware: ConversationMenuMiddleware<C>[]; payload?: MaybeDynamicString<C>; }; type NoCb = Exclude<InlineKeyboardButton, InlineKeyboardButton.CallbackButton>; type RemoveAllTexts<T> = T extends { text: string; } ? Omit<T, "text"> : T; type MakeUrlDynamic<C extends Context, T> = T extends { url: string; } ? Omit<T, "url"> & { url: MaybeDynamicString<C>; } : T; /** * Button of a conversational menu. Almost the same type as InlineKeyboardButton * but with texts that can be generated on the fly, and middleware for callback * buttons. */ export type MenuButton<C extends Context> = { /** * Label text on the button, or a function that can generate this text. The * function is supplied with the context object that is used to make the * request. */ text: MaybeDynamicString<C>; } & MakeUrlDynamic<C, RemoveAllTexts<NoCb | Cb<C>>>; type RawRange<C extends Context> = MenuButton<C>[][]; type MaybeRawRange<C extends Context> = ConversationMenuRange<C> | RawRange<C>; type DynamicRange<C extends Context> = (ctx: C) => MaybePromise<MaybeRawRange<C>>; type MaybeDynamicRange<C extends Context> = MaybeRawRange<C> | DynamicRange<C>; /** * A conversational menu range is a two-dimensional array of buttons. * * This array is a part of the total two-dimensional array of buttons. This is * mostly useful if you want to dynamically generate the structure of the * conversational menu on the fly. */ export declare class ConversationMenuRange<C extends Context> { [ops]: MaybeDynamicRange<C>[]; /** * This method is used internally whenever a new range is added. * * @param range A range object or a two-dimensional array of menu buttons */ addRange(...range: MaybeDynamicRange<C>[]): this; /** * This method is used internally whenever new buttons are added. Adds the * buttons to the current row. * * @param btns Menu button object */ add(...btns: MenuButton<C>[]): this; /** * Adds a 'line break'. Call this method to make sure that the next added * buttons will be on a new row. */ row(): this; /** * Adds a new URL button. Telegram clients will open the provided URL when * the button is pressed. Note that they will not notify your bot when that * happens, so you cannot react to this button. * * @param text The text to display * @param url HTTP or tg:// url to be opened when button is pressed. Links tg://user?id=<user_id> can be used to mention a user by their ID without using a username, if this is allowed by their privacy settings. */ url(text: MaybeDynamicString<C>, url: MaybeDynamicString<C>): this; /** * Adds a new text button. You may pass any number of listeners. They will * be called when the button is pressed. * * ```ts * menu.text('Hit me!', ctx => ctx.reply('Ouch!')) * ``` * * If you pass several listeners, make sure that you understand what * [middleware](https://grammy.dev/guide/middleware.html) is. * * You can also use this method to register a button that depends on the * current context. * * ```ts * function greetInstruction(ctx: MyConversationContext): string { * const username = ctx.from?.first_name * return `Greet ${username ?? 'me'}!`, * } * * const menu = conversation.menu() * .text(greetInstruction, ctx => ctx.reply("I'm too shy.")) * * // This will send a conversational menu with one text button, * // and the text has the name of the user that the bot is replying to. * await ctx.reply('What shall I do?', { reply_markup: menu }) * ``` * * If you base the text on a variable defined inside the conversation, you * can easily create a settings panel with toggle buttons. * * ```ts * // Button will toggle between 'Yes' and 'No' when pressed * let flag = true * menu.text(ctx => flag ? 'Yes' : 'No', async ctx => { * flag = !flag * ctx.menu.update() * }) * ``` * * @param text The text to display, or a text with payload * @param middleware The listeners to call when the button is pressed */ text(text: MaybeDynamicString<C>, ...middleware: ConversationMenuMiddleware<C>[]): this; text(text: TextAndPayload<C>, ...middleware: ConversationMenuMiddleware<C & { match: string; }>[]): this; text(text: MaybePayloadString<C>, ...middleware: ConversationMenuMiddleware<C>[]): this; /** * Adds a new web app button, confer https://core.telegram.org/bots/webapps * * @param text The text to display * @param url An HTTPS URL of a Web App to be opened with additional data */ webApp(text: MaybeDynamicString<C>, url: string): this; /** * Adds a new login button. This can be used as a replacement for the * Telegram Login Widget. You must specify an HTTPS URL used to * automatically authorize the user. * * @param text The text to display * @param loginUrl The login URL as string or `LoginUrl` object */ login(text: MaybeDynamicString<C>, loginUrl: string | LoginUrl): this; /** * Adds a new inline query button. Telegram clients will let the user pick a * chat when this button is pressed. This will start an inline query. The * selected chat will be prefilled with the name of your bot. You may * provide a text that is specified along with it. * * Your bot will in turn receive updates for inline queries. You can listen * to inline query updates like this: * * ```ts * // Listen for specifc query * bot.inlineQuery('my-query', ctx => { ... }) * // Listen for any query * bot.on('inline_query', ctx => { ... }) * ``` * * Technically, it is also possible to wait for an inline query inside the * conversation using `conversation.waitFor('inline_query')`. However, * updates about inline queries do not contain a chat identifier. Hence, it * is typically not possible to handle them inside a conversation, as * conversation data is stored per chat by default. * * @param text The text to display * @param query The (optional) inline query string to prefill */ switchInline(text: MaybeDynamicString<C>, query?: string): this; /** * Adds a new inline query button that acts on the current chat. The * selected chat will be prefilled with the name of your bot. You may * provide a text that is specified along with it. This will start an inline * query. * * Your bot will in turn receive updates for inline queries. You can listen * to inline query updates like this: * * ```ts * // Listen for specifc query * bot.inlineQuery('my-query', ctx => { ... }) * // Listen for any query * bot.on('inline_query', ctx => { ... }) * ``` * * Technically, it is also possible to wait for an inline query inside the * conversation using `conversation.waitFor('inline_query')`. However, * updates about inline queries do not contain a chat identifier. Hence, it * is typically not possible to handle them inside a conversation, as * conversation data is stored per chat by default. * * @param text The text to display * @param query The (optional) inline query string to prefill */ switchInlineCurrent(text: MaybeDynamicString<C>, query?: string): this; /** * Adds a new inline query button. Telegram clients will let the user pick a * chat when this button is pressed. This will start an inline query. The * selected chat will be prefilled with the name of your bot. You may * provide a text that is specified along with it. * * Your bot will in turn receive updates for inline queries. You can listen * to inline query updates like this: * ```ts * bot.on('inline_query', ctx => { ... }) * ``` * * Technically, it is also possible to wait for an inline query inside the * conversation using `conversation.waitFor('inline_query')`. However, * updates about inline queries do not contain a chat identifier. Hence, it * is typically not possible to handle them inside a conversation, as * conversation data is stored per chat by default. * * @param text The text to display * @param query The query object describing which chats can be picked */ switchInlineChosen(text: MaybeDynamicString<C>, query?: SwitchInlineQueryChosenChat): this; /** * Adds a new copy text button. When clicked, the specified text will be * copied to the clipboard. * * @param text The text to display * @param copyText The text to be copied to the clipboard */ copyText(text: string, copyText: string | CopyTextButton): this; /** * Adds a new game query button, confer * https://core.telegram.org/bots/api#games * * This type of button must always be the first button in the first row. * * @param text The text to display */ game(text: MaybeDynamicString<C>): this; /** * Adds a new payment button, confer * https://core.telegram.org/bots/api#payments * * This type of button must always be the first button in the first row and can only be used in invoice messages. * * @param text The text to display */ pay(text: MaybeDynamicString<C>): this; /** * Adds a button that navigates to a given conversational submenu when * pressed. You can pass in an instance of another conversational menu, or * just the identifier of a conversational menu. This way, you can * effectively create a network of conversational menus with navigation * between them. * * You can also navigate to this submenu manually by calling * `ctx.menu.nav(menu)`, where `menu` is the target submenu (or its * identifier). * * You can call `submenu.back()` to add a button that navigates back to the * parent menu. For this to work, you must specify the `parent` option when * creating the conversational menu via `conversation.menu`. * * @param text The text to display, or a text with payload * @param menu The submenu to open, or its identifier * @param middleware The listeners to call when the button is pressed */ submenu(text: MaybeDynamicString<C>, menu: string | { id: string; }, ...middleware: ConversationMenuMiddleware<C>[]): this; submenu(text: TextAndPayload<C>, menu: string | { id: string; }, ...middleware: ConversationMenuMiddleware<C & { match: string; }>[]): this; submenu(text: MaybePayloadString<C>, menu: string | { id: string; }, ...middleware: ConversationMenuMiddleware<C>[]): this; /** * Adds a text button that performs a navigation to the parent menu via * `ctx.menu.back()`. For this to work, you must specify the `parent` option * when creating the conversational menu via `conversation.menu`. * * @param text The text to display, or a text with payload * @param middleware The listeners to call when the button is pressed */ back(text: MaybeDynamicString<C>, ...middleware: ConversationMenuMiddleware<C>[]): this; back(text: TextAndPayload<C>, ...middleware: ConversationMenuMiddleware<C & { match: string; }>[]): this; back(text: MaybePayloadString<C>, ...middleware: ConversationMenuMiddleware<C>[]): this; /** * This is a dynamic way to initialize the conversational menu. A typical * use case is when you want to create an arbitrary conversational menu, * using the data from your database: * * ```ts * const menu = conversation.menu() * const data = await conversation.external(() => fetchDataFromDatabase()) * menu.dynamic(ctx => data.reduce((range, entry) => range.text(entry)), new ConversationMenuRange()) * await ctx.reply("Menu", { reply_markup: menu }) * ``` * * @param menuFactory Async menu factory function */ dynamic(rangeBuilder: (ctx: C, range: ConversationMenuRange<C>) => MaybePromise<MaybeRawRange<C> | void>): this; /** * Appends a given range to this range. This will effectively replay all * operations of the given range onto this range. * * @param range A potentially raw range */ append(range: MaybeRawRange<C>): this; } /** * A conversational menu is a set of interactive buttons that is displayed * beneath a message. It uses an [inline * keyboard](https://grammy.dev/plugins/keyboard.html) for that, so in a sense, * a conversational menu is just an inline keyboard spiced up with interactivity * (such as navigation between multiple pages). * * ```ts * // Create a simple conversational menu * const menu = conversation.menu() * .text('A', ctx => ctx.reply('You pressed A!')).row() * .text('B', ctx => ctx.reply('You pressed B!')) * * // Send the conversational menu * await ctx.reply('Check out this menu:', { reply_markup: menu }) * ``` * * Check out the [official * documentation](https://grammy.dev/plugins/conversations) to see how you can * create menus that span several pages, how to navigate between them, and more. */ export declare class ConversationMenu<C extends Context> extends ConversationMenuRange<C> implements InlineKeyboardMarkup { readonly id: string; [opts]: ConversationMenuOptions<C>; constructor(id: string, options?: Partial<ConversationMenuOptions<C>>); readonly inline_keyboard: []; } export {};