Building UI

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.