questionaire

package
v0.0.0-...-9c0d265 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 10, 2025 License: MIT Imports: 11 Imported by: 0

README

Questionnaire Component

The questionaire component for tgui allows you to guide a Telegram bot user through a sequence of questions, collect their answers, and process them. It supports text input, single-choice (radio button style), and multiple-choice (checkbox style) questions.

Quick Start

Try the live demo: You can test the questionnaire component right now by messaging @TGUIEXbot and sending /survey.

To run the example locally:

  1. Navigate to /home/joketa/tgui/examples/
  2. The .env file is already configured with a bot token
  3. Run: go run questionaire_example.go
  4. Send /survey to the bot to test the questionnaire functionality
Enhanced Example Features

The included example (questionaire_example.go) showcases various ButtonGrid patterns:

  • Text Input: Name and experience questions with validation
  • Radio Buttons: Age group selection with single column layout
  • 2×2 Grid: Interest selection with organized checkbox layout
  • Quick Choices: Yes/No developer question using QuickChoices
  • Paired Layout: Programming language selection using QuickPairedChoices
  • Custom Rating: Star rating system with custom emoji layout
  • Smart Results: Beautifully formatted result summary with all answers

The example demonstrates 7 different question types and ButtonGrid patterns, making it a comprehensive reference for real-world usage.

Edit Functionality

The questionnaire includes a powerful edit feature that allows users to go back and modify their previous answers:

  • Automatic Edit Buttons: After answering each question, previous questions are automatically updated with an edit button showing the user's answer
  • User-Friendly Display: Edit buttons show human-readable text rather than internal data:
    • Text answers: Shows the actual text entered
    • Radio selections: Shows the selected option text (e.g., "Edit: Under 18")
    • Checkbox selections: Shows the first selection plus count (e.g., "Edit: Technology + 2 more")
  • Smart Reset: When going back to edit, all subsequent questions are automatically cleared and reset
  • Seamless Flow: Users can navigate back and forth through questions without losing progress on earlier steps

Core Concepts

  • Questionaire: The main object that manages a series of questions for a specific chat. It handles the flow, displays questions, and collects answers.
  • Question: Represents an individual question within a Questionaire. It holds the question text, possible choices (if any), the format of the question, and an optional validation function.
  • Manager: A session manager for Questionaire instances. It keeps track of active questionnaires for different chat IDs and routes incoming text messages from users to the appropriate active Questionaire.
  • QuestionFormat: Defines the type of question:
    • QuestionFormatText: User provides a free-text answer.
    • QuestionFormatRadio: User selects one option from a list of buttons.
    • QuestionFormatCheck: User selects one or more options from a list of buttons.

Using ButtonGrid for Choices

The questionnaire supports the powerful ButtonGrid builder pattern for creating clean, organized choice layouts. This is the recommended way to create choices as it provides better maintainability and flexibility than manual button creation.

ButtonGrid Builder Methods
Basic Methods:
  • NewBuilder(): Creates a new ButtonGrid builder
  • Row(): Starts a new row in the grid
  • Add(button.Button): Adds a button to the current row
  • Build(): Returns the final [][]button.Button for use in questions
Convenience Methods:
  • Choice(text): Adds a button with auto-generated callback data
  • ChoiceWithData(text, callbackData): Adds a button with custom callback data
  • SingleChoice(text): Creates a new row with one choice (perfect for radio buttons)
  • SingleChoiceWithData(text, data): Creates a new row with custom callback data
Basic ButtonGrid Usage
import "github.com/jkevinp/tgui/button"

// Simple single choices (radio buttons) - each on its own row
ageChoices := button.NewBuilder().
    SingleChoiceWithData("Under 18", "age_under_18").
    SingleChoiceWithData("18-30", "age_18_30").
    SingleChoiceWithData("31-45", "age_31_45").
    SingleChoiceWithData("Over 45", "age_over_45").
    Build()

// 2x2 grid layout (great for checkboxes)
topicChoices := button.NewBuilder().
    Row().
    ChoiceWithData("Technology", "topic_tech").
    ChoiceWithData("Sports", "topic_sports").
    Row().
    ChoiceWithData("Music", "topic_music").
    ChoiceWithData("Travel", "topic_travel").
    Build()

// 3-column rating scale
ratingChoices := button.NewBuilder().
    Row().
    ChoiceWithData("⭐", "1").
    ChoiceWithData("⭐⭐", "2").
    ChoiceWithData("⭐⭐⭐", "3").
    Row().
    ChoiceWithData("⭐⭐⭐⭐", "4").
    ChoiceWithData("⭐⭐⭐⭐⭐", "5").
    Build()

// Auto-generated callback data (text becomes callback)
simpleChoices := button.NewBuilder().
    Row().
    Choice("Option A").    // callback: "option_a"
    Choice("Option B").    // callback: "option_b"
    Build()
Quick Helper Functions

For common patterns, use these convenient helper functions:

// Simple Yes/No choices (single column)
yesNoChoices := button.QuickChoices("Yes", "No")

// Paired layout (2 per row) - perfect for even number of options
languageChoices := button.QuickPairedChoices(
    "Go", "Python", "JavaScript", "Java", "C++", "Rust")

// Custom callback data with explicit pairs
customChoices := button.QuickChoicesWithData(
    "Display Text", "callback_data",
    "Another Option", "another_callback",
    "Third Option", "third_option")
Advanced Layout Examples
// Mixed row sizes for complex layouts
complexChoices := button.NewBuilder().
    Row().
    ChoiceWithData("Priority 1", "p1").
    Row().
    ChoiceWithData("Normal", "normal").
    ChoiceWithData("Low", "low").
    Row().
    ChoiceWithData("Not Important", "ni").
    Build()

// Emoji-based choices with custom layout
feedbackChoices := button.NewBuilder().
    Row().
    ChoiceWithData("😍 Love it", "love").
    ChoiceWithData("😊 Like it", "like").
    Row().
    ChoiceWithData("😐 Neutral", "neutral").
    Row().
    ChoiceWithData("😞 Dislike", "dislike").
    ChoiceWithData("😡 Hate it", "hate").
    Build()
Complete Example Usage
// Create questionnaire with ButtonGrid choices
q := questionaire.NewBuilder(chatID, manager).
    SetOnDoneHandler(handleResults).
    SetOnCancelHandler(handleCancel)

// Text question (no choices needed)
q.AddQuestion("name", "What's your name?", nil, validateNonEmpty)

// Radio question with ButtonGrid
ageChoices := button.NewBuilder().
    SingleChoiceWithData("Under 18", "age_under_18").
    SingleChoiceWithData("18-30", "age_18_30").
    SingleChoiceWithData("31-45", "age_31_45").
    SingleChoiceWithData("Over 45", "age_over_45").
    Build()
q.AddQuestion("age_group", "Which age group do you belong to?", ageChoices, nil)

// Checkbox question with 2x2 grid
interestChoices := button.NewBuilder().
    Row().
    ChoiceWithData("Technology", "tech").
    ChoiceWithData("Sports", "sports").
    Row().
    ChoiceWithData("Music", "music").
    ChoiceWithData("Travel", "travel").
    Build()
q.AddMultipleAnswerQuestion("interests",
    "Which topics interest you? (Select multiple, then click Done)",
    interestChoices, nil)

// Simple Yes/No with QuickChoices
developerChoices := button.QuickChoices("Yes", "No")
q.AddQuestion("is_developer", "Are you a software developer?", developerChoices, nil)

// Start the questionnaire
q.Show(ctx, b, chatID)
ButtonGrid Benefits & Best Practices
Why Use ButtonGrid?
  1. Cleaner Code: Builder pattern is more readable than manual array creation
  2. Flexible Layouts: Easily create 1×n, 2×2, 3×2, or custom arrangements
  3. Less Boilerplate: Quick helpers eliminate repetitive code
  4. Maintainable: Easy to modify layouts without restructuring arrays
  5. Type Safety: Builder pattern prevents malformed button arrays
  6. Consistent: Standardized approach across your application
Best Practices:

For Radio Questions (Single Selection):

// ✅ Good: Use SingleChoiceWithData for clear single-column layout
choices := button.NewBuilder().
    SingleChoiceWithData("Yes", "yes").
    SingleChoiceWithData("No", "no").
    SingleChoiceWithData("Maybe", "maybe").
    Build()

// ❌ Avoid: Manual creation for simple cases
choices := [][]button.Button{
    {button.Button{Text: "Yes", CallbackData: "yes"}},
    {button.Button{Text: "No", CallbackData: "no"}},
}

For Checkbox Questions (Multiple Selection):

// ✅ Good: Use grid layout for better visual organization
choices := button.NewBuilder().
    Row().
    ChoiceWithData("Tech", "tech").
    ChoiceWithData("Sports", "sports").
    Row().
    ChoiceWithData("Music", "music").
    ChoiceWithData("Travel", "travel").
    Build()

// ✅ Also good: Use QuickPairedChoices for even numbers
choices := button.QuickPairedChoices("Tech", "Sports", "Music", "Travel")

For Rating/Scale Questions:

// ✅ Good: Custom layout for visual appeal
ratingChoices := button.NewBuilder().
    Row().
    ChoiceWithData("1⭐", "1").
    ChoiceWithData("2⭐", "2").
    ChoiceWithData("3⭐", "3").
    Row().
    ChoiceWithData("4⭐", "4").
    ChoiceWithData("5⭐", "5").
    Build()

For Simple Yes/No Questions:

// ✅ Best: Use QuickChoices helper
choices := button.QuickChoices("Yes", "No")

// ✅ Also good: Explicit if you need custom callback data
choices := button.NewBuilder().
    SingleChoiceWithData("Accept", "accept").
    SingleChoiceWithData("Decline", "decline").
    Build()
Layout Guidelines:
  • 1 column: Perfect for radio buttons with many options
  • 2 columns: Great for checkboxes, pairs of options
  • 3+ columns: Use sparingly, only for compact items (emojis, numbers)
  • Mixed layouts: Combine different row sizes for emphasis

Configuration Options

Edit Functionality Control

You can control whether users can edit their previous answers using SetAllowEditAnswers():

// Default behavior - users can edit previous answers
q := questionaire.NewBuilder(chatID, manager).
    SetAllowEditAnswers(true)

// Disable editing - no edit buttons will be shown
q := questionaire.NewBuilder(chatID, manager).
    SetAllowEditAnswers(false)

When SetAllowEditAnswers(false) is used:

  • ✅ Answered questions will show as completed (with checkmark)
  • ❌ No "◀️ Edit" buttons will be displayed
  • ❌ Users cannot go back to modify previous answers
  • ✅ Questionnaire flow becomes linear and faster

This is useful for:

  • Surveys where answers shouldn't be changed (e.g., data collection, polls)
  • Linear workflows where going back would break the logic
  • Faster completion by removing the temptation to second-guess answers
  • Simplified UI with fewer buttons and options

Initialization and Configuration

A Questionaire is created using NewBuilder. You can then chain setter methods to configure it.

// Forward declaration of manager
var manager *questionaire.Manager 
var chatID int64 // Typically from update.Message.Chat.ID
var b *bot.Bot // Your bot instance
ctx := context.Background() // Your context

// Initialize the manager (typically once when your bot starts)
// manager = questionaire.NewManager()

q := questionaire.NewBuilder(chatID, manager).
    SetContext(ctx). // Optional: Set a context for the questionnaire
    SetInitialData(map[string]interface{}{"source": "campaign_xyz"}). // Optional: Pre-fill some data
    SetOnDoneHandler(onSurveyDone). // Handler for when all questions are answered
    SetOnCancelHandler(onSurveyCancel) // Handler for when the user cancels

Setter Methods:

  • (*Questionaire) SetManager(m *Manager) *Questionaire: Sets or updates the manager for this questionnaire.
  • (*Questionaire) SetContext(ctx context.Context) *Questionaire: Sets a parent context for the questionnaire. This context will be merged with the context passed to handlers.
  • (*Questionaire) SetInitialData(data map[string]interface{}) *Questionaire: Provides initial data that will be included in the final answers map.
  • (*Questionaire) SetOnDoneHandler(handler onDoneHandlerFunc) *Questionaire: Sets the callback function to be executed when the questionnaire is successfully completed.
    • Signature: func(ctx context.Context, b *bot.Bot, chatID any, answers map[string]interface{}) error
  • (*Questionaire) SetOnCancelHandler(handler func()) *Questionaire: Sets the callback function for when the questionnaire is explicitly cancelled by the user (e.g., via a "Cancel" button).

Adding Questions

You can add different types of questions to the Questionaire.

  • (*Questionaire) AddQuestion(key string, text string, choices [][]button.Button, validateFunc func(answer string) error) *Questionaire

    • Adds a question that expects a single answer.
    • If choices is nil or empty, it's a QuestionFormatText question.
    • If choices are provided, it's a QuestionFormatRadio question.
    • key: A unique string to identify this question's answer in the results.
    • text: The question text displayed to the user.
    • choices: A 2D slice of tgui/button.Button for radio options. Each inner slice represents a row of buttons. For text questions, pass nil.
    • validateFunc: An optional function to validate the user's answer. It should return nil if valid, or an error if invalid (the error message will be shown to the user).
  • (*Questionaire) AddMultipleAnswerQuestion(key string, text string, choices [][]button.Button, validateFunc func(answer string) error) *Questionaire

    • Adds a QuestionFormatCheck question (checkbox style).
    • choices are mandatory for this type.
    • The validateFunc here would typically validate individual selections if needed, though often validation for checkboxes is about the overall set of choices (handled after completion).

Creating Choices with ButtonGrid (Recommended):

import "github.com/jkevinp/tgui/button"

// For radio questions - clean single choices
radioChoices := button.NewBuilder().
    SingleChoiceWithData("Option A", "opt_a").
    SingleChoiceWithData("Option B", "opt_b").
    Build()

// For checkbox questions - flexible grid layout
checkboxChoices := button.NewBuilder().
    Row().
    ChoiceWithData("Choice X", "choice_x").
    ChoiceWithData("Choice Y", "choice_y").
    Row().
    ChoiceWithData("Choice Z", "choice_z").
    Build()

// Quick helpers for common patterns
yesNoChoices := button.QuickChoices("Yes", "No")
pairedChoices := button.QuickPairedChoices("Option 1", "Option 2", "Option 3", "Option 4")

Legacy Manual Creation (Not Recommended):

// Old way - manual button creation (harder to maintain)
radioChoices := [][]button.Button{
    {button.Button{Text: "Option A", CallbackData: "opt_a"}}, // Row 1
    {button.Button{Text: "Option B", CallbackData: "opt_b"}}, // Row 2
}

Note: The CallbackData for buttons should be unique for each choice within a question.

Example of validateFunc:

func validateNonEmpty(answer string) error {
    if strings.TrimSpace(answer) == "" {
        return fmt.Errorf("Answer cannot be empty.")
    }
    return nil
}

func validateNumber(answer string) error {
    if _, err := strconv.Atoi(answer); err != nil {
        return fmt.Errorf("Please enter a valid number.")
    }
    return nil
}

Using the Manager

The Manager is crucial for handling text-based answers from users.

  1. Initialization:

    qsManager := questionaire.NewManager()
    

    This is typically done once when your bot application starts.

  2. Handling Messages: You must register the Manager.HandleMessage method with your go-telegram/bot instance to process incoming text messages. This allows the manager to route text replies to the correct active Questionaire.

    // In your bot setup, after creating the bot instance `b` and `qsManager`
    b.RegisterHandler(bot.HandlerTypeMessageText, "", bot.MatchTypePrefix, qsManager.HandleMessage)
    // Or, if you have a more complex routing system:
    // b.RegisterHandler(bot.HandlerTypeMessageText, "my_text_input_prefix", bot.MatchTypePrefix, qsManager.HandleMessage)
    

    When a user sends a text message, and an active questionnaire exists for their chatID in the manager, HandleMessage will call the Answer() method of that Questionaire.

The Questionaire.Show() method, if a manager was provided during NewBuilder or via SetManager, will automatically add the Questionaire instance to the manager. The manager will automatically remove the Questionaire instance after the onDoneHandler completes successfully or if the onCancelHandler is triggered through a managed cancel button.

Starting and Running the Questionnaire

Once configured, start the questionnaire:

q.Show(ctx, b, chatID)

This will send the first question to the user.

  • For text questions, the user replies with a text message, which is caught by Manager.HandleMessage.
  • For radio/checkbox questions, the Questionaire internally uses tgui/keyboard/inline to display buttons. User clicks are handled by internal callback handlers within the Questionaire, which then call its Answer() method.
  • If an answer is invalid (based on your validateFunc), the error is shown, and the question is re-asked.
  • The process continues until all questions are answered or the questionnaire is cancelled.

Getting Answers

The onDoneHandler receives the collected answers:

func onSurveyDone(ctx context.Context, b *bot.Bot, chatID any, answers map[string]interface{}) error {
    // Process answers
    // For QuestionFormatCheck, the answer will be a []string of selected CallbackData
    // For QuestionFormatText/Radio, it will be a string.
    fmt.Printf("Survey for chat %v completed. Answers: %+v\n", chatID, answers)
    b.SendMessage(ctx, &bot.SendMessageParams{
        ChatID: chatID,
        Text:   "Thanks for completing the survey!",
    })
    return nil
}

You can also call q.GetAnswers() map[string]interface{} on a Questionaire instance at any time to get the current state of answers, though it's most relevant in the onDoneHandler.

Interaction Flow (Mermaid Diagram)

sequenceDiagram
    participant User
    participant BotLib ("go-telegram/bot")
    participant AppHandler (Your Bot Code)
    participant QManager ("questionaire.Manager")
    participant QInstance ("questionaire.Questionaire")

    User->>BotLib: Sends /start_survey
    BotLib->>AppHandler: Triggers command handler (e.g., /survey)

    AppHandler->>QInstance: q = questionaire.NewBuilder(chatID, qm)
    AppHandler->>QInstance: q.SetOnDoneHandler(onDoneFunc)
    AppHandler->>QInstance: q.SetOnCancelHandler(onCancelFunc)
    AppHandler->>QInstance: q.AddQuestion("name", "What's your name?", nil, nameValidator)
    AppHandler->>QInstance: q.AddMultipleAnswerQuestion("topics", "Select topics:", topicChoices, nil)
    AppHandler->>QInstance: q.Show(ctx, bot, chatID)

    QInstance->>QManager: (If q.manager is not nil) q.manager.Add(chatID, q)
    QInstance->>BotLib: SendMessage(question 1 text & buttons if any)
    BotLib->>User: Displays question 1

    alt User types text answer (for QuestionFormatText)
        User->>BotLib: Sends text reply
        BotLib->>AppHandler: Triggers general message handler (which should call QManager.HandleMessage)
        AppHandler->>QManager: qm.HandleMessage(ctx, bot, update)
        QManager->>QInstance: q = qm.Get(chatID)
        QManager->>QInstance: isDone = q.Answer(update.Message.Text, bot, chatID)
    else User clicks an inline button (for QuestionFormatRadio/Check)
        User->>BotLib: Clicks inline button
        BotLib->>QInstance: (Routes callback to QInstance's internal handler like onInlineKeyboardSelect, onDoneChoosing)
        QInstance->>QInstance: Internal handler calls isDone = q.Answer(callbackData, bot, chatID)
    end

    QInstance->>QInstance: (Inside Answer) Validates input
    alt Validation Fails
        QInstance->>BotLib: SendMessage(error message to user)
        QInstance->>BotLib: SendMessage(current question again to re-prompt)
        BotLib->>User: Shows error & re-asks question
    else Validation Succeeds
        alt More questions remain OR current question was checkbox type and not "cmd_done"
            QInstance->>QInstance: (If checkbox & not "cmd_done") Updates selected choices
            QInstance->>QInstance: (If not checkbox OR checkbox & "cmd_done") Advances currentQuestionIndex
            QInstance->>BotLib: SendMessage(next question OR current question with updated checkbox UI)
            BotLib->>User: Displays next/updated question
        else All questions answered (isDone = true)
            QInstance->>QInstance: (If called from QManager.HandleMessage) q.Done(ctx, bot, update)
            QInstance->>AppHandler: (Inside q.Done) Calls user's onDoneHandler(ctx, bot, chatID, answers)
            AppHandler->>BotLib: (Typically) SendMessage(confirmation/summary to user)
            QManager->>QManager: (If called from QManager.HandleMessage) qm.Remove(chatID)
        end
    end

    alt User cancels (e.g. clicks cancel button added by q.Show)
        User->>BotLib: Clicks cancel button
        BotLib->>QInstance: (Routes callback to QInstance's onCancel method)
        QInstance->>AppHandler: Calls user's onCancelHandler()
        QInstance->>BotLib: DeleteMessages (clears questionnaire messages)
        QManager->>QManager: (The onCancel handler in Questionaire should ensure q.manager.Remove(chatID) is called if a manager exists)
    end

Documentation

Overview

Package questionaire provides an interactive questionnaire system for Telegram bots.

This package allows you to create multi-step questionnaires with support for text input, single-choice (radio button), and multiple-choice (checkbox) questions. It includes features like answer validation, edit functionality, and clean ButtonGrid layouts.

Basic usage:

manager := questionaire.NewManager()
q := questionaire.NewBuilder(chatID, manager).
	SetOnDoneHandler(handleResults).
	AddQuestion("name", "What's your name?", nil, validateName).
	AddQuestion("age", "Select age group:", ageChoices, nil)
q.Show(ctx, bot, chatID)

The package integrates with the button package for creating organized choice layouts using the ButtonGrid builder pattern.

Index

Constants

View Source
const (
	// RadioUnselected is the symbol displayed for unselected radio button options.
	RadioUnselected = "⚪"
	// RadioSelected is the symbol displayed for the selected radio button option.
	RadioSelected = "🔘"
	// CheckUnselected is the symbol displayed for unselected checkbox options.
	CheckUnselected = "☑️"
	// CheckSelected is the symbol displayed for selected checkbox options.
	CheckSelected = "✅"
	// EditButtonText is the text displayed on edit buttons for answered questions.
	EditButtonText = "◀️ Edit"
	// DoneButtonText is the text displayed on the done button for checkbox questions.
	DoneButtonText = "✅ Done"
	// CancelButtonText is the text displayed on cancel buttons.
	CancelButtonText = "❌ Cancel"
)

UI constants for consistent button symbols and text across the questionnaire interface. These can be customized by modifying the constants if different symbols are preferred.

View Source
const (
	QUESTION_FORMAT = "✒️[%d/%d] %s"
)

Variables

This section is empty.

Functions

func GetResultByte

func GetResultByte(q *Questionaire) ([]byte, error)

GetResultByte marshals the questionnaire answers to JSON bytes.

This utility function serializes the complete answers map (including InitialData) to JSON format, making it suitable for logging, storage, or API transmission.

Returns:

  • []byte: JSON representation of the answers map
  • error: JSON marshaling error, if any

The JSON structure matches the answers map returned by GetAnswers():

  • Text/Radio answers: {"key": "value"}
  • Checkbox answers: {"key": ["option1", "option2"]}
  • Initial data: {"key": "value"}

Example usage:

jsonData, err := questionaire.GetResultByte(q)
if err != nil {
	log.Printf("Failed to serialize answers: %v", err)
	return
}
log.Printf("Survey results: %s", string(jsonData))

Types

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager handles thread-safe operations for questionnaire conversations. It manages active questionnaire sessions across multiple chats and routes incoming text messages to the appropriate questionnaire instance.

A Manager is required for questionnaires that include text input questions, as it handles the routing of user text replies to the correct questionnaire.

func NewManager

func NewManager() *Manager

NewManager creates a new Manager instance for handling questionnaire sessions. Typically, you create one Manager when your bot starts and register its HandleMessage method with your bot to process text messages.

Example:

manager := questionaire.NewManager()
bot.RegisterHandler(bot.HandlerTypeMessageText, "", bot.MatchTypePrefix, manager.HandleMessage)

func (*Manager) Add

func (m *Manager) Add(chatID int64, q *Questionaire)

Add stores a questionnaire conversation for the given chat ID. This is called automatically when a questionnaire with a manager is shown. Multiple questionnaires can be active simultaneously in different chats.

func (*Manager) Exists

func (m *Manager) Exists(chatID int64) bool

Exists checks if a questionnaire conversation exists for the given chat ID. Returns true if an active questionnaire is running in the specified chat. This method is thread-safe and can be called concurrently.

func (*Manager) Get

func (m *Manager) Get(chatID int64) *Questionaire

Get retrieves the questionnaire conversation for the given chat ID. Returns nil if no active questionnaire exists for the chat. This method is thread-safe and can be called concurrently.

func (*Manager) HandleMessage

func (m *Manager) HandleMessage(ctx context.Context, b *bot.Bot, update *models.Update)

HandleMessage processes incoming text messages for active questionnaire conversations. This method should be registered with your bot to handle text message updates:

bot.RegisterHandler(bot.HandlerTypeMessageText, "", bot.MatchTypePrefix, manager.HandleMessage)

The method:

  • Checks if an active questionnaire exists for the message's chat ID
  • Routes the message text to the appropriate questionnaire's Answer method
  • Handles questionnaire completion and cleanup automatically
  • Ignores messages from chats without active questionnaires

This enables seamless text input handling for questionnaire text questions without requiring manual message routing in your application code.

func (*Manager) Remove

func (m *Manager) Remove(chatID int64)

Remove deletes the questionnaire conversation for the given chat ID. This is called automatically when a questionnaire completes or is cancelled. After removal, text messages from this chat will no longer be routed to a questionnaire.

type Question

type Question struct {
	// Key is the unique identifier for this question used in the answers map
	Key string
	// Text is the question text displayed to the user
	Text string
	// Choices contains the button layout for radio/checkbox questions (created with ButtonGrid)
	Choices [][]button.Button
	// ChoicesSelected stores selected callback data for checkbox questions
	ChoicesSelected []string
	// Answer stores the user's response (text input or selected callback data)
	Answer string

	// QuestionFormat determines the type of question (text, radio, or checkbox)
	QuestionFormat QuestionFormat
	// MsgID stores the Telegram message ID of the question message for editing
	MsgID int
	// contains filtered or unexported fields
}

Question represents a single question within a questionnaire. It contains the question text, possible choices (for radio/checkbox questions), user's answer, validation function, and formatting information.

func (*Question) AddChoiceSelected

func (q *Question) AddChoiceSelected(answer string)

AddChoiceSelected appends a selected choice to the ChoicesSelected slice.

func (*Question) GetDisplayAnswer

func (q *Question) GetDisplayAnswer() string

GetDisplayAnswer returns a user-friendly display string for the question's answer.

The returned string is suitable for display in the Telegram chat:

  • Text questions: Returns the user's text input or "Not answered"
  • Radio questions: Returns the display text of the selected button or "Not selected"
  • Checkbox questions: Returns the first selection plus count (e.g., "Tech + 2 more") or "None selected"

This method converts internal callback data back to human-readable text, making it perfect for edit buttons and result summaries.

func (*Question) GetSelectedChoices

func (q *Question) GetSelectedChoices() [][]button.Button

func (*Question) GetUnselectedChoices

func (q *Question) GetUnselectedChoices() [][]button.Button

func (*Question) IsSelected

func (q *Question) IsSelected(choice string) bool

func (*Question) SetAnswer

func (q *Question) SetAnswer(answer string)

SetAnswer sets the answer for the question. This method is used internally during answer processing. For most use cases, answers are set automatically through user interaction.

func (*Question) SetMsgID

func (q *Question) SetMsgID(msgID int)

SetMsgID sets the Telegram message ID for this question. This is used internally to track message IDs for editing and cleanup purposes.

func (*Question) Validate

func (q *Question) Validate(answer string) error

Validate runs the validator function for the question, if set.

type QuestionFormat

type QuestionFormat int

QuestionFormat defines the type of input expected for a question.

const (
	// QuestionFormatText indicates a free-text input question where users type their response.
	QuestionFormatText QuestionFormat = 0
	// QuestionFormatRadio indicates a single-choice question with radio button style selection.
	QuestionFormatRadio QuestionFormat = 1
	// QuestionFormatCheck indicates a multiple-choice question with checkbox style selection.
	QuestionFormatCheck QuestionFormat = 2
)

type Questionaire

type Questionaire struct {

	// InitialData contains pre-filled data that will be included in final answers
	InitialData map[string]interface{}
	// contains filtered or unexported fields
}

Questionaire represents an interactive questionnaire session for a specific chat. It manages a sequence of questions, tracks user progress, and handles answer collection.

Use NewBuilder() to create a new questionnaire instance rather than constructing this struct directly.

func NewBuilder

func NewBuilder(chatID any, manager *Manager) *Questionaire

NewBuilder creates a new Questionaire instance for the specified chat.

Parameters:

  • chatID: The Telegram chat ID where this questionnaire will run
  • manager: Optional Manager instance to handle text message routing (can be nil)

Returns a new Questionaire instance that can be configured using the builder pattern.

Example:

manager := questionaire.NewManager()
q := questionaire.NewBuilder(chatID, manager).
	SetOnDoneHandler(handleCompletion).
	AddQuestion("name", "What's your name?", nil, validateName)

func (*Questionaire) AddMultipleAnswerQuestion

func (q *Questionaire) AddMultipleAnswerQuestion(key string, text string, choices [][]button.Button, validateFunc func(answer string) error) *Questionaire

AddMultipleAnswerQuestion adds a checkbox-style question that allows multiple selections.

Parameters:

  • key: Unique identifier for this question (used in the final answers map)
  • text: The question text shown to the user
  • choices: Button layout created with ButtonGrid (e.g., button.NewBuilder().Row().Choice("Option1").Build())
  • validateFunc: Optional validation function (usually nil for checkbox questions)

The user can select multiple options and click "Done" to proceed. The answer will be stored as a []string in the final results.

Example:

interests := button.NewBuilder().
	Row().ChoiceWithData("Tech", "tech").ChoiceWithData("Sports", "sports").
	Row().ChoiceWithData("Music", "music").ChoiceWithData("Travel", "travel").Build()
q.AddMultipleAnswerQuestion("interests", "Select your interests:", interests, nil)

func (*Questionaire) AddQuestion

func (q *Questionaire) AddQuestion(key string, text string, choices [][]button.Button, validateFunc func(answer string) error) *Questionaire

AddQuestion adds a text input or single-choice (radio) question to the questionnaire.

Parameters:

  • key: Unique identifier for this question (used in the final answers map)
  • text: The question text shown to the user
  • choices: Button layout for radio questions (nil for text input)
  • validateFunc: Optional validation function for user input

Question type is determined automatically:

  • If choices is nil: Creates a text input question
  • If choices is provided: Creates a radio button question (single selection)

Examples:

// Text input question
q.AddQuestion("name", "What's your name?", nil, validateNonEmpty)

// Radio button question
ageChoices := button.NewBuilder().
	SingleChoiceWithData("Under 18", "age_under_18").
	SingleChoiceWithData("18-30", "age_18_30").Build()
q.AddQuestion("age_group", "Select your age group:", ageChoices, nil)

func (*Questionaire) Answer

func (q *Questionaire) Answer(ctx context.Context, answer string, b *bot.Bot, chatID any) bool

Answer processes the user's answer for the current question and advances the questionnaire if appropriate. Returns true if all questions have been answered.

func (*Questionaire) Done

func (q *Questionaire) Done(ctx context.Context, b *bot.Bot, update *models.Update)

Done is called internally when all questions have been answered. It marshals the answers to JSON and calls the onDoneHandler. This method is typically not called directly by user code.

func (*Questionaire) GetAnswers

func (q *Questionaire) GetAnswers() map[string]interface{}

GetAnswers returns a map of question keys to their answers or selected choices. For text and radio questions, the value is a string. For checkbox questions, the value is a slice of strings ([]string). The returned map also includes any InitialData that was set on the questionnaire.

func (*Questionaire) GetQuestionIndex

func (q *Questionaire) GetQuestionIndex(question *Question) int

func (*Questionaire) SetAllowEditAnswers

func (q *Questionaire) SetAllowEditAnswers(allow bool) *Questionaire

SetAllowEditAnswers controls whether answered questions can be edited by the user. When set to false, answered questions will not show edit buttons and users cannot go back to modify their responses. When set to true (default), users can click edit buttons to modify previous answers.

Parameters:

  • allow: true to enable editing (default), false to disable editing of answered questions

Example:

// Create a questionnaire without edit functionality
q := questionaire.NewBuilder(chatID, manager).
	SetAllowEditAnswers(false).
	AddQuestion("name", "What's your name?", nil, nil)

func (*Questionaire) SetContext

func (q *Questionaire) SetContext(ctx context.Context) *Questionaire

SetContext sets the context for the questionnaire and returns the updated instance. This context will be merged with contexts passed to handler functions. Useful for passing request-scoped data or implementing timeouts.

func (*Questionaire) SetInitialData

func (q *Questionaire) SetInitialData(data map[string]interface{}) *Questionaire

SetInitialData sets pre-filled data for the questionnaire and returns the updated instance. This data will be included in the final answers map alongside user responses. Useful for including metadata like user ID, campaign source, etc.

func (*Questionaire) SetManager

func (q *Questionaire) SetManager(m *Manager) *Questionaire

SetManager sets or updates the manager for this questionnaire and returns the updated instance. The manager is responsible for routing text messages to active questionnaires. If you're using text questions, you need a manager and must register its HandleMessage method with your bot.

func (*Questionaire) SetOnCancelHandler

func (q *Questionaire) SetOnCancelHandler(handler func()) *Questionaire

SetOnCancelHandler sets the cancellation handler called when the user cancels the questionnaire. The handler typically sends a cancellation message to the user. Questionnaire cleanup (message deletion) is handled automatically.

func (*Questionaire) SetOnDoneHandler

func (q *Questionaire) SetOnDoneHandler(handler onDoneHandlerFunc) *Questionaire

SetOnDoneHandler sets the completion handler called when all questions have been answered.

The handler receives:

  • ctx: Context (merged with questionnaire context if set)
  • b: Bot instance
  • chatID: Chat ID where questionnaire is running
  • answers: Map of question keys to user answers

Example:

q.SetOnDoneHandler(func(ctx context.Context, b *bot.Bot, chatID any, answers map[string]interface{}) error {
	fmt.Printf("Survey completed: %+v\n", answers)
	return b.SendMessage(ctx, &bot.SendMessageParams{
		ChatID: chatID,
		Text:   "Thank you for completing the survey!",
	})
})

func (*Questionaire) Show

func (q *Questionaire) Show(ctx context.Context, b *bot.Bot, chatID any)

Show starts the questionnaire by displaying the first (or current) question to the user.

This method:

  • Registers the questionnaire with the manager (if available) for text message handling
  • Sends the current question with appropriate keyboard layout
  • Sets up callback handlers for button interactions
  • Handles error display if validation failed on previous attempts

Parameters:

  • ctx: Context for the operation
  • b: Bot instance for sending messages
  • chatID: Telegram chat ID where to display the question

The method automatically determines the question type and creates the appropriate interface:

  • Text questions: Simple message requesting text input
  • Radio questions: Message with inline keyboard buttons
  • Checkbox questions: Message with selectable buttons and "Done" option

Example:

q := questionaire.NewBuilder(chatID, manager).
	AddQuestion("name", "What's your name?", nil, nil).
	SetOnDoneHandler(handleResults)
q.Show(ctx, bot, chatID)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL