{"id":42137791,"url":"https://github.com/whynot00/go-telegram-fsm","last_synced_at":"2026-01-26T16:31:24.155Z","repository":{"id":309427644,"uuid":"1033900078","full_name":"whynot00/go-telegram-fsm","owner":"whynot00","description":null,"archived":false,"fork":false,"pushed_at":"2025-08-19T19:21:28.000Z","size":54,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-19T20:46:11.987Z","etag":null,"topics":["go-telegram","go-telegram-fsm","telegram"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/whynot00.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-07T14:17:49.000Z","updated_at":"2025-08-19T19:20:00.000Z","dependencies_parsed_at":"2025-08-11T21:39:26.199Z","dependency_job_id":"ba50b537-aa50-48b1-b038-b4767d1777e2","html_url":"https://github.com/whynot00/go-telegram-fsm","commit_stats":null,"previous_names":["whynot00/go-telegram-fsm"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/whynot00/go-telegram-fsm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whynot00%2Fgo-telegram-fsm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whynot00%2Fgo-telegram-fsm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whynot00%2Fgo-telegram-fsm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whynot00%2Fgo-telegram-fsm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/whynot00","download_url":"https://codeload.github.com/whynot00/go-telegram-fsm/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whynot00%2Fgo-telegram-fsm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28782167,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-26T13:55:28.044Z","status":"ssl_error","status_checked_at":"2026-01-26T13:55:26.068Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go-telegram","go-telegram-fsm","telegram"],"created_at":"2026-01-26T16:31:23.590Z","updated_at":"2026-01-26T16:31:24.146Z","avatar_url":"https://github.com/whynot00.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# go-telegram-fsm\n\n`go-telegram-fsm` provides a lightweight finite state machine (FSM) for [go-telegram/bot](https://github.com/go-telegram/bot) based bots.  \nThe 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.\n\n\u003e **Why?**  \n\u003e 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.  \n\u003e This package handles the boilerplate so you can focus on your bot logic.\n\n## Features\n\n- Thread‑safe FSM with per‑user state and last access timestamp.\n- Pluggable storage layer with an in‑memory implementation shipped by default.\n- Automatic cleanup of stale states and cache entries based on TTL.\n- Optional key/value cache and media‑group cache bound to a user.\n- `bot.Middleware` that automatically:\n  - extracts the user ID from incoming updates,\n  - creates a default state entry for new users,\n  - attaches both the FSM instance and user ID to `context.Context`.\n- `fsm.WithStates` middleware to guard handlers by allowed states.\n- Simple API: `Transition`, `Finish`, `CurrentState`, `Set`, `Get`, `SetMedia`, …\n- Zero dependencies besides the Telegram SDK and the standard library.\n\n## Installation\n\n```bash\ngo get github.com/whynot00/go-telegram-fsm\n```\n\nThe module requires Go 1.20+ (see `go.mod` for the exact version).\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"time\"\n\n    fsm \"github.com/whynot00/go-telegram-fsm\"\n    \"github.com/go-telegram/bot\"\n    \"github.com/go-telegram/bot/models\"\n)\n\nfunc main() {\n    ctx := context.Background()\n\n    // Create FSM with custom TTL and cleanup interval.\n    machine := fsm.New(ctx,\n        fsm.WithTTL(30*time.Minute),\n        fsm.WithCleanupInterval(30*time.Second),\n    )\n\n    b, _ := bot.New(\"\u003cTOKEN\u003e\", bot.WithMiddlewares(fsm.Middleware(machine)))\n\n    // /start is allowed only in the default state and moves user to \"ask-name\".\n\tb.RegisterHandler(bot.HandlerTypeMessageText, \"/start\", bot.MatchTypeCommand,\n\t\tfunc(ctx context.Context, b *bot.Bot, upd *models.Update) {\n\t\t\tmachine := fsm.FromContext(ctx)\n\n\t\t\tmachine.Transition(ctx, \"ask-name\")\n\t\t\tb.SendMessage(ctx, \u0026bot.SendMessageParams{ChatID: upd.Message.Chat.ID, Text: \"Hi! What is your name?\"})\n\t\t},\n\t\tfsm.WithStates(fsm.StateDefault),\n\t)\n\n    // Handler for the next state\n    b.RegisterHandler(bot.HandlerTypeMessageText, \"\", bot.MatchTypeExact,\n        func(ctx context.Context, b *bot.Bot, upd *models.Update) {\n            name := upd.Message.Text\n            b.SendMessage(ctx, \u0026bot.SendMessageParams{ChatID: upd.Message.Chat.ID, Text: \"Nice to meet you, \" + name})\n            machine.Finish(ctx) // back to default, cache cleaned\n        },\n        fsm.WithStates(\"ask-name\"),\n    )\n\n    b.Start(ctx)\n}\n```\n\n## FSM Concepts\n\n### States\nA state is represented by `fsm.StateFSM` (alias of `string`).\nThree special states are provided:\n\n- `StateDefault` – automatically assigned to every new user. Transitioning back to this state also clears the user's cache.\n- `StateAny` – wildcard used in `WithStates` middleware to run a handler regardless of current state.\n- `StateNil` – returned by `CurrentState` when no state exists for a user.\n\n### Creating and Accessing States\nNormally you do not create state manually – `Middleware` does it lazily when a user first interacts with the bot.  \nStill, you can explicitly call `f.Create(ctx)` if required.  `CurrentState` returns the current state and refreshes the \"last used\" timestamp:\n\n```go\nst, ok := f.CurrentState(ctx)\nif !ok {\n    // no state stored yet\n}\n```\n\n### Transitions and Finish\nUse `Transition` to move a user to another state.  \nCalling `Finish` is a shortcut for `Transition(ctx, StateDefault)` and also purges all cached data for that user:\n\n```go\nf.Transition(ctx, \"awaiting_email\")\n...\nf.Finish(ctx) // back to StateDefault + cache cleanup\n```\n\n## Middleware Integration\n\n### Middleware(fsm)\n`fsm.Middleware` wraps handlers to inject FSM and user ID into the context:\n\n1. Extracts the user ID from `models.Update` (handles most Telegram update types).\n2. Creates an entry with `StateDefault` if the user was not seen before.\n3. Stores both the FSM instance and user ID in `context.Context` so downstream handlers can access them with `fsm.FromContext` and `userFromContext` (internally).\n\nAttach it globally when creating the bot:\n\n```go\nb, _ := bot.New(token, bot.WithMiddlewares(fsm.Middleware(f)))\n```\n\n### WithStates\n`fsm.WithStates` is an additional middleware that allows a handler to run only when a user's state matches one of the provided states:\n\n```go\nb.RegisterHandler(bot.HandlerTypeMessageText, \"\", handler, fsm.WithStates(\"step1\", \"step2\"))\n```\n\nSpecial rules:\n\n- No states passed → handler always runs.\n- `StateAny` present → handler always runs.\n- No FSM or no state in context → handler is skipped.\n\n## User Cache\n\nEach 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:\n\n```go\nfsm.Set(ctx, userID, \"key\", 42)\nval, ok := fsm.Get(ctx, userID, \"key\")\n```\n\nThe 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.\n\n### Media Group Cache\nTelegram can send media as groups.  FSM keeps an in-memory accumulator per user \u0026 media group:\n\n```go\nfile := media.File{Type: \"photo\", FileID: someID}\nfsm.SetMedia(ctx, userID, mediaGroupID, file)\n\nmd, _ := fsm.GetMedia(ctx, userID, mediaGroupID)\nfiles := md.Files() // copy of stored files\n```\n\nYou may remove media groups manually with `CleanMediaCache` or wipe everything with `CleanCache`/`Finish`.\n\n## Custom Storage\n\nThe storage backend is abstracted by the `storage.Storage` interface:\n\n```go\ntype Storage interface {\n    Set(ctx context.Context, userID int64, key string, value any)\n    Get(ctx context.Context, userID int64, key string) (any, bool)\n    SetMedia(ctx context.Context, userID int64, mediaGroupID string, file media.File)\n    GetMedia(ctx context.Context, userID int64, mediaGroupID string) (*media.MediaData, bool)\n    CleanMediaCache(ctx context.Context, userID int64, mediaGroupID string) bool\n    CleanCache(ctx context.Context, userID int64)\n    Close()\n}\n```\n\nBy default the FSM uses an in-memory implementation (`storage/memory`) that:\n\n- partitions data by user ID,\n- tracks last access time and runs a background goroutine to evict idle users,\n- is safe for concurrent access.\n\nProvide your own implementation and pass it via `WithStorage` option:\n\n```go\nstore := redisStorage{...} // any struct implementing storage.Storage\nf := fsm.New(ctx, fsm.WithStorage(store))\n```\n\nIf you supply custom storage the FSM will not manage its lifecycle (no automatic `Close`).\n\n## Configuration Options\n\nOptions are applied when creating an FSM instance:\n\n```go\nfsm.New(ctx,\n    fsm.WithStorage(store),      // custom storage instead of in-memory\n    fsm.WithTTL(time.Hour),      // how long to keep user state without activity\n    fsm.WithCleanupInterval(time.Minute), // how often expired states are purged\n)\n```\n\n## Testing\n\nRun the test suite with:\n\n```bash\ngo test ./...\n```\n\nIt includes unit tests for state transitions, middleware behaviour and integration tests covering a typical conversation flow.\n\n## License\n\nThis project is provided without an explicit license file.  Use at your own risk or contact the author to clarify licensing terms.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhynot00%2Fgo-telegram-fsm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwhynot00%2Fgo-telegram-fsm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhynot00%2Fgo-telegram-fsm/lists"}