{"id":23508958,"url":"https://github.com/maxbolgarin/bote","last_synced_at":"2026-04-02T11:57:46.968Z","repository":{"id":257122828,"uuid":"857384440","full_name":"maxbolgarin/bote","owner":"maxbolgarin","description":"Build interactive Telegram bots with telebot.v4 wrapper","archived":false,"fork":false,"pushed_at":"2026-03-25T08:47:07.000Z","size":323,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-25T08:52:03.562Z","etag":null,"topics":["go","golang","telebot","telegram","telegram-bot"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maxbolgarin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-09-14T14:12:53.000Z","updated_at":"2026-03-25T08:47:11.000Z","dependencies_parsed_at":"2024-12-14T20:24:24.893Z","dependency_job_id":"5cb864f3-c4f1-4c7c-bc4a-43d2227c5f93","html_url":"https://github.com/maxbolgarin/bote","commit_stats":null,"previous_names":["maxbolgarin/bote"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/maxbolgarin/bote","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxbolgarin%2Fbote","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxbolgarin%2Fbote/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxbolgarin%2Fbote/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxbolgarin%2Fbote/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maxbolgarin","download_url":"https://codeload.github.com/maxbolgarin/bote/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxbolgarin%2Fbote/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31305967,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T09:48:21.550Z","status":"ssl_error","status_checked_at":"2026-04-02T09:48:19.196Z","response_time":89,"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","golang","telebot","telegram","telegram-bot"],"created_at":"2024-12-25T11:36:08.953Z","updated_at":"2026-04-02T11:57:46.962Z","avatar_url":"https://github.com/maxbolgarin.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Bote: Interactive Telegram Bot Framework for Go\n\nBote is a powerful wrapper for [Telebot v4](https://gopkg.in/telebot.v4) that simplifies building interactive Telegram bots with smart message management, user state tracking, and advanced keyboard handling.\n\n[![Go Version][version-img]][doc] [![GoDoc][doc-img]][doc] [![Build][ci-img]][ci] [![Coverage][coverage-img]][coverage] [![GoReport][report-img]][report]\n\n## Features\n\n- **Smart Message Management** — main, head, notification, error, and history message lifecycle\n- **User State Tracking** — per-message states with text input handling\n- **Interactive Keyboards** — inline keyboard builder with automatic row layout\n- **Middleware Support** — user-level and chat-type middlewares\n- **Internationalization** — built-in multi-language message provider\n- **Privacy \u0026 Encryption** — optional strict mode with AES-256 encrypted user IDs\n- **Persistence** — pluggable storage with ordered async writes via [gorder](https://github.com/maxbolgarin/gorder)\n- **Webhook Support** — built-in webhook server with TLS, secret token, IP filtering, and rate limiting\n- **Prometheus Metrics** — updates, handlers, errors, active users, session length, webhooks\n- **Bot Restart Recovery** — automatic re-initialization of user messages via state map\n- **Context-based API** — clean handler interface with `Context` for all operations\n\n## Installation\n\n```bash\ngo get github.com/maxbolgarin/bote\n```\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"log\"\n    \"os\"\n    \"os/signal\"\n    \"syscall\"\n\n    \"github.com/maxbolgarin/bote\"\n)\n\nfunc main() {\n    token := os.Getenv(\"TELEGRAM_BOT_TOKEN\")\n    if token == \"\" {\n        log.Fatalln(\"TELEGRAM_BOT_TOKEN is not set\")\n    }\n\n    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n    defer cancel()\n\n    b, err := bote.New(ctx, token)\n    if err != nil {\n        log.Fatalln(err)\n    }\n\n    stopCh := b.Start(ctx, startHandler, nil)\n    \u003c-stopCh\n}\n\nfunc startHandler(ctx bote.Context) error {\n    kb := bote.InlineBuilder(3, bote.OneBytePerRune,\n        ctx.Btn(\"Option 1\", option1Handler),\n        ctx.Btn(\"Option 2\", option2Handler),\n        ctx.Btn(\"Option 3\", option3Handler),\n    )\n    return ctx.SendMain(bote.NoChange, \"Welcome! Choose an option:\", kb)\n}\n\nfunc option1Handler(ctx bote.Context) error {\n    return ctx.EditMain(bote.NoChange, \"You selected Option 1\", nil)\n}\n\nfunc option2Handler(ctx bote.Context) error {\n    return ctx.EditMain(bote.NoChange, \"You selected Option 2\", nil)\n}\n\nfunc option3Handler(ctx bote.Context) error {\n    return ctx.EditMain(bote.NoChange, \"You selected Option 3\", nil)\n}\n```\n\n## Core Concepts\n\n### Message Types\n\nBote manages five message types per user, each with automatic lifecycle handling:\n\n| Type | Description | Behavior |\n|------|-------------|----------|\n| **Main** | Primary interactive message | Previous main becomes history on new send |\n| **Head** | Optional message above main | Deleted when new main is sent |\n| **Notification** | Temporary user notification | Old notification auto-deleted on new one |\n| **Error** | Error feedback | Auto-deleted on next user action |\n| **History** | Previous main messages | Tracked for editing and cleanup |\n\n### States\n\nEach message has an associated state. States control which handler runs when a user interacts with an old message after a bot restart.\n\n```go\n// Define your states as a string type implementing the State interface\ntype AppState string\n\nfunc (s AppState) String() string  { return string(s) }\nfunc (s AppState) IsText() bool    { return s == StateAwaitingInput }\nfunc (s AppState) NotChanged() bool { return false }\n\nconst (\n    StateMenu          AppState = \"menu\"\n    StateSettings      AppState = \"settings\"\n    StateAwaitingInput AppState = \"awaiting_input\" // IsText() returns true\n)\n```\n\nBuilt-in states: `bote.NoChange`, `bote.FirstRequest`, `bote.Unknown`, `bote.Disabled`.\n\n### Context\n\nEvery handler receives a `Context` that provides:\n\n```go\nfunc myHandler(ctx bote.Context) error {\n    // User info\n    ctx.User().ID()\n    ctx.User().Username()\n    ctx.User().Language()\n    ctx.User().StateMain()\n\n    // Message operations\n    ctx.SendMain(state, \"text\", keyboard)\n    ctx.EditMain(state, \"text\", keyboard)\n    ctx.Send(state, \"main text\", \"head text\", mainKb, headKb)\n    ctx.SendNotification(\"info\", nil)\n    ctx.SendError(\"something went wrong\")\n\n    // Callback data\n    ctx.ButtonID()\n    ctx.Data()           // raw: \"a|b|c\"\n    ctx.DataParsed()     // []string{\"a\", \"b\", \"c\"}\n\n    // Text input\n    ctx.Text()\n\n    // Custom values (persisted)\n    ctx.User().SetValue(\"key\", value)\n    val, ok := ctx.User().GetValue(\"key\")\n\n    return nil\n}\n```\n\n## Configuration\n\n### Using Option Functions\n\n```go\nb, err := bote.New(ctx, token,\n    bote.WithDefaultLanguage(\"en\"),\n    bote.WithLogger(myLogger),\n    bote.WithUserDB(myStorage),\n    bote.WithMsgsProvider(myMessages),\n    bote.WithDebugIncomingUpdates(),\n)\n```\n\n### Using Config Struct\n\n```go\nb, err := bote.New(ctx, token, func(opts *bote.Options) {\n    opts.Config = bote.Config{\n        Bot: bote.BotConfig{\n            DefaultLanguage: \"en\",\n            DeleteMessages:  bote.Ptr(true),\n            NoPreview:       true,\n        },\n        Log: bote.LogConfig{\n            Enable:     bote.Ptr(true),\n            LogUpdates: bote.Ptr(true),\n        },\n    }\n    opts.UserDB = myStorage\n    opts.Msgs = myMessages\n})\n```\n\n### Environment Variables\n\nAll config fields can be set via `BOTE_*` environment variables (e.g., `BOTE_DEFAULT_LANGUAGE=en`).\n\n## Keyboards\n\n```go\n// Single row\nkb := bote.SingleRow(\n    ctx.Btn(\"Yes\", yesHandler),\n    ctx.Btn(\"No\", noHandler),\n)\n\n// Auto-layout with column count and rune type\nkb := bote.InlineBuilder(2, bote.TwoBytesPerRune,\n    ctx.Btn(\"Option A\", handlerA),\n    ctx.Btn(\"Option B\", handlerB),\n    ctx.Btn(\"Option C\", handlerC),\n    ctx.Btn(\"Option D\", handlerD),\n)\n\n// Manual builder\nkb := bote.NewKeyboard(3)\nkb.Add(ctx.Btn(\"One\", h1))\nkb.Add(ctx.Btn(\"Two\", h2))\nkb.StartNewRow()\nkb.Add(ctx.Btn(\"Three\", h3))\nkb.AddFooter(ctx.Btn(\"Back\", backHandler))\nmarkup := kb.CreateInlineMarkup()\n\n// Buttons with callback data\nctx.Btn(\"Delete\", deleteHandler, userID, itemID)\n// In handler: ctx.DataParsed() returns []string{userID, itemID}\n```\n\nRune size types for automatic row sizing: `OneBytePerRune` (English), `TwoBytesPerRune` (Cyrillic), `FourBytesPerRune` (emoji).\n\n## Text Input Handling\n\nRegister text-expecting states and set a text handler:\n\n```go\n// In your state definition\nfunc (s AppState) IsText() bool {\n    return s == StateAwaitingName || s == StateAwaitingEmail\n}\n\n// Set the text handler\nb.SetTextHandler(func(ctx bote.Context) error {\n    text := ctx.Text()\n\n    switch ctx.User().StateMain() {\n    case StateAwaitingName:\n        ctx.User().SetValue(\"name\", text)\n        return ctx.EditMain(StateAwaitingEmail, \"Now enter your email:\", nil)\n\n    case StateAwaitingEmail:\n        ctx.User().SetValue(\"email\", text)\n        name, _ := ctx.User().GetValue(\"name\")\n        msg := bote.FB(\"Name: \") + name.(string) + \"\\n\" + bote.FB(\"Email: \") + text\n        return ctx.EditMain(StateMenu, msg, menuKeyboard(ctx))\n\n    default:\n        return ctx.SendNotification(\"Send /start to begin\", nil)\n    }\n})\n\n// Trigger text input by sending a message with a text state\nfunc askNameHandler(ctx bote.Context) error {\n    return ctx.SendMain(StateAwaitingName, \"Enter your name:\", nil)\n}\n```\n\n## Persistence\n\nImplement `UsersStorage` to persist user data between restarts:\n\n```go\ntype MyStorage struct {\n    db *sql.DB\n}\n\nfunc (s *MyStorage) Insert(ctx context.Context, user bote.UserModel) error {\n    // Insert user into database\n    return nil\n}\n\nfunc (s *MyStorage) Find(ctx context.Context, id bote.FullUserID) (bote.UserModel, bool, error) {\n    // Find user by ID (use id.IDPlain or id.IDHMAC depending on privacy mode)\n    return bote.UserModel{}, false, nil\n}\n\nfunc (s *MyStorage) UpdateAsync(id bote.FullUserID, diff *bote.UserModelDiff) {\n    // Apply partial update. Bote wraps this with an ordered queue (gorder),\n    // so updates are guaranteed to arrive in order per user.\n    // You can use a simple synchronous DB call here.\n}\n\nb, err := bote.New(ctx, token, bote.WithUserDB(\u0026MyStorage{db: db}))\n```\n\n## Bot Restart Recovery\n\nProvide a state map so users can continue from where they left off:\n\n```go\nstateMap := map[bote.State]bote.InitBundle{\n    StateMenu: {\n        Handler: menuHandler,\n    },\n    StateSettings: {\n        Handler: settingsHandler,\n    },\n    StateAwaitingName: {\n        Handler: askNameHandler,\n    },\n}\n\nstopCh := b.Start(ctx, startHandler, stateMap)\n```\n\nWhen a user clicks a button on an old message after a restart, bote looks up the message's state in this map and runs the corresponding handler to rebuild the UI.\n\n## Middleware\n\n```go\n// User-level middleware (private chats only)\nb.AddUserMiddleware(func(upd *tele.Update, user bote.User) bool {\n    log.Printf(\"User %d made an action\", user.ID())\n    return true // return false to drop the update\n})\n\n// Chat-type middleware\nb.AddMiddleware(func(upd *tele.Update) bool {\n    // Rate limiting, analytics, etc.\n    return true\n}, tele.ChatGroup, tele.ChatSuperGroup)\n```\n\n## Webhook Mode\n\n```go\nb, err := bote.New(ctx, token,\n    bote.WithWebhook(\"https://example.com/webhook\", \":8443\"),\n    bote.WithWebhookSecretToken(\"my-secret\"),\n    bote.WithWebhookRateLimit(100, 200),\n    bote.WithWebhookSecurityHeaders(),\n    bote.WithWebhookAllowedTelegramIPs(),\n)\n```\n\nOptions: `WithWebhookCertificate`, `WithWebhookGenerateCertificate`, `WithWebhookAllowedIPs`, `WithWebhookMetrics`.\n\n## Privacy Mode\n\nStrict privacy mode encrypts user IDs with AES-256 and stores only HMAC for lookups:\n\n```go\nencKey := \"hex-encoded-32-byte-key\"\nhmacKey := \"hex-encoded-32-byte-key\"\n\nb, err := bote.New(ctx, token,\n    bote.WithStrictPrivacyMode(\u0026encKey, nil, \u0026hmacKey, nil),\n)\n```\n\nIn strict mode: no usernames or names are stored, user IDs are encrypted in the database, and logs show only HMAC prefixes.\n\n## Prometheus Metrics\n\n```go\nimport \"github.com/prometheus/client_golang/prometheus\"\n\nregistry := prometheus.NewRegistry()\nb, err := bote.New(ctx, token,\n    bote.WithMetricsConfig(bote.MetricsConfig{\n        Registry: registry,\n    }),\n)\n```\n\nTracked metrics: `bote_updates_total`, `bote_handlers_in_flight`, `bote_handler_duration_seconds`, `bote_errors_total`, `bote_messages_send_total`, `bote_users_current_active`, `bote_users_session_length_seconds`, and webhook metrics.\n\n## Message Formatting\n\n```go\nmsg := bote.FB(\"Bold\") + \" and \" + bote.FI(\"italic\") + \"\\n\"\nmsg += bote.FC(\"code\") + \" or \" + bote.FP(\"pre\", \"go\") + \"\\n\"\nmsg += bote.FS(\"strikethrough\") + \" \" + bote.FU(\"underline\")\n\n// Using the builder\nb := bote.NewBuilder()\nb.Writeln(bote.FB(\"User Profile\"))\nb.Writeln(\"\")\nb.Writeln(\"Name: \" + name)\nb.Writeln(\"Email: \" + email)\nb.WriteIf(isAdmin, bote.FB(\"Admin\"))\nmsg = b.String()\n```\n\n`FB` = bold, `FI` = italic, `FC` = code, `FP` = pre, `FS` = strikethrough, `FU` = underline.\n\n## Complete Example: Todo Bot\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n    \"os\"\n    \"os/signal\"\n    \"strconv\"\n    \"syscall\"\n\n    \"github.com/maxbolgarin/bote\"\n)\n\ntype State string\n\nfunc (s State) String() string  { return string(s) }\nfunc (s State) IsText() bool    { return s == StateAddingTask }\nfunc (s State) NotChanged() bool { return false }\n\nconst (\n    StateMenu       State = \"menu\"\n    StateAddingTask State = \"adding_task\"\n    StateViewTasks  State = \"view_tasks\"\n)\n\nfunc main() {\n    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n    defer cancel()\n\n    b, err := bote.New(ctx, os.Getenv(\"TELEGRAM_BOT_TOKEN\"))\n    if err != nil {\n        log.Fatalln(err)\n    }\n\n    b.SetTextHandler(textHandler)\n\n    stopCh := b.Start(ctx, menuHandler, map[bote.State]bote.InitBundle{\n        StateMenu:       {Handler: menuHandler},\n        StateAddingTask: {Handler: addTaskHandler},\n        StateViewTasks:  {Handler: viewTasksHandler},\n    })\n    \u003c-stopCh\n}\n\nfunc menuHandler(ctx bote.Context) error {\n    kb := bote.InlineBuilder(1, bote.OneBytePerRune,\n        ctx.Btn(\"Add Task\", addTaskHandler),\n        ctx.Btn(\"View Tasks\", viewTasksHandler),\n    )\n    return ctx.SendMain(StateMenu, bote.FB(\"Todo List\")+\"\\nChoose an action:\", kb)\n}\n\nfunc addTaskHandler(ctx bote.Context) error {\n    kb := bote.SingleRow(ctx.Btn(\"Cancel\", menuHandler))\n    return ctx.EditMain(StateAddingTask, \"Enter your task:\", kb)\n}\n\nfunc viewTasksHandler(ctx bote.Context) error {\n    tasks, ok := ctx.User().GetValue(\"tasks\")\n    if !ok || len(tasks.([]string)) == 0 {\n        kb := bote.SingleRow(ctx.Btn(\"Add Task\", addTaskHandler))\n        return ctx.EditMain(StateViewTasks, \"No tasks yet!\", kb)\n    }\n\n    b := bote.NewBuilder()\n    b.Writeln(bote.FB(\"Your Tasks:\"))\n    b.Writeln(\"\")\n    for i, task := range tasks.([]string) {\n        b.Writeln(fmt.Sprintf(\"%d. %s\", i+1, task))\n    }\n\n    kb := bote.InlineBuilder(1, bote.OneBytePerRune,\n        ctx.Btn(\"Add Task\", addTaskHandler),\n        ctx.Btn(\"Clear All\", func(ctx bote.Context) error {\n            ctx.User().DeleteValue(\"tasks\")\n            return viewTasksHandler(ctx)\n        }),\n        ctx.Btn(\"Back\", menuHandler),\n    )\n    return ctx.EditMain(StateViewTasks, b.String(), kb)\n}\n\nfunc textHandler(ctx bote.Context) error {\n    if ctx.User().StateMain() != StateAddingTask {\n        return nil\n    }\n    task := ctx.Text()\n\n    tasks, ok := ctx.User().GetValue(\"tasks\")\n    var list []string\n    if ok {\n        list = tasks.([]string)\n    }\n    list = append(list, task)\n    ctx.User().SetValue(\"tasks\", list)\n\n    ctx.SendNotification(\"Task added: \"+bote.FI(task), nil)\n    return viewTasksHandler(ctx)\n}\n```\n\n## Public Chat Support\n\nBote can handle messages in groups and channels alongside private chats:\n\n```go\n// Register a handler for text messages in any chat\nb.Handle(tele.OnText, func(ctx bote.Context) error {\n    if !ctx.IsPrivate() {\n        // Group/channel message\n        if ctx.IsMentioned() {\n            return ctx.SendInChat(ctx.ChatID(), 0, \"Hello from the bot!\", nil)\n        }\n        return nil\n    }\n    // Private message — handled by text handler\n    return nil\n})\n```\n\n## API Reference\n\nSee the full API documentation on [pkg.go.dev](https://pkg.go.dev/github.com/maxbolgarin/bote).\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n\n---\n\n[version-img]: https://img.shields.io/badge/Go-%3E%3D%201.24-%23007d9c\n[doc-img]: https://pkg.go.dev/badge/github.com/maxbolgarin/bote\n[doc]: https://pkg.go.dev/github.com/maxbolgarin/bote\n[ci-img]: https://github.com/maxbolgarin/bote/actions/workflows/go.yml/badge.svg\n[ci]: https://github.com/maxbolgarin/bote/actions\n[report-img]: https://goreportcard.com/badge/github.com/maxbolgarin/bote\n[report]: https://goreportcard.com/report/github.com/maxbolgarin/bote\n[coverage-img]: https://codecov.io/gh/maxbolgarin/bote/branch/main/graph/badge.svg\n[coverage]: https://codecov.io/gh/maxbolgarin/bote/branch/main\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxbolgarin%2Fbote","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaxbolgarin%2Fbote","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxbolgarin%2Fbote/lists"}