Conversations
Ask a follow-up question and wait for the answer without wiring your own state machine. gogram gives you a one-line Ask and a fuller Conversation object for anything more elaborate.
The problem
Update handlers are event callbacks — each incoming message arrives in its own goroutine with no memory of what came before. For a wizard ("What is your name? Now your email? Now confirm."), the classic solution is a state machine keyed by user id.
The conversation helper hides the state machine. You block your handler goroutine on the next matching message; gogram wires the filter up for you and tears it down when you are done.
The quick Ask shortcut
For single-question flows, m.Askis a one-liner. It sends a prompt and returns the user's next reply as a *NewMessage:
client.On("command:reset", func(m *telegram.NewMessage) error {
prompt, answer, err := m.Ask("Are you sure? Type YES to confirm.",
&telegram.SendOptions{Timeouts: 30}, // seconds
)
if err != nil {
return err
}
_ = prompt // the message you just sent
if answer.Text() != "YES" {
_, _ = m.Reply("Cancelled.")
return nil
}
reset()
_, _ = m.Reply("Done.")
return nil
})client.On("command:reset", func(m *telegram.NewMessage) error {
prompt, answer, err := m.Ask("Are you sure? Type YES to confirm.",
&telegram.SendOptions{Timeouts: 30}, // seconds
)
if err != nil {
return err
}
_ = prompt // the message you just sent
if answer.Text() != "YES" {
_, _ = m.Reply("Cancelled.")
return nil
}
reset()
_, _ = m.Reply("Done.")
return nil
})The return signature is (prompt, response, error). The prompt is the message you just sent (handy if you want to edit or delete it afterwards); the response is the user's reply. Default timeout is 120 seconds; override with SendOptions.Timeouts in seconds.
The full Conversation flow
When you need more than one question, or you want to tune matchers and filters, drop into NewConversation directly:
client.On("command:setup", func(m *telegram.NewMessage) error {
conv, err := client.NewConversation(m.ChatID(),
&telegram.ConversationOptions{Timeout: 60},
)
if err != nil {
return err
}
defer conv.Close()
conv.SetFromUser(m.SenderID())
conv.Respond("Your name?")
name, err := conv.GetResponse()
if err != nil { return err }
conv.Respond("Your email?")
email, err := conv.GetResponse()
if err != nil { return err }
save(name.Text(), email.Text())
_, _ = m.Reply("Saved.")
return nil
})client.On("command:setup", func(m *telegram.NewMessage) error {
conv, err := client.NewConversation(m.ChatID(),
&telegram.ConversationOptions{Timeout: 60},
)
if err != nil {
return err
}
defer conv.Close()
conv.SetFromUser(m.SenderID())
conv.Respond("Your name?")
name, err := conv.GetResponse()
if err != nil { return err }
conv.Respond("Your email?")
email, err := conv.GetResponse()
if err != nil { return err }
save(name.Text(), email.Text())
_, _ = m.Reply("Saved.")
return nil
})NewConversation takes the chat to listen in. It returns a *Conversation with fluent With* setters and imperative Set* equivalents; both do the same thing so pick the style you like. Always defer conv.Close().
Multi-step
Just chain calls. The conversation tracks position for you:
conv.Respond("Step 1?")
a, _ := conv.GetResponse()
conv.Respond("Step 2?")
b, _ := conv.GetResponse()
conv.Respond("Step 3?")
c, _ := conv.GetResponse()conv.Respond("Step 1?")
a, _ := conv.GetResponse()
conv.Respond("Step 2?")
b, _ := conv.GetResponse()
conv.Respond("Step 3?")
c, _ := conv.GetResponse()The whole wizard runs in a single handler goroutine.
Restricting by user
By default the conversation matches any message in the chat — not what you want in a group. Pin the listener to the user who triggered the flow:
conv.SetFromUser(m.SenderID()) // or: conv.WithFromUser(m.SenderID()) // only messages from that specific user in this chat wake // GetResponse; everything else the chat sees is ignored.
conv.SetFromUser(m.SenderID())
// or:
conv.WithFromUser(m.SenderID())
// only messages from that specific user in this chat wake
// GetResponse; everything else the chat sees is ignored.Matching the response
Several GetResponse variants ship for common patterns:
// wait for any response
answer, err := conv.GetResponse()
// wait for a response containing one of these words
answer, err = conv.GetResponseContaining("yes", "y")
// wait for a response that matches a regex
answer, err = conv.GetResponseMatching(regexp.MustCompile(`^\\d{6}$`))
// wait for one of a fixed set of exact strings
answer, err = conv.GetResponseExact("YES", "NO", "MAYBE")// wait for any response
answer, err := conv.GetResponse()
// wait for a response containing one of these words
answer, err = conv.GetResponseContaining("yes", "y")
// wait for a response that matches a regex
answer, err = conv.GetResponseMatching(regexp.MustCompile(`^\\d{6}$`))
// wait for one of a fixed set of exact strings
answer, err = conv.GetResponseExact("YES", "NO", "MAYBE")Each variant blocks until a matching message arrives or the timeout fires. Non-matching messages in the chat are silently ignored.
Waiting for button clicks
For inline-keyboard flows, wait for a callback query on a message you just sent:
client.On("/vote", func(m *telegram.NewMessage) error {
kbd := telegram.NewKeyboard().AddRow(
telegram.Button.Data("yes", "vote:y"),
telegram.Button.Data("no", "vote:n"),
).Build()
prompt, _ := m.Reply("Vote:", &telegram.SendOptions{ReplyMarkup: kbd})
_ = prompt
click, err := m.WaitClick(30) // seconds
if err != nil {
return err
}
_ = click.Answer("registered")
return nil
})client.On("/vote", func(m *telegram.NewMessage) error {
kbd := telegram.NewKeyboard().AddRow(
telegram.Button.Data("yes", "vote:y"),
telegram.Button.Data("no", "vote:n"),
).Build()
prompt, _ := m.Reply("Vote:", &telegram.SendOptions{ReplyMarkup: kbd})
_ = prompt
click, err := m.WaitClick(30) // seconds
if err != nil {
return err
}
_ = click.Answer("registered")
return nil
})m.WaitClick(timeout) is the shorthand equivalent of conv.WaitClick(). It returns the *CallbackQuery so you can answer, edit the original message, or start a follow-up conversation from the click.
Abort keywords
Let users bail out of a wizard by typing a magic word without you sprinkling if text == "cancel" checks at every step:
conv.SetAbortKeywords("cancel", "/cancel", "stop")
answer, err := conv.GetResponse()
if err != nil {
// conv.Close is called; the abort keyword message counts as
// the response. Handle it however you like.
}conv.SetAbortKeywords("cancel", "/cancel", "stop")
answer, err := conv.GetResponse()
if err != nil {
// conv.Close is called; the abort keyword message counts as
// the response. Handle it however you like.
}Cleanup
Always defer conv.Close(). It releases the per-chat filter and any pending goroutines so the dispatcher does not hold references. m.Ask and m.WaitClick do the cleanup for you.
