https://github.com/whynot00/go-telegram-fsm
https://github.com/whynot00/go-telegram-fsm
go-telegram go-telegram-fsm telegram
Last synced: 5 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/whynot00/go-telegram-fsm
- Owner: whynot00
- Created: 2025-08-07T14:17:49.000Z (11 months ago)
- Default Branch: main
- Last Pushed: 2025-08-19T19:21:28.000Z (10 months ago)
- Last Synced: 2025-08-19T20:46:11.987Z (10 months ago)
- Topics: go-telegram, go-telegram-fsm, telegram
- Language: Go
- Homepage:
- Size: 52.7 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# go-telegram-fsm
`go-telegram-fsm` provides a lightweight finite state machine (FSM) for [go-telegram/bot](https://github.com/go-telegram/bot) based bots.
The library tracks a state for each Telegram user, supplies handy middleware to inject the FSM into request context and offers a per-user cache for arbitrary data and media groups.
> **Why?**
> Chat‑bot flows often look like conversational state machines: you ask a question, wait for a reply, move to the next step and so on.
> This package handles the boilerplate so you can focus on your bot logic.
## Features
- Thread‑safe FSM with per‑user state and last access timestamp.
- Pluggable storage layer with an in‑memory implementation shipped by default.
- Automatic cleanup of stale states and cache entries based on TTL.
- Optional key/value cache and media‑group cache bound to a user.
- `bot.Middleware` that automatically:
- extracts the user ID from incoming updates,
- creates a default state entry for new users,
- attaches both the FSM instance and user ID to `context.Context`.
- `fsm.WithStates` middleware to guard handlers by allowed states.
- Simple API: `Transition`, `Finish`, `CurrentState`, `Set`, `Get`, `SetMedia`, …
- Zero dependencies besides the Telegram SDK and the standard library.
## Installation
```bash
go get github.com/whynot00/go-telegram-fsm
```
The module requires Go 1.20+ (see `go.mod` for the exact version).
## Quick Start
```go
package main
import (
"context"
"time"
fsm "github.com/whynot00/go-telegram-fsm"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
)
func main() {
ctx := context.Background()
// Create FSM with custom TTL and cleanup interval.
machine := fsm.New(ctx,
fsm.WithTTL(30*time.Minute),
fsm.WithCleanupInterval(30*time.Second),
)
b, _ := bot.New("", bot.WithMiddlewares(fsm.Middleware(machine)))
// /start is allowed only in the default state and moves user to "ask-name".
b.RegisterHandler(bot.HandlerTypeMessageText, "/start", bot.MatchTypeCommand,
func(ctx context.Context, b *bot.Bot, upd *models.Update) {
machine := fsm.FromContext(ctx)
machine.Transition(ctx, "ask-name")
b.SendMessage(ctx, &bot.SendMessageParams{ChatID: upd.Message.Chat.ID, Text: "Hi! What is your name?"})
},
fsm.WithStates(fsm.StateDefault),
)
// Handler for the next state
b.RegisterHandler(bot.HandlerTypeMessageText, "", bot.MatchTypeExact,
func(ctx context.Context, b *bot.Bot, upd *models.Update) {
name := upd.Message.Text
b.SendMessage(ctx, &bot.SendMessageParams{ChatID: upd.Message.Chat.ID, Text: "Nice to meet you, " + name})
machine.Finish(ctx) // back to default, cache cleaned
},
fsm.WithStates("ask-name"),
)
b.Start(ctx)
}
```
## FSM Concepts
### States
A state is represented by `fsm.StateFSM` (alias of `string`).
Three special states are provided:
- `StateDefault` – automatically assigned to every new user. Transitioning back to this state also clears the user's cache.
- `StateAny` – wildcard used in `WithStates` middleware to run a handler regardless of current state.
- `StateNil` – returned by `CurrentState` when no state exists for a user.
### Creating and Accessing States
Normally you do not create state manually – `Middleware` does it lazily when a user first interacts with the bot.
Still, you can explicitly call `f.Create(ctx)` if required. `CurrentState` returns the current state and refreshes the "last used" timestamp:
```go
st, ok := f.CurrentState(ctx)
if !ok {
// no state stored yet
}
```
### Transitions and Finish
Use `Transition` to move a user to another state.
Calling `Finish` is a shortcut for `Transition(ctx, StateDefault)` and also purges all cached data for that user:
```go
f.Transition(ctx, "awaiting_email")
...
f.Finish(ctx) // back to StateDefault + cache cleanup
```
## Middleware Integration
### Middleware(fsm)
`fsm.Middleware` wraps handlers to inject FSM and user ID into the context:
1. Extracts the user ID from `models.Update` (handles most Telegram update types).
2. Creates an entry with `StateDefault` if the user was not seen before.
3. Stores both the FSM instance and user ID in `context.Context` so downstream handlers can access them with `fsm.FromContext` and `userFromContext` (internally).
Attach it globally when creating the bot:
```go
b, _ := bot.New(token, bot.WithMiddlewares(fsm.Middleware(f)))
```
### WithStates
`fsm.WithStates` is an additional middleware that allows a handler to run only when a user's state matches one of the provided states:
```go
b.RegisterHandler(bot.HandlerTypeMessageText, "", handler, fsm.WithStates("step1", "step2"))
```
Special rules:
- No states passed → handler always runs.
- `StateAny` present → handler always runs.
- No FSM or no state in context → handler is skipped.
## User Cache
Each FSM instance also serves as a small per-user cache. The storage implements the `storage.Storage` interface. Functions operate on the user ID you pass explicitly:
```go
fsm.Set(ctx, userID, "key", 42)
val, ok := fsm.Get(ctx, userID, "key")
```
The default memory storage keeps cache items in `sync.Map` partitions and tracks the last access time per user. When a state expires (by TTL) or you call `Finish`, the cache for that user is dropped.
### Media Group Cache
Telegram can send media as groups. FSM keeps an in-memory accumulator per user & media group:
```go
file := media.File{Type: "photo", FileID: someID}
fsm.SetMedia(ctx, userID, mediaGroupID, file)
md, _ := fsm.GetMedia(ctx, userID, mediaGroupID)
files := md.Files() // copy of stored files
```
You may remove media groups manually with `CleanMediaCache` or wipe everything with `CleanCache`/`Finish`.
## Custom Storage
The storage backend is abstracted by the `storage.Storage` interface:
```go
type Storage interface {
Set(ctx context.Context, userID int64, key string, value any)
Get(ctx context.Context, userID int64, key string) (any, bool)
SetMedia(ctx context.Context, userID int64, mediaGroupID string, file media.File)
GetMedia(ctx context.Context, userID int64, mediaGroupID string) (*media.MediaData, bool)
CleanMediaCache(ctx context.Context, userID int64, mediaGroupID string) bool
CleanCache(ctx context.Context, userID int64)
Close()
}
```
By default the FSM uses an in-memory implementation (`storage/memory`) that:
- partitions data by user ID,
- tracks last access time and runs a background goroutine to evict idle users,
- is safe for concurrent access.
Provide your own implementation and pass it via `WithStorage` option:
```go
store := redisStorage{...} // any struct implementing storage.Storage
f := fsm.New(ctx, fsm.WithStorage(store))
```
If you supply custom storage the FSM will not manage its lifecycle (no automatic `Close`).
## Configuration Options
Options are applied when creating an FSM instance:
```go
fsm.New(ctx,
fsm.WithStorage(store), // custom storage instead of in-memory
fsm.WithTTL(time.Hour), // how long to keep user state without activity
fsm.WithCleanupInterval(time.Minute), // how often expired states are purged
)
```
## Testing
Run the test suite with:
```bash
go test ./...
```
It includes unit tests for state transitions, middleware behaviour and integration tests covering a typical conversation flow.
## License
This project is provided without an explicit license file. Use at your own risk or contact the author to clarify licensing terms.