@grammyjs/conversations
Version:
Conversational interfaces for grammY
594 lines (593 loc) • 25.6 kB
TypeScript
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 {};