### Basic Stateless Question Setup Source: https://github.com/grammyjs/stateless-question/blob/main/README.md Initialize a StatelessQuestion instance and register its middleware. This example sets up a question about unicorns. ```typescript import {StatelessQuestion} from '@grammyjs/stateless-question'; const bot = new Bot(token); const unicornQuestion = new StatelessQuestion('unicorns', ctx => { console.log('User thinks unicorns are doing:', ctx.message) }) // Dont forget to use the middleware bot.use(unicornQuestion.middleware()) ``` -------------------------------- ### Basic StatelessQuestion Setup Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Set up a basic StatelessQuestion middleware for your bot. This example initializes the question handler and applies its middleware to the bot. ```typescript import {StatelessQuestion} from '@grammyjs/stateless-question'; import {Bot} from 'grammy'; const bot = new Bot('TOKEN'); const q = new StatelessQuestion('id', (ctx, state) => { console.log(ctx.message.text); }); bot.use(q.middleware()); bot.start(); ``` -------------------------------- ### Install grammyjs/stateless-question Source: https://github.com/grammyjs/stateless-question/blob/main/README.md Install the library using npm. This command installs both grammy and the stateless-question package. ```bash npm install grammy @grammyjs/stateless-question ``` -------------------------------- ### Basic Usage Pattern Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt A fundamental example demonstrating how to set up and use the StatelessQuestion for a single, straightforward question. ```typescript import { Bot } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; const bot = new Bot("YOUR_BOT_TOKEN"); const question = new StatelessQuestion( "ask-name", async (ctx, answer) => { await ctx.reply(`Hello, ${answer}!`); } ); bot.use(question.middleware()); bot.command("start", async (ctx) => { await ctx.reply("What is your name?", { reply_markup: { keyboard: [["Alice", "Bob", "Charlie"]], one_time_keyboard: true, }, }); }); bot.start(); ``` -------------------------------- ### AnswerFunction Example Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/types.md Illustrates how to implement both synchronous and asynchronous AnswerFunction handlers. Shows instantiation of StatelessQuestion with a handler. ```typescript import {StatelessQuestion, type AnswerFunction} from '@grammyjs/stateless-question'; import {type Context} from 'grammy'; // Synchronous answer function const syncHandler: AnswerFunction = (ctx, state) => { console.log(`Got answer in state ${state}: ${ctx.message.text}`); }; // Asynchronous answer function const asyncHandler: AnswerFunction = async (ctx, state) => { await ctx.reply(`Thank you for your input in ${state}`); }; const question = new StatelessQuestion('feedback', asyncHandler); ``` -------------------------------- ### Testing Example with Bot Mock Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Provides an example of how to test StatelessQuestion functionality using a mock bot instance. This is essential for unit testing your bot's logic. ```typescript import { Bot, Context, Keyboard } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; import { mock } from "@grammyjs/mock"; // Mock context type interface MyMockContext extends Context { session: Record; } async function runTest() { const bot = new Bot("DUMMY_TOKEN"); const question = new StatelessQuestion( "test-question", async (ctx, answer) => { await ctx.reply(`You said: ${answer}`); } ); bot.use(question.middleware()); // Use the mock adapter const mockAdapter = mock(bot); // Simulate a user sending a message to trigger the question await mockAdapter.command("start"); // Simulate a user answering the question await mockAdapter.message("My Answer"); // Assert that the bot replied correctly expect(mockAdapter.messages()).toEqual([ "What is your favorite color?", // Assuming this was the question text "You said: My Answer", ]); } // runTest(); // Call the test function ``` -------------------------------- ### Basic Usage of StatelessQuestion Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/README.md Demonstrates the basic setup for using StatelessQuestion, including creating a question, registering its middleware, and sending the question to a user. ```typescript import {StatelessQuestion} from '@grammyjs/stateless-question'; import {Bot, type Context} from 'grammy'; const bot = new Bot('YOUR_BOT_TOKEN'); // Step 1: Create a question const locationQuestion = new StatelessQuestion( 'ask_location', (ctx, state) => { console.log(`Location for ${state}: ${ctx.message.text}`); } ); // Step 2: Register middleware bot.use(locationQuestion.middleware()); // Step 3: Send the question bot.command('ask', async (ctx) => { await locationQuestion.replyWithHTML( ctx, 'Where are you?', 'user_123' // Optional state ); }); bot.start(); ``` -------------------------------- ### StatelessQuestion Constructor and Middleware Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Demonstrates the basic instantiation of the StatelessQuestion class and its middleware usage. This is the fundamental setup for using the library. ```typescript import { Bot } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; // Create a new bot instance const bot = new Bot("YOUR_BOT_TOKEN"); // Create a new StatelessQuestion instance const question = new StatelessQuestion( "unique-question-identifier", async (ctx, answer) => { await ctx.reply(answer); } ); // Use the middleware bot.use(question.middleware()); // ... rest of your bot logic ``` -------------------------------- ### Basic Stateless Question Example Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/START_HERE.md This snippet demonstrates how to create, register, and use a stateless question with the @grammyjs/stateless-question library. It includes setting up the bot, defining the question, registering its middleware, and handling a command to ask the question. ```typescript import { StatelessQuestion } from '@grammyjs/stateless-question'; import {Bot} from 'grammy'; const bot = new Bot('TOKEN'); // Create question const question = new StatelessQuestion( 'my_question', (ctx, state) => { console.log('User answered:', ctx.message.text); } ); // Register middleware bot.use(question.middleware()); // Send question bot.command('ask', async (ctx) => { await question.replyWithHTML(ctx, 'What is your name?'); }); bot.start(); ``` -------------------------------- ### Replying with Stateless Question Source: https://github.com/grammyjs/stateless-question/blob/main/README.md Use the question instance to reply to a user. This example shows how to dynamically set the question text based on user language. ```typescript bot.command('rainbows', async ctx => { let text if (ctx.session.language === 'de') { text = 'Was machen Einhörner?' } else { text = 'What are unicorns doing?' } return unicornQuestion.replyWithMarkdown(ctx, text) ``` -------------------------------- ### Core Exports from @grammyjs/stateless-question Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/INDEX.md This snippet shows the main exports from the library, including the StatelessQuestion class and related types. Ensure you have grammy installed as a peer dependency. ```typescript // From @grammyjs/stateless-question export class StatelessQuestion { ... } export type AnswerFunction = ... export type ReplyToMessageContext = ... export type UrlMessageEntity = ... ``` -------------------------------- ### Handling Markdown Errors Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Illustrates how to handle errors that occur due to incorrect Markdown formatting. This example shows a strategy for preventing and managing such errors. ```typescript // Assume 'ctx' is your context object // Assume 'question' is an instance of StatelessQuestion try { // Attempt to send a message with potentially invalid Markdown await ctx.reply( "This is a message with *invalid* Markdown.", { parse_mode: "MarkdownV2" } // Or "Markdown" ); } catch (error) { // Check if the error is a Markdown parsing error if (error.message.includes("Markdown parse error")) { console.error("Markdown parsing failed. Please check your formatting."); // Implement error handling strategy, e.g., reply with a user-friendly message await ctx.reply("Sorry, I couldn't format that message correctly. Please try again later."); } else { // Handle other potential errors console.error("An unexpected error occurred:", error); } } ``` -------------------------------- ### Sequential Questions (State Machine Pattern) Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Implements a state machine pattern using StatelessQuestion to guide users through a series of dependent questions. Each question's answer determines the next step. ```typescript import { Bot } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; const bot = new Bot("YOUR_BOT_TOKEN"); const question1 = new StatelessQuestion( "step1", async (ctx, answer) => { await ctx.reply(`Step 1 answer: ${answer}. Now for step 2.`); // Trigger the next question await askStep2(ctx); } ); const question2 = new StatelessQuestion( "step2", async (ctx, answer) => { await ctx.reply(`Step 2 answer: ${answer}. Process complete.`); } ); bot.use(question1.middleware()); bot.use(question2.middleware()); async function askStep1(ctx: MyContext) { await ctx.reply("Enter value for Step 1:", { reply_markup: { keyboard: [["Value A", "Value B"]], one_time_keyboard: true, }, }); } async function askStep2(ctx: MyContext) { await ctx.reply("Enter value for Step 2:", { reply_markup: { keyboard: [["Option X", "Option Y"]], one_time_keyboard: true, }, }); } bot.command("start", async (ctx) => { await askStep1(ctx); }); bot.start(); ``` -------------------------------- ### Example: No Error with MarkdownV2 and Parenthesis Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/errors.md Shows that MarkdownV2 correctly handles closing parentheses within the identifier and additional state, preventing errors. ```typescript const question = new StatelessQuestion('survey)', (ctx, state) => { // same identifier with ), but MarkdownV2 handles it fine console.log(state); }); // No error - MarkdownV2 escapes parentheses await question.replyWithMarkdownV2(ctx, 'Survey question', 'answer)2'); ``` -------------------------------- ### Example: Markdown Error with Parenthesis in Additional State Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/errors.md Demonstrates the error thrown when `replyWithMarkdown` is called with an `additionalState` containing a closing parenthesis. ```typescript const question = new StatelessQuestion('survey', (ctx, state) => { console.log(state); }); // This throws because additionalState contains ) try { await question.replyWithMarkdown(ctx, 'Rate this!', 'score_5)'); } catch (error) { // Error: Markdown does not work with a stateless-question identifier // or additionalState containing a close bracket `)`. // Use MarkdownV2 or HTML. } ``` -------------------------------- ### Debugging: Check if Question Matches Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md This setup helps debug message handling by logging whether a question was matched or if no question matched. It's useful for verifying that your middleware is correctly registered and processing messages. ```typescript const q = new StatelessQuestion('my_q', (ctx, state) => { console.log('Got answer!', ctx.message.text); }); bot.use(q.middleware()); bot.use(ctx => { console.log('No question matched'); }); ``` -------------------------------- ### Example: Markdown Error with Parenthesis in Identifier Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/errors.md Illustrates the error that occurs when `replyWithMarkdown` is used with a question identifier that contains a closing parenthesis. ```typescript const question = new StatelessQuestion('survey)', (ctx, state) => { // identifier contains ), triggers error when using Markdown console.log(state); }); try { await question.replyWithMarkdown(ctx, 'Survey question'); // Throws immediately because identifier contains ) } catch (error) { console.log('Use MarkdownV2 instead'); } ``` -------------------------------- ### Stateless Menu Integration Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Demonstrates how to integrate StatelessQuestion with a menu system, allowing users to navigate options and trigger questions from a menu interface. ```typescript import { Bot, Keyboard } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; const bot = new Bot("YOUR_BOT_TOKEN"); const question = new StatelessQuestion( "menu-question", async (ctx, answer) => { await ctx.reply(`You chose: ${answer}`); } ); bot.use(question.middleware()); function getMainKeyboard() { return new Keyboard() .text("Ask Question 1") .text("Ask Question 2") .resized(); } bot.command("start", async (ctx) => { await ctx.reply("Welcome! Choose an option:", { reply_markup: getMainKeyboard(), }); }); bot.hears("Ask Question 1", async (ctx) => { await ctx.reply("What is your favorite color?", { reply_markup: new Keyboard([["Red", "Blue"]]).oneTime().resized(), }); }); bot.hears("Ask Question 2", async (ctx) => { await ctx.reply("What is your favorite fruit?", { reply_markup: new Keyboard([["Apple", "Banana"]]).oneTime().resized(), }); }); bot.start(); ``` -------------------------------- ### Import StatelessQuestion and AnswerFunction Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/README.md Import the main StatelessQuestion class and the AnswerFunction type from the library. ```typescript import {StatelessQuestion, type AnswerFunction} from '@grammyjs/stateless-question'; ``` -------------------------------- ### Instantiate StatelessQuestion Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/api-reference/StatelessQuestion.md Create a new StatelessQuestion instance. Provide a unique identifier for the question and a callback function to handle user replies. The callback receives the context and any additional state encoded in the reply. ```typescript import {StatelessQuestion} from '@grammyjs/stateless-question'; import {Bot, type Context} from 'grammy'; const bot = new Bot('YOUR_BOT_TOKEN'); const locationQuestion = new StatelessQuestion( 'ask_location', (ctx, additionalState) => { console.log(`User at ${additionalState} says: ${ctx.message.text}`); } ); ``` -------------------------------- ### Combining with Inline Keyboards Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Demonstrates how to use StatelessQuestion in conjunction with inline keyboards. This allows for more interactive and complex user flows. ```typescript import { Bot, InlineKeyboard } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; const bot = new Bot("YOUR_BOT_TOKEN"); const question = new StatelessQuestion( "ask-with-inline", async (ctx, answer) => { await ctx.reply(`You selected: ${answer}`); } ); bot.use(question.middleware()); bot.command("start", async (ctx) => { const inlineKeyboard = new InlineKeyboard() .text("Choice A", "choice_a") .text("Choice B", "choice_b"); await ctx.reply("Please choose an option:", { reply_markup: inlineKeyboard, }); }); // Handle inline keyboard button presses that trigger the question bot.on("callback_query:data", async (ctx) => { const data = ctx.callbackQuery.data; if (data === "choice_a" || data === "choice_b") { // Use the StatelessQuestion to handle the answer await question.replyToChat(ctx, data); // Assuming replyToChat is the correct method } await ctx.answerCallbackQuery(); // Acknowledge the callback query }); bot.start(); ``` -------------------------------- ### Stateless Question with Additional State Source: https://github.com/grammyjs/stateless-question/blob/main/README.md Initialize a StatelessQuestion with an additional state parameter. This allows storing context-specific information with the question. ```javascript const locationQuestion = new StatelessQuestion( "target", (ctx, additionalState) => { console.log("Location of", additionalState, "is", ctx.message.text); saveHeroLocation(additionalState, ctx.message.text); }, ); // Dont forget to use the middleware bot.use(locationQuestion.middleware()); ``` -------------------------------- ### Get Message Suffix for Reply Markup Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Generate a suffix string to append to messages when using `force_reply` to ensure the question is correctly handled. Supports HTML, Markdown, and MarkdownV2, with Markdown having limitations. ```typescript // HTML suffix const html = q.messageSuffixHTML(); const html_state = q.messageSuffixHTML('state'); // Markdown suffix (throws on ') const md = q.messageSuffixMarkdown(); const md_state = q.messageSuffixMarkdown('state'); // MarkdownV2 suffix (always works) const mdv2 = q.messageSuffixMarkdownV2(); const mdv2_state = q.messageSuffixMarkdownV2('state)'); // Manual send await ctx.replyWithHTML(text + q.messageSuffixHTML(), { parse_mode: 'HTML', reply_markup: {force_reply: true} }); ``` -------------------------------- ### StatelessQuestion Constructor Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/api-reference/StatelessQuestion.md Initializes a new StatelessQuestion instance. This class is used to create stateless questions in Telegram bots, encoding question identifiers in zero-width characters within message URLs to respect user privacy. ```APIDOC ## StatelessQuestion Constructor ### Description Initializes a new StatelessQuestion instance. This class is used to create stateless questions in Telegram bots, encoding question identifiers in zero-width characters within message URLs to respect user privacy. ### Parameters #### Parameters - **uniqueIdentifier** (string) - Required - A unique identifier for this question within the bot. Used to identify replies to this question. Will be URL-encoded automatically. - **answer** (AnswerFunction) - Required - Callback function invoked when a reply to this question is received. Receives the context and optional additional state. ### Example ```typescript import {StatelessQuestion} from '@grammyjs/stateless-question'; import {Bot, type Context} from 'grammy'; const bot = new Bot('YOUR_BOT_TOKEN'); const locationQuestion = new StatelessQuestion( 'ask_location', (ctx, additionalState) => { console.log(`User at ${additionalState} says: ${ctx.message.text}`); } ); ``` ``` -------------------------------- ### Custom Middleware Chain Ordering Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Explains the importance of middleware order and how to correctly place the StatelessQuestion middleware within the bot's chain to ensure proper functionality. ```typescript import { Bot } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; const bot = new Bot("YOUR_BOT_TOKEN"); const question = new StatelessQuestion( "unique-id", async (ctx, answer) => { await ctx.reply(answer); } ); // IMPORTANT: Middleware order matters. // Place StatelessQuestion middleware *after* session or other middleware // that might modify the context, but *before* handlers that rely on it. bot.use(async (ctx, next) => { // Example: Custom middleware before StatelessQuestion console.log("Processing message..."); await next(); }); bot.use(question.middleware()); // StatelessQuestion middleware bot.use(async (ctx, next) => { // Example: Handler middleware after StatelessQuestion if (ctx.message?.text === "/status") { await ctx.reply("Bot is running."); } await next(); }); bot.start(); ``` -------------------------------- ### Unique Identifiers for Questions Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/README.md Illustrates how to create multiple StatelessQuestion instances, each with a unique identifier to distinguish them. ```typescript const question1 = new StatelessQuestion('color_question', handler); const question2 = new StatelessQuestion('size_question', handler); ``` -------------------------------- ### Handling Different Parse Modes Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md Demonstrates how to use different Markdown and HTML parse modes for sending questions. `replyWithMarkdownV2` is recommended for states containing parentheses, ensuring proper parsing. ```typescript const question = new StatelessQuestion('format', handler); bot.use(question.middleware()); bot.command('markdown', async (ctx) => { // Safe - no parentheses await question.replyWithMarkdown(ctx, 'Your **bold** question'); }); bot.command('html', async (ctx) => { // Safer for arbitrary content await question.replyWithHTML(ctx, 'Your bold question'); }); bot.command('markdownv2', async (ctx) => { // Always works, even with parentheses in state await question.replyWithMarkdownV2(ctx, 'Your _italic_ question', 'state)with)parens'); }); ``` -------------------------------- ### Language-Specific Questions Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md Illustrates how to use a single question handler for multiple languages by dynamically setting the question text based on the user's session language. This ensures a localized user experience. ```typescript const feedbackQuestion = new StatelessQuestion( 'feedback', (ctx, state) => { console.log('Feedback:', ctx.message.text); } ); bot.use(feedbackQuestion.middleware()); bot.command('feedback', async (ctx) => { const text = ctx.session.language === 'de' ? 'Bitte geben Sie Ihr Feedback' : 'Please provide your feedback'; await feedbackQuestion.replyWithMarkdown(ctx, text); }); ``` -------------------------------- ### Multiple Independent Questions Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Shows how to manage multiple, unrelated questions using separate StatelessQuestion instances. Each instance has its own identifier and answer handler. ```typescript import { Bot } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; const bot = new Bot("YOUR_BOT_TOKEN"); const question1 = new StatelessQuestion( "ask-color", async (ctx, answer) => { await ctx.reply(`Your favorite color is ${answer}.`); } ); const question2 = new StatelessQuestion( "ask-food", async (ctx, answer) => { await ctx.reply(`Your favorite food is ${answer}.`); } ); bot.use(question1.middleware()); bot.use(question2.middleware()); bot.command("start", async (ctx) => { await ctx.reply("What is your favorite color?", { reply_markup: { keyboard: [["Red", "Blue", "Green"]], one_time_keyboard: true, }, }); }); bot.command("food", async (ctx) => { await ctx.reply("What is your favorite food?", { reply_markup: { keyboard: [["Pizza", "Burger", "Salad"]], one_time_keyboard: true, }, }); }); bot.start(); ``` -------------------------------- ### Pattern: Language-Aware Questions Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Create questions that adapt their text based on the user's language preference, typically stored in the bot's session. This ensures a localized user experience. ```typescript const q = new StatelessQuestion('q', (ctx, state) => { console.log(ctx.message.text); }); bot.use(q.middleware()); bot.command('ask', async ctx => { const text = ctx.session.language === 'de' ? 'Frage?' : 'Question?'; await q.replyWithHTML(ctx, text); }); ``` -------------------------------- ### middleware() Method Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/api-reference/StatelessQuestion.md Returns a grammy middleware function that detects replies to this question and invokes the answer callback. This middleware should be registered with `bot.use()` to handle incoming messages. ```APIDOC ## middleware() Method ### Description Returns a grammy middleware function that detects replies to this question and invokes the answer callback. This middleware should be registered with `bot.use()` to handle incoming messages. ### Parameters None. ### Returns A middleware function compatible with `bot.use()`. The middleware checks if the incoming message is a reply to one of this question's messages. If yes, it calls the answer function and returns. If no, it calls `next()` to pass control to subsequent middleware. ### Example ```typescript const question = new StatelessQuestion('unicorns', (ctx, state) => { console.log('User replied:', ctx.message.text); }); bot.use(question.middleware()); bot.command('ask', async (ctx) => { await question.replyWithMarkdown(ctx, 'What are unicorns doing?'); }); ``` ``` -------------------------------- ### Using Multiple StatelessQuestions Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Configure and use multiple StatelessQuestion instances for different conversational flows within the same bot. Each question is assigned a unique ID and its own handler. ```typescript const q1 = new StatelessQuestion('q1', h1); const q2 = new StatelessQuestion('q2', h2); bot.use(q1.middleware()); bot.use(q2.middleware()); bot.command('ask1', ctx => q1.replyWithHTML(ctx, '?')); bot.command('ask2', ctx => q2.replyWithHTML(ctx, '?')); ``` -------------------------------- ### Encoding Additional State with Questions Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/README.md Shows how to include additional, context-specific state with a question and how this state is received by the answer function. ```typescript // Send different questions for different contexts await question.replyWithHTML(ctx, 'Pick a room', 'building_A'); await question.replyWithHTML(ctx, 'Pick a room', 'building_B'); // The answer function receives the state (ctx, state) => { console.log(`User picked room for: ${state}`); // 'building_A' or 'building_B' } ``` -------------------------------- ### Multiple Independent Questions Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md Demonstrates how to manage multiple independent questions by assigning each a unique identifier and registering their respective middlewares. This allows for distinct question flows within the same bot. ```typescript const colorQuestion = new StatelessQuestion('color', (ctx, state) => { console.log('Color:', ctx.message.text); }); const sizeQuestion = new StatelessQuestion('size', (ctx, state) => { console.log('Size:', ctx.message.text); }); bot.use(colorQuestion.middleware()); bot.use(sizeQuestion.middleware()); bot.command('colors', async (ctx) => { await colorQuestion.replyWithHTML(ctx, 'What is your favorite color?'); }); bot.command('sizes', async (ctx) => { await sizeQuestion.replyWithHTML(ctx, 'What is your preferred size?'); }); ``` -------------------------------- ### StatelessQuestion with Context Session Data Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md Shows how to integrate StatelessQuestion with grammy's context session data. The question handler can access and utilize session properties like `userId` and `userName`. ```typescript interface MyContext extends Context { session: { userId: number; userName: string; }; } const question = new StatelessQuestion( 'favorite_food', (ctx, state) => { console.log(`${ctx.session.userName} likes: ${ctx.message.text}`); } ); bot.use(question.middleware()); bot.command('food', async (ctx) => { await question.replyWithHTML(ctx, 'What is your favorite food?'); }); ``` -------------------------------- ### Import Statement with Custom Context Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/api-reference/exports.md Illustrates importing StatelessQuestion and AnswerFunction, and how to use them with a custom Context type that extends the base Grammy Context. ```typescript import {StatelessQuestion, type AnswerFunction} from '@grammyjs/stateless-question'; import {type Context} from 'grammy'; interface MyContext extends Context { session: {userId: number; language: string}; } const handler: AnswerFunction = (ctx, state) => { // ctx is typed as MyContext with session console.log(ctx.session.userId); }; ``` -------------------------------- ### Sequential Questions (State Machine Pattern) Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md Implements a state machine pattern using multiple StatelessQuestions chained together. The state from one question can be passed to the next, enabling multi-step interactions. ```typescript const nameQuestion = new StatelessQuestion( 'name_question', (ctx, state) => { const name = ctx.message.text; // Proceed to next question with name as state console.log(`Name: ${name}`); } ); const ageQuestion = new StatelessQuestion( 'age_question', (ctx, state) => { const name = state; // Name from previous question const age = ctx.message.text; console.log(`${name} is ${age} years old`); } ); bot.use(nameQuestion.middleware()); bot.use(ageQuestion.middleware()); bot.command('signup', async (ctx) => { await nameQuestion.replyWithHTML(ctx, 'What is your name?', ''); }); // Inside nameQuestion handler, trigger ageQuestion: // await ageQuestion.replyWithHTML(ctx, 'How old are you?', name); ``` -------------------------------- ### Using Additional State with StatelessQuestion Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md Shows how to encode context-specific data, such as an event ID, within the question using the `state` parameter. This state is then available in the answer handler. ```typescript const eventQuestion = new StatelessQuestion( 'event_location', (ctx, state) => { console.log(`User location for event ${state}: ${ctx.message.text}`); // state contains the event ID } ); bot.use(eventQuestion.middleware()); bot.command('event', async (ctx) => { const eventId = ctx.match; // e.g., /event paris await eventQuestion.replyWithHTML( ctx, 'Where should we meet for the event?', eventId // Encoded in the question ); }); ``` -------------------------------- ### Interleaving Middleware in Custom Chains Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md Demonstrates how to integrate StatelessQuestion middleware into a custom middleware chain. The order of middleware determines processing priority, allowing for flexible request handling. ```typescript const question1 = new StatelessQuestion('q1', handler1); const question2 = new StatelessQuestion('q2', handler2); // Order matters - first middleware to match processes the update bot.use(question1.middleware()); bot.use(question2.middleware()); bot.use(loggingMiddleware); bot.use(commandHandler); // question1 has highest priority // If no questions match, falls through to logging and command handler ``` -------------------------------- ### Import Statement with Context Type Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/api-reference/exports.md Demonstrates how to import StatelessQuestion and AnswerFunction along with the base Context type from the 'grammy' package. ```typescript import {StatelessQuestion, type AnswerFunction} from '@grammyjs/stateless-question'; import {type Context} from 'grammy'; const handler: AnswerFunction = (ctx, state) => { // ctx is typed as Context }; ``` -------------------------------- ### Common Mistake: Forgetting Middleware Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Ensure you register the StatelessQuestion middleware with `bot.use()` to enable it to handle incoming messages. Forgetting this step will prevent the question from being answered. ```typescript const q = new StatelessQuestion('id', handler); // Missing: bot.use(q.middleware()); ``` -------------------------------- ### Pattern: Sequential Questions Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Implement a sequence of questions where the answer to one question is used to prompt the next. This pattern manages conversational flow by passing state between questions. ```typescript const q1 = new StatelessQuestion('step1', async (ctx, state) => { const name = ctx.message.text; await q2.replyWithHTML(ctx, 'Age?', name); }); const q2 = new StatelessQuestion('step2', (ctx, state) => { const name = state; const age = ctx.message.text; console.log(`${name}: ${age}`); }); bot.use(q1.middleware()); bot.use(q2.middleware()); bot.command('start', ctx => q1.replyWithHTML(ctx, 'Name?')); ``` -------------------------------- ### Send Questions with Different Formats Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Send questions to the user using different formatting options like HTML, Markdown, and MarkdownV2. Choose the format based on safety and requirements regarding special characters. ```typescript await q.replyWithHTML(ctx, 'Q?') ``` ```typescript await q.replyWithMarkdown(ctx, 'Q?') ``` ```typescript await q.replyWithMarkdownV2(ctx, 'Q?') ``` -------------------------------- ### Language-Specific Questions Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Illustrates how to handle questions that require different responses or logic based on the user's language. This involves checking the user's language and potentially using different question identifiers. ```typescript import { Bot } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; const bot = new Bot("YOUR_BOT_TOKEN"); const questionEn = new StatelessQuestion( "ask-greeting-en", async (ctx, answer) => { await ctx.reply(`Hello, ${answer}!`); } ); const questionEs = new StatelessQuestion( "ask-greeting-es", async (ctx, answer) => { await ctx.reply(`Hola, ${answer}!`); } ); bot.use(questionEn.middleware()); bot.use(questionEs.middleware()); bot.command("start", async (ctx) => { const userLang = ctx.from?.language_code; if (userLang && userLang.startsWith("es")) { await ctx.reply("¿Cómo te llamas?", { reply_markup: { keyboard: [["Juan", "Maria"]], one_time_keyboard: true, }, }); } else { await ctx.reply("What is your name?", { reply_markup: { keyboard: [["Alice", "Bob"]], one_time_keyboard: true, }, }); } }); bot.start(); ``` -------------------------------- ### Test Answer Handler with Mocked Context Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md This snippet demonstrates how to test the answer handler of a StatelessQuestion by mocking the context and simulating a reply to a question message. It verifies that the answer text and state are correctly captured. ```typescript import {Bot, type Context} from 'grammy'; import {StatelessQuestion} from '@grammyjs/stateless-question'; import {test} from 'node:test'; import {strictEqual} from 'node:assert'; await test('answer function is called with state', async (t) => { const answers: Array<{text: string; state: string}> = []; const question = new StatelessQuestion('test_q', (ctx, state) => { answers.push({text: ctx.message.text, state}); }); const bot = new Bot('123:ABC'); (bot as any).botInfo = {}; bot.use(question.middleware()); // Simulate reply to a question message await bot.handleUpdate({ update_id: 1, message: { message_id: 1, date: Date.now(), chat: {id: 1, type: 'private'}, from: {id: 1, is_bot: false, first_name: 'User'}, text: 'My answer', reply_to_message: { message_id: 0, date: Date.now(), chat: {id: 1, type: 'private'}, from: {id: 2, is_bot: true, first_name: 'Bot'}, text: 'Question', entities: [{ type: 'text_link', url: 'http://t.me/#test_q#my_state', offset: 0, length: 4, }], }, }, }); strictEqual(answers.length, 1); strictEqual(answers[0].text, 'My answer'); strictEqual(answers[0].state, 'my_state'); }); ``` -------------------------------- ### Manually Sending Stateless Question (HTML) Source: https://github.com/grammyjs/stateless-question/blob/main/README.md Manually construct the reply message including the question suffix for HTML parsing. Ensure `force_reply` is enabled. ```typescript bot.command('unicorn', async ctx => ctx.replyWithHTML( 'What are unicorns doing?' + unicornQuestion.messageSuffixHTML(), {parse_mode: 'HTML', reply_markup: {force_reply: true}}) ``` -------------------------------- ### replyWithMarkdownV2() Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/api-reference/StatelessQuestion.md Sends a message with the question suffix using MarkdownV2 parse mode and automatic ForceReply. This is the recommended method for Markdown formatting as it handles escaping of special characters, including parentheses, within the additional state. ```APIDOC ## replyWithMarkdownV2() ### Description Send a message with the question suffix using MarkdownV2 parse mode and automatic ForceReply. ### Method `async replyWithMarkdownV2(context: BaseContext, text: string, additionalState?: string): Promise` ### Parameters #### Path Parameters - `context` (BaseContext) - Yes - The grammy context object. - `text` (string) - Yes - The question text to send. - `additionalState` (string) - No - `''` - Optional state to attach. Parentheses are escaped automatically. ### Returns `Promise` — The sent message object. ### Example ```typescript const question = new StatelessQuestion('event)', handler); bot.use(question.middleware()); bot.command('event', async (ctx) => { await question.replyWithMarkdownV2(ctx, 'Where is the event?', 'party)2024'); }); ``` ``` -------------------------------- ### Manually Sending Stateless Question (Markdown) Source: https://github.com/grammyjs/stateless-question/blob/main/README.md Manually construct the reply message including the question suffix for Markdown parsing. Ensure `force_reply` is enabled. ```typescript bot.command('unicorn', async ctx => ctx.replyWithMarkdown('What are unicorns doing?' + unicornQuestion.messageSuffixMarkdown(), {parse_mode: 'Markdown', reply_markup: {force_reply: true}}) ``` -------------------------------- ### Stateless Menu Integration with Identifiers Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md Embeds StatelessQuestion identifiers within menu items for stateless menu libraries. This allows the bot to track menu choices and navigate user interactions without maintaining server-side state. ```typescript const choiceQuestion = new StatelessQuestion( 'menu_choice', (ctx, state) => { // state contains the menu path console.log(`Menu choice in: ${state}`); } ); bot.use(choiceQuestion.middleware()); bot.command('menu', async (ctx) => { const menuPath = 'main_menu'; await ctx.replyWithHTML( 'Choose an option:\n' + '1️⃣ Option A' + choiceQuestion.messageSuffixHTML(menuPath + '/a') + '\n' + '2️⃣ Option B' + choiceQuestion.messageSuffixHTML(menuPath + '/b'), { reply_markup: {force_reply: true}, parse_mode: 'HTML', } ); }); ``` -------------------------------- ### StatelessQuestion Class Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/api-reference/exports.md The StatelessQuestion class is used to create handlers that can reply to messages with a specific identifier and manage additional state. ```APIDOC ## Class: StatelessQuestion ### Description Represents a stateless question handler that allows replying to messages and managing additional state. ### Constructor ```typescript new StatelessQuestion(uniqueIdentifier: string, answer: AnswerFunction) ``` - **uniqueIdentifier** (string) - A unique string to identify this question. - **answer** (AnswerFunction) - The function to be called when the question is answered. ### Properties - **uniqueIdentifier** (string) - The unique identifier for this question. ### Methods - **middleware()**: Returns a middleware function that can be used with Grammy.js. - **messageSuffixHTML(additionalState?: string)**: Generates an HTML suffix for messages. - **messageSuffixMarkdown(additionalState?: string)**: Generates a Markdown suffix for messages. - **messageSuffixMarkdownV2(additionalState?: string)**: Generates a MarkdownV2 suffix for messages. - **replyWithHTML(context: BaseContext, text: string, additionalState?: string)**: Replies to the context with HTML formatted text. - **replyWithMarkdown(context: BaseContext, text: string, additionalState?: string)**: Replies to the context with Markdown formatted text. - **replyWithMarkdownV2(context: BaseContext, text: string, additionalState?: string)**: Replies to the context with MarkdownV2 formatted text. ``` -------------------------------- ### Error Cases with Markdown Formatting Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Illustrates scenarios where Markdown formatting can cause errors due to special characters like parentheses, and how MarkdownV2 or HTML can be used as alternatives. ```typescript // ❌ Throws: Markdown with ) in identifier q.messageSuffixMarkdown(); // if id contains ) // ❌ Throws: Markdown with ) in state q.replyWithMarkdown(ctx, 'Q?', 'state)'); // ✅ Works: MarkdownV2 with ) in identifier q.messageSuffixMarkdownV2(); // even if id contains ) // ✅ Works: MarkdownV2 with ) in state q.replyWithMarkdownV2(ctx, 'Q?', 'state)'); // ✅ Works: HTML with ) anywhere q.messageSuffixHTML('state)'); ``` -------------------------------- ### Handling Different Parse Modes Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Explains how to configure the StatelessQuestion to work with different message parse modes like HTML or Markdown. This is crucial for correctly rendering formatted text. ```typescript import { Bot } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; const bot = new Bot("YOUR_BOT_TOKEN"); // Example using MarkdownV2 const questionMarkdown = new StatelessQuestion( "ask-markdown", async (ctx, answer) => { await ctx.reply(answer, { parse_mode: "MarkdownV2" }); } ); bot.use(questionMarkdown.middleware()); bot.command("md", async (ctx) => { await ctx.reply("Enter some *bold* text:", { reply_markup: { keyboard: [["*bold* text"]], one_time_keyboard: true, }, }); }); // Example using HTML const questionHtml = new StatelessQuestion( "ask-html", async (ctx, answer) => { await ctx.reply(answer, { parse_mode: "HTML" }); } ); bot.use(questionHtml.middleware()); bot.command("html", async (ctx) => { await ctx.reply("Enter some bold text:", { reply_markup: { keyboard: [["bold text"]], one_time_keyboard: true, }, }); }); bot.start(); ``` -------------------------------- ### Combining Force Reply with Inline Keyboards Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/usage-patterns.md Integrates ForceReply with inline keyboards by sending them in separate messages. This allows for both direct text input and button-based choices within the same interaction flow. ```typescript const question = new StatelessQuestion( 'survey_q1', (ctx, state) => { console.log('Answer:', ctx.message.text); } ); bot.use(question.middleware()); bot.command('survey', async (ctx) => { // Question with ForceReply await question.replyWithHTML(ctx, 'Rate our service (1-5)?'); // Options in separate message with inline keyboard await ctx.replyWithHTML( 'Or choose from options:', { reply_markup: { inline_keyboard: [ [{text: '1 - Poor', callback_data: 'rating_1'}], [{text: '5 - Excellent', callback_data: 'rating_5'}], ], }, } ); }); ``` -------------------------------- ### Markdown Format for Invisible Link Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/implementation-details.md This Markdown snippet demonstrates creating an invisible link using standard Markdown syntax. A zero-width non-joiner character serves as the link text. A limitation exists where the URL cannot contain the ')' character. ```markdown [​](http://t.me/#identifier#state) ``` -------------------------------- ### StatelessQuestion with Custom Context and Session Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Integrate StatelessQuestion with custom context types that include session data. The answer handler can then access session properties like `userId`. ```typescript interface Ctx extends Context { session: {userId: number}; } const q = new StatelessQuestion( 'id', (ctx, state) => { // Access session console.log(ctx.session.userId); } ); ``` -------------------------------- ### Using Additional State for Context Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/DOCUMENTATION_SUMMARY.txt Demonstrates how to pass additional state data to the StatelessQuestion. This state can be used within the answer function or to customize the question identifier. ```typescript import { Bot } from "grammy"; import { StatelessQuestion } from "@grammyjs/stateless-question"; const bot = new Bot("YOUR_BOT_TOKEN"); const question = new StatelessQuestion( "ask-city", async (ctx, answer, state) => { // 'state' here would be the additional data passed during setup await ctx.reply(`You live in ${answer}. Your country code is ${state.countryCode}`); } ); bot.use(question.middleware()); bot.command("start", async (ctx) => { const additionalState = { countryCode: "US" }; // Example state await ctx.reply("What city do you live in?", { reply_markup: { keyboard: [["New York", "Los Angeles", "Chicago"]], one_time_keyboard: true, }, }); // Pass the state when initiating the question (if needed, depends on exact API usage) // The example above assumes state is implicitly handled or passed differently. // For explicit state passing, refer to the library's specific methods. }); bot.start(); ``` -------------------------------- ### replyWithMarkdown() Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/api-reference/StatelessQuestion.md Sends a message with the question suffix using Markdown parse mode and automatic ForceReply. Use this when your question text contains Markdown formatting, but be aware of limitations with closing parentheses in the additional state. ```APIDOC ## replyWithMarkdown() ### Description Send a message with the question suffix using Markdown parse mode and automatic ForceReply. ### Method `async replyWithMarkdown(context: BaseContext, text: string, additionalState?: string): Promise` ### Parameters #### Path Parameters - `context` (BaseContext) - Yes - The grammy context object. - `text` (string) - Yes - The question text to send. - `additionalState` (string) - No - `''` - Optional state to attach. Throws if contains `)`. ### Returns `Promise` — The sent message object. ### Throws - `Error` if `additionalState` contains a closing parenthesis `)`. ### Example ```typescript const unicornQuestion = new StatelessQuestion('unicorns', handler); bot.use(unicornQuestion.middleware()); bot.command('rainbows', async (ctx) => { const text = ctx.session.language === 'de' ? 'Was machen Einhörner?' : 'What are unicorns doing?'; await unicornQuestion.replyWithMarkdown(ctx, text); }); ``` ``` -------------------------------- ### StatelessQuestion Constructor Type Signature Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Type signature for the StatelessQuestion constructor, showing the expected types for the unique identifier and the answer handler function. ```typescript new StatelessQuestion( uniqueIdentifier: string, answer: (ctx: ReplyToMessageContext, state: string) => void | Promise ) ``` -------------------------------- ### Common Mistake: Multiple Questions with Same ID Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/quick-reference.md Each StatelessQuestion instance must have a unique ID. Using the same ID for multiple questions will lead to conflicts and unpredictable behavior. ```typescript const q1 = new StatelessQuestion('same_id', handler1); const q2 = new StatelessQuestion('same_id', handler2); // ❌ Conflicts! ``` -------------------------------- ### replyWithHTML() Source: https://github.com/grammyjs/stateless-question/blob/main/_autodocs/api-reference/StatelessQuestion.md Sends a message with the question suffix using HTML parse mode and automatic ForceReply. This method is suitable for questions where the text content is formatted using HTML. ```APIDOC ## replyWithHTML() ### Description Send a message with the question suffix using HTML parse mode and automatic ForceReply. ### Method `async replyWithHTML(context: BaseContext, text: string, additionalState?: string): Promise` ### Parameters #### Path Parameters - `context` (BaseContext) - Yes - The grammy context object. - `text` (string) - Yes - The question text to send. - `additionalState` (string) - No - `''` - Optional state to attach to this question instance. ### Returns `Promise` — A promise resolving to the sent message object. ### Example ```typescript const question = new StatelessQuestion('color', handler); bot.use(question.middleware()); bot.command('colors', async (ctx) => { await question.replyWithHTML(ctx, 'What is your favorite color?', 'user_123'); }); ``` ```