Telegram State Manager (tg-state-manager
) is a lightweight, type-safe state management library for Telegram bots in Go. It simplifies multi-step conversations with a clean API, flexible storage options (in-memory or Redis), and compatibility with any Telegram bot framework. Whether you're building a small bot or a complex application, this library keeps state handling straightforward and intuitive.
- Easy to Use: Define states with simple
Prompt
andHandle
functions. - Type-Safe: Leverages Go generics for safety (requires Go 1.18+).
- Flexible Storage: Use in-memory or Redis backends.
- Thread-Safe: Built-in concurrency support for in-memory storage.
- Framework-Agnostic: Works with any Telegram bot library (e.g., telebot, telego, telegram-bot-api).
- Lightweight: Minimal dependencies for quick integration.
Get the library with:
go get github.com/sudosz/tg-state-manager
For Redis storage (optional):
go get github.com/redis/go-redis/v9
Here's a simple example showing how to use tg-state-manager
to collect a user's name and age. This uses the telebot
framework, but the library works with any Telegram bot framework.
package main
import (
"fmt"
"log"
"os"
"strconv"
"time"
tgsm "github.com/sudosz/tg-state-manager"
tele "gopkg.in/telebot.v4"
)
// UserData holds the information collected during the conversation
type UserData struct {
Name string
Age int
}
func main() {
// Initialize bot
bot, err := tele.NewBot(tele.Settings{
Token: os.Getenv("TOKEN"),
Poller: &tele.LongPoller{Timeout: 10 * time.Second},
})
if err != nil {
log.Fatal(err)
}
// Setup state manager with in-memory storage
storage := tgsm.NewInMemoryStorage[UserData]()
sm := tgsm.NewStateManager(storage, func(u tele.Update) int64 {
return u.Message.Chat.ID
})
sm.SetInitialState("ask_name")
err = sm.Add(
&tgsm.State[UserData, tele.Update]{
Name: "ask_name",
Prompt: func(u tele.Update, data *UserData) error {
_, err := bot.Send(u.Message.Chat, "What's your name?")
return err
},
Handle: func(u tele.Update, data *UserData) (string, error) {
if u.Message.Text == "" {
return "", tgsm.ErrValidation
}
data.Name = u.Message.Text
return "ask_age", nil
},
},
&tgsm.State[UserData, tele.Update]{
Name: "ask_age",
Prompt: func(u tele.Update, data *UserData) error {
_, err := bot.Send(u.Message.Chat, "How old are you?")
return err
},
Handle: func(u tele.Update, data *UserData) (string, error) {
age, err := strconv.Atoi(u.Message.Text)
if err != nil || age < 0 {
return "", tgsm.ErrValidation
}
data.Age = age
_, err = bot.Send(u.Message.Chat, "All set!")
return "", err
},
},
)
if err != nil {
log.Fatal(err)
}
// State management middleware
bot.Use(func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
handled, err := sm.Handle(c.Update())
if err != nil {
return err
}
if !handled {
return next(c)
}
return nil
}
})
// Start command handler
bot.Handle("/start", func(c tele.Context) error {
userData, _, err := storage.Get(c.Chat().ID)
if err != nil {
return err
}
_, err = bot.Send(c.Message().Chat, fmt.Sprintf("Hello, %s!", userData.Data.Name))
return err
})
// Fallback handler (required due to middleware)
bot.Handle(tele.OnText, func(c tele.Context) error {
return nil // Do nothing for unhandled text messages
})
bot.Start()
}
Why the Fallback Handler?
Since we use middleware to process state manager updates, a fallback handler (like tele.OnText
) is required to catch any updates the state manager doesn't handle. This prevents the bot from silently ignoring messages.
Create states with the State[S, U]
struct:
S
: Your custom data type (e.g.,UserData
).U
: The update type from your framework.Name
: A unique state identifier.Prompt
: Sends a message when entering the state (optional).Handle
: Processes input and returns the next state (or""
to end).
state := &tgsm.State[UserData, UpdateType]{
Name: "example",
Prompt: func(u UpdateType, data *UserData) error {
// Send a message
return nil
},
Handle: func(u UpdateType, data *UserData) (string, error) {
// Process input, update data, return next state
return "next_state", nil
},
}
Pick a storage backend:
- In-Memory (thread-safe):
storage := tgsm.NewInMemoryStorage[UserData]()
- Redis (persistent):
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"}) storage := tgsm.NewRedisStorage[UserData](client, "bot-prefix")
Initialize with storage and a key function:
sm := tgsm.NewStateManager[UserData, UpdateType](storage, func(u UpdateType) int64 {
return u.ChatID // Extract chat/user ID
})
Process updates with middleware or a handler:
// Example middleware
bot.Use(func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
handled, err := sm.Handle(c.Update())
if err != nil {
return err
}
if !handled {
return next(c)
}
return nil
}
})
Set an initial state:
sm.SetInitialState("first_state")
Add states with validation to prevent duplicates:
err := sm.Add(state1, state2, state3)
if err != nil {
log.Fatalf("Error adding states: %v", err)
}
Note: The Add
function returns an error if duplicate state names are detected to prevent accidental overwrites. This ensures state integrity by allowing developers to handle duplicates explicitly (e.g., logging or skipping them) rather than silently overwriting existing states, which could lead to bugs.
- Group Related States: Keep states for a specific flow together.
- Use Descriptive Names: Name states clearly to understand their purpose.
- Handle Edge Cases: Consider what happens when users send unexpected inputs.
- Provide Clear Feedback: Always inform users about validation errors.
- End States: Use empty string or
tgsm.NopState
to indicate the end of a conversation flow.
Here's a simple examples of using the library with telebot
, telego
and telegram-bot-api
:
package main
import (
"log"
"os"
"time"
tgsm "github.com/sudosz/tg-state-manager"
tele "gopkg.in/telebot.v4"
)
type UserData struct {
Name string
}
func main() {
bot, err := tele.NewBot(tele.Settings{
Token: os.Getenv("TOKEN"),
Poller: &tele.LongPoller{Timeout: 10 * time.Second},
})
if err != nil {
log.Fatal(err)
}
storage := tgsm.NewInMemoryStorage[UserData]()
sm := tgsm.NewStateManager(storage, func(u tele.Update) int64 {
return u.Message.Chat.ID
})
sm.SetInitialState("ask_name")
err = sm.Add(&tgsm.State[UserData, tele.Update]{
Name: "ask_name",
Prompt: func(u tele.Update, data *UserData) error {
_, err := bot.Send(u.Message.Chat, "What's your name?")
return err
},
Handle: func(u tele.Update, data *UserData) (string, error) {
data.Name = u.Message.Text
_, err := bot.Send(u.Message.Chat, "Got it!")
return "", err
},
})
if err != nil {
log.Fatal(err)
}
bot.Handle(tele.OnText, func(c tele.Context) error {
_, err := sm.Handle(c.Update())
return err
})
bot.Start()
}
package main
import (
"context"
"log"
"os"
"github.com/mymmrac/telego"
tgsm "github.com/sudosz/tg-state-manager"
)
type UserData struct {
Name string
}
func main() {
ctx := context.Background()
bot, err := telego.NewBot(os.Getenv("TOKEN"))
if err != nil {
log.Fatal(err)
}
storage := tgsm.NewInMemoryStorage[UserData]()
sm := tgsm.NewStateManager[UserData, telego.Update](storage, func(u telego.Update) int64 {
return u.Message.Chat.ID
})
sm.SetInitialState("ask_name")
err = sm.Add(&tgsm.State[UserData, telego.Update]{
Name: "ask_name",
Prompt: func(u telego.Update, data *UserData) error {
_, err := bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: u.Message.Chat.ID},
Text: "What's your name?",
})
return err
},
Handle: func(u telego.Update, data *UserData) (string, error) {
data.Name = u.Message.Text
_, err := bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: u.Message.Chat.ID},
Text: "Got it!",
})
return "", err
},
})
if err != nil {
log.Fatal(err)
}
updates, _ := bot.UpdatesViaLongPolling(ctx, nil)
for update := range updates {
_, err := sm.Handle(telego.Update(update))
if err != nil {
log.Println(err)
}
}
}
package main
import (
"log"
"os"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
tgsm "github.com/sudosz/tg-state-manager"
)
type UserData struct {
Name string
}
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("TOKEN"))
if err != nil {
log.Fatal(err)
}
storage := tgsm.NewInMemoryStorage[UserData]()
sm := tgsm.NewStateManager[UserData, tgbotapi.Update](storage, func(u tgbotapi.Update) int64 {
return u.Message.Chat.ID
})
sm.SetInitialState("ask_name")
err = sm.Add(&tgsm.State[UserData, tgbotapi.Update]{
Name: "ask_name",
Prompt: func(u tgbotapi.Update, data *UserData) error {
msg := tgbotapi.NewMessage(u.Message.Chat.ID, "What's your name?")
_, err := bot.Send(msg)
return err
},
Handle: func(u tgbotapi.Update, data *UserData) (string, error) {
data.Name = u.Message.Text
msg := tgbotapi.NewMessage(u.Message.Chat.ID, "Got it!")
_, err := bot.Send(msg)
return "", err
},
})
if err != nil {
log.Fatal(err)
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
_, err := sm.Handle(update)
if err != nil {
log.Println(err)
}
}
}
For complex bots, consider creating helper functions that generate states with common patterns: For example, In telebot
we can do this:
// Helper function to create states with standard validation
func createState(
bot *tele.Bot,
name string,
promptMsg string,
validator func(string) (bool, error),
errorMsg string,
nextState string,
updateState func(string, *UserData),
) *tgsm.State[UserData, tele.Update] {
return &tgsm.State[UserData, tele.Update]{
Name: name,
Prompt: func(u tele.Update, state *UserData) error {
_, err := bot.Send(u.Message.Chat, promptMsg)
return err
},
Handle: func(u tele.Update, state *UserData) (string, error) {
text := u.Message.Text
if valid, _ := validator(text); !valid {
_, err := bot.Send(u.Message.Chat, errorMsg)
if err != nil {
return "", err
}
return "", tgsm.ErrValidation
}
updateState(text, state)
return nextState, nil
},
}
}
// Using the helper to create a specific state
nameState := createState(
bot,
"ask_name",
"What's your name?",
func(text string) (bool, error) {
return len(text) >= 2 && len(text) <= 50, nil
},
"Please enter a valid name (2-50 characters).",
"ask_age",
func(text string, data *UserData) {
data.Name = text
},
)
For more advanced examples, refer to the examples directory.
Name | Description |
---|---|
StateManager[S, U any] |
Manages states and transitions. |
NewStateManager(storage, keyFunc) |
Creates a new state manager. |
Add(states ...*State[S, U]) error |
Adds states to the manager with duplicate checking. |
Handle(update U) (bool, error) |
Processes an update. |
State[S, U any] |
Defines a state with Prompt /Handle . |
UserState[S any] |
Holds current state and data. |
StateStorage[S any] |
Storage interface (Get , Set ). |
NewInMemoryStorage[S any]() |
Creates in-memory storage. |
NewRedisStorage[S any](client, prefix) |
Creates Redis storage. |
We'd love your help! To contribute:
- Fork the repo on GitHub.
- Create a branch (
git checkout -b my-feature
). - Commit your changes (
git commit -m "Add feature"
). - Push (
git push origin my-feature
). - Open a pull request.
Please follow Go standards and add tests if possible. Report bugs or suggest ideas in the issues section.
Licensed under the MIT License. See LICENSE for details.
Thanks to everyone who's helped improve tg-state-manager
!
Enjoying tg-state-manager
? Give it a ⭐ on GitHub!