{"id":51503849,"url":"https://github.com/ilyabrin/tgbase","last_synced_at":"2026-07-07T22:30:56.705Z","repository":{"id":366905184,"uuid":"982449233","full_name":"ilyabrin/tgbase","owner":"ilyabrin","description":"Project Template for Go Telegram Bot (tgbase)","archived":false,"fork":false,"pushed_at":"2026-06-23T19:29:50.000Z","size":228,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-06-23T21:14:48.419Z","etag":null,"topics":["framework","telegram","telegrambot","template","template-generic-repo","template-project","template-repository"],"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/ilyabrin.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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-05-12T22:48:27.000Z","updated_at":"2026-06-23T20:16:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ilyabrin/tgbase","commit_stats":null,"previous_names":["ilyabrin/tgbase"],"tags_count":4,"template":true,"template_full_name":null,"purl":"pkg:github/ilyabrin/tgbase","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilyabrin%2Ftgbase","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilyabrin%2Ftgbase/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilyabrin%2Ftgbase/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilyabrin%2Ftgbase/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ilyabrin","download_url":"https://codeload.github.com/ilyabrin/tgbase/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilyabrin%2Ftgbase/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35245324,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["framework","telegram","telegrambot","template","template-generic-repo","template-project","template-repository"],"created_at":"2026-07-07T22:30:56.228Z","updated_at":"2026-07-07T22:30:56.693Z","avatar_url":"https://github.com/ilyabrin.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tgbase - Go Telegram Bot Template\n\n\u003e 🇷🇺 [Читать на русском](README_RU.md)\n\n[![CI Status](https://github.com/ilyabrin/tgbase/actions/workflows/ci.yml/badge.svg)](https://github.com/ilyabrin/tgbase/actions/workflows/ci.yml)\n[![Codecov](https://codecov.io/gh/ilyabrin/tgbase/branch/main/graph/badge.svg)](https://codecov.io/gh/ilyabrin/tgbase)\n[![Go Report Card](https://goreportcard.com/badge/github.com/ilyabrin/tgbase)](https://goreportcard.com/report/github.com/ilyabrin/tgbase)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nDeveloper-friendly template for building Telegram bots in Go. Clone, configure, add handlers.\n\n**Batteries included:** database (Postgres/SQLite), Redis, FSM, middleware, i18n, payments (Telegram Stars), inline + reply keyboards, pagination, broadcast, deep links, inline mode, Docker.\n\n---\n\n## Quick start\n\n```bash\ngit clone https://github.com/ilyabrin/tgbase \u0026\u0026 cd tgbase\ngo mod edit -module github.com/yourname/yourbot  # rename the module to your own\ngo mod tidy\n# set your token in config.yaml\ngo run cmd/app/main.go\n```\n\n---\n\n## Project structure\n\n```text\ntgbase/\n├── cmd/app/main.go               # entry point - wires everything together\n├── config/\n│   └── config.go                 # YAML config + env overrides\n├── internal/\n│   ├── bot/\n│   │   ├── bot.go                # Bot type, functional options, Run/Start\n│   │   ├── broadcast.go          # Broadcast - send to multiple users\n│   │   ├── handler.go            # RegisterHandlers - add your handlers here\n│   │   └── handlers/\n│   │       ├── inline.go         # Inline mode (OnQuery)\n│   │       ├── payment.go        # Telegram Stars payments\n│   │       ├── registration.go   # FSM multi-step flow demo\n│   │       ├── start.go          # /start\n│   │       └── text.go           # OnText fallback\n│   ├── database/\n│   │   ├── database.go           # Database + SoftDeleteDatabase interfaces\n│   │   ├── postgres.go           # PostgreSQL implementation\n│   │   └── sqlite.go             # SQLite implementation\n│   ├── fsm/\n│   │   ├── fsm.go                # FSM: New, Route, On, SetState, GetData…\n│   │   └── storage.go            # Storage interface, RedisStorage, MemoryStorage\n│   ├── i18n/\n│   │   └── i18n.go               # go-i18n wrapper, locales/*.yaml\n│   └── redis/\n│       ├── redis.go              # Client, Config, NewRedisClient, NewMockClient\n│       └── real_redis.go         # go-redis/v9 implementation\n├── pkg/\n│   ├── deeplink/deeplink.go      # Parse /start payload, Build deep link URLs\n│   ├── keyboard/keyboard.go      # Inline + reply keyboard builders\n│   ├── logger/logger.go          # stdlib logger wrapper\n│   ├── middleware/middleware.go  # AdminOnly, Logger, RateLimit, Recover\n│   └── pagination/pagination.go  # Inline ← N/M → keyboard + Page() helper\n├── config.yaml\n├── docker-compose.yml\n└── Dockerfile\n```\n\n---\n\n## Configuration (`config.yaml`)\n\n```yaml\ndatabase:\n  type: sqlite          # or \"postgres\"\n  postgres:\n    dsn: \"host=localhost user=postgres password=secret dbname=app port=5432 sslmode=disable\"\n  sqlite:\n    path: \"app.db\"\n\nredis:\n  enabled: true\n  addr: \"localhost:6379\"\n  password: \"\"\n  db: 0\n\ntelegram:\n  token: \"YOUR_BOT_TOKEN\"\n  admin_ids:\n    - 123456789          # your Telegram user ID\n```\n\nEnvironment variable overrides: `TELEGRAM_TOKEN`, `POSTGRES_DSN`, `REDIS_ADDR`.\n\n---\n\n## Bot creation API\n\n`bot.New` accepts functional options - pass only what you need:\n\n```go\nb, err := bot.New(cfg.Telegram.Token,\n    bot.WithDB(db),\n    bot.WithRedis(redisClient),\n    bot.WithI18n(locale),\n    bot.WithAdminIDs(cfg.Telegram.AdminIDs),\n    bot.WithLogger(logger),\n    bot.WithErrorHandler(func(err error, c telebot.Context) {\n        if c != nil {\n            c.Send(\"Something went wrong, please try again.\")\n        }\n    }),\n    bot.WithWebhook(\":8080\"),              // optional: switch from polling to webhook\n    bot.WithPollerTimeout(30*time.Second), // optional: default 10s\n)\n```\n\nStart the bot (blocks until SIGINT/SIGTERM):\n\n```go\nb.RegisterHandlers()\nb.Run()\n```\n\n---\n\n## Adding handlers\n\nAll handlers live in `internal/bot/handler.go` → `RegisterHandlers()`:\n\n```go\nfunc (b *Bot) RegisterHandlers() {\n    b.Use(middleware.Logger(b.logger))\n    b.Use(middleware.RateLimit(30, time.Minute))\n\n    b.Handle(\"/start\", handlers.StartHandler(b.i18n))\n    b.Handle(\"/help\",  myHelpHandler)\n\n    // admin-only\n    b.Handle(\"/ban\", banHandler, middleware.AdminOnly(b.adminIDs))\n}\n```\n\n`b.Handle` returns `*Bot` for chaining. `b.Use` registers global middleware.\n\n### Handler pattern\n\n```go\n// internal/bot/handlers/my_command.go\nfunc MyHandler(i18n *i18n.I18n) telebot.HandlerFunc {\n    return func(c telebot.Context) error {\n        lang := c.Sender().LanguageCode\n        if lang == \"\" {\n            lang = \"en\"\n        }\n        return c.Send(i18n.Localize(lang, \"my_key\", nil))\n    }\n}\n```\n\n---\n\n## Middleware (`pkg/middleware`)\n\n```go\n// Global - applies to all handlers\nb.Use(middleware.Recover(logger))      // always first\nb.Use(middleware.Logger(logger))\nb.Use(middleware.RateLimit(5, time.Minute))\n\n// Per-handler\nb.Handle(\"/admin\", adminHandler, middleware.AdminOnly(adminIDs))\n\n// Custom reject message\nb.Use(middleware.RateLimit(3, time.Minute, func(c telebot.Context) error {\n    return c.Send(\"Too many messages, please wait.\")\n}))\n```\n\n| Middleware                     | Description                                    |\n| ------------------------------ | ---------------------------------------------- |\n| `Recover(l)`                   | Catch handler panics, log, return error        |\n| `AdminOnly(ids, onReject?)`    | Allow only listed user IDs                     |\n| `Logger(l)`                    | Log every update: user ID, name, text/callback |\n| `RateLimit(n, per, onReject?)` | Per-user sliding window rate limiter           |\n\n---\n\n## Broadcast\n\nSend a message to multiple users with automatic rate limiting (default 50 ms between sends ≈ 20 msg/s, safely below Telegram's 30 msg/s cap). Stops early on context cancellation.\n\n```go\nresult := b.Broadcast(ctx, userIDs, \"🔔 Announcement text\",\n    bot.WithBroadcastDelay(50*time.Millisecond), // optional, 50ms is default\n    bot.WithBroadcastOnError(func(id int64, err error) {\n        log.Printf(\"failed to send to %d: %v\", id, err)\n    }),\n)\nfmt.Printf(\"sent: %d, failed: %d\\n\", result.Sent, result.Failed)\n```\n\n---\n\n## Pagination (`pkg/pagination`)\n\nBuild ← N/M → navigation keyboards for DB result sets.\n\n```go\nimport \"tgbase/pkg/pagination\"\n\npg := pagination.New(totalUsers, 10) // 10 per page\n\n// In list handler:\nitems := fetchPage(pg.Offset(page), pg.PageSize)\nc.Send(renderList(items), pg.Keyboard(page, \"users\"))\n\n// Register callbacks:\nb.Handle(\"\\fusers_prev\", usersPageHandler)\nb.Handle(\"\\fusers_next\", usersPageHandler)\n\nfunc usersPageHandler(c telebot.Context) error {\n    page, _ := pagination.Page(c) // parse target page from callback\n    items := fetchPage(pg.Offset(page), pg.PageSize)\n    return c.Edit(renderList(items), pg.Keyboard(page, \"users\"))\n}\n```\n\n| Function                  | Description                                   |\n| ------------------------- | --------------------------------------------- |\n| `New(total, pageSize)`    | Create a pager                                |\n| `.Pages()`                | Total page count                              |\n| `.Offset(page)`           | SQL OFFSET for page (1-indexed)               |\n| `.Keyboard(page, prefix)` | Build prev/N/M/next inline row; nil if 1 page |\n| `Page(c)`                 | Parse target page from callback context       |\n\n---\n\n## Deep links (`pkg/deeplink`)\n\n```go\nimport \"tgbase/pkg/deeplink\"\n\n// /start handler - works for both plain /start and /start \u003cpayload\u003e\nb.Handle(\"/start\", func(c telebot.Context) error {\n    payload := deeplink.Parse(c) // \"\" for plain /start\n    if payload == \"\" {\n        return c.Send(\"Welcome!\")\n    }\n    return c.Send(\"You came via: \" + payload)\n})\n\n// Generate a shareable deep link\nlink := deeplink.Build(\"mybot\", \"ref_42\")\n// → \"https://t.me/mybot?start=ref_42\"\n```\n\n---\n\n## Inline mode\n\nLets users type `@botname \u003cquery\u003e` in any chat and pick a result to send.\nHandler registered automatically in `RegisterHandlers`. Customise\n`InlineHandler()` in `internal/bot/handlers/inline.go`.\n\n```go\nfunc InlineHandler() telebot.HandlerFunc {\n    return func(c telebot.Context) error {\n        text := c.Query().Text\n        results := telebot.Results{\n            \u0026telebot.ArticleResult{\n                ResultBase:  telebot.ResultBase{ID: \"r0\"},\n                Title:       text,\n                Description: \"Send this to the chat\",\n                Text:        text,\n            },\n        }\n        return c.Answer(\u0026telebot.QueryResponse{Results: results, CacheTime: 60})\n    }\n}\n```\n\n\u003e **Note:** enable inline mode in [@BotFather](https://t.me/BotFather) → Bot Settings → Inline Mode.\n\n---\n\n## Keyboards (`pkg/keyboard`)\n\n### Inline keyboard\n\nButtons displayed inside a message.\n\n```go\nkb := keyboard.New().\n    Row(keyboard.Btn(\"Buy\", \"btn_buy\"), keyboard.Btn(\"Cancel\", \"btn_cancel\")).\n    Row(keyboard.URL(\"Website\", \"https://example.com\")).\n    Build()\n\nc.Send(\"Choose:\", kb)\n\n// Register callback handlers\nb.Handle(\"\\fbtn_buy\",    buyHandler)\nb.Handle(\"\\fbtn_cancel\", cancelHandler)\n```\n\n| Function                   | Description                                 |\n| -------------------------- | ------------------------------------------- |\n| `Btn(text, unique, data?)` | Callback button; handler key: `\"\\f\"+unique` |\n| `URL(text, url)`           | Button that opens a URL                     |\n| `New()`                    | Create a builder                            |\n| `.Row(btns...)`            | Append a row; chainable                     |\n| `.Build()`                 | Return `*telebot.ReplyMarkup`               |\n\n### Reply keyboard\n\nPersistent button row shown above the message input field.\n\n```go\nkb := keyboard.Reply().\n    Row(\"Profile\", \"Settings\").\n    Row(\"Help\").\n    OneTime().\n    Build()\nc.Send(\"Choose:\", kb)\n\n// Reply buttons send their text as a plain OnText message — handle with c.Text()\nb.Handle(telebot.OnText, func(c telebot.Context) error {\n    switch c.Text() {\n    case \"Profile\":\n        return c.Send(\"Your profile...\")\n    case \"Settings\":\n        return c.Send(\"Settings menu...\")\n    case \"Help\":\n        return c.Send(\"Help text...\")\n    }\n    return nil\n})\n\n// Remove keyboard\nc.Send(\"Done!\", keyboard.Remove())\n```\n\n| Method           | Description                                |\n| ---------------- | ------------------------------------------ |\n| `Reply()`        | Create builder (resize enabled by default) |\n| `.Row(texts...)` | Append a row of text buttons               |\n| `.OneTime()`     | Hide after first use                       |\n| `.Persistent()`  | Keep visible between messages              |\n| `.Placeholder()` | Input field hint text                      |\n| `.Build()`       | Return `*telebot.ReplyMarkup`              |\n| `Remove()`       | Remove current reply keyboard              |\n\n---\n\n## FSM - conversation flows (`internal/fsm`)\n\n```go\nf := fsm.New(fsm.NewRedisStorage(b.redis, fsm.WithTTL(3600))).\n    Fallback(handlers.TextHandler(b.i18n))\n\nb.Handle(\"/register\", func(c telebot.Context) error {\n    f.SetState(c, \"ask_name\")\n    return c.Send(\"What's your name?\")\n})\n\nb.Handle(telebot.OnText, f.Route(\n    fsm.On(\"ask_name\", func(c telebot.Context) error {\n        f.SetStateData(c, \"ask_age\", c.Text()) // save name, advance state\n        return c.Send(\"How old are you?\")\n    }),\n    fsm.On(\"ask_age\", func(c telebot.Context) error {\n        name, _ := f.GetData(c)\n        f.ClearState(c)\n        return c.Send(\"Done, \" + name + \"!\")\n    }),\n))\n```\n\nUse `fsm.NewMemoryStorage()` in tests - no Redis required.\n\n---\n\n## Database\n\n```go\n// Auto-selects Postgres or SQLite based on config\ndb, err := database.FromConfig(ctx, cfg)\n\n// Core operations\ndb.Exec(ctx, query, args...)\ndb.Query(ctx, query, args...)\ndb.QueryRow(ctx, query, args...)\n\n// CRUD helpers\ndb.Insert(ctx, \"users\", map[string]any{\"name\": \"Alice\", \"age\": 30})\ndb.Update(ctx, \"users\", map[string]any{\"age\": 31}, \"name = $1\", \"Alice\")\ndb.Delete(ctx, \"users\", \"id = $1\", userID)\ndb.Select(ctx, \"users\", []string{\"id\", \"name\"}, \"active = $1\", true)\n```\n\n### Soft delete\n\nFor tables with a `deleted_at` column, type-assert to `database.SoftDeleteDatabase`:\n\n```go\nsdb := db.(database.SoftDeleteDatabase)\nsdb.SoftDelete(ctx, \"users\", \"id = $1\", userID)  // sets deleted_at = now()\nsdb.Restore(ctx,     \"users\", \"id = $1\", userID)  // clears deleted_at\nsdb.HardDelete(ctx,  \"users\", \"id = $1\", userID)  // DELETE FROM …\nsdb.SelectDeleted(ctx, \"users\", cols, \"id = $1\", userID)\n```\n\nBoth `PostgresDB` and `SQLiteDB` implement `SoftDeleteDatabase`.\n\n---\n\n## Payments - Telegram Stars\n\n```go\nproduct := handlers.StarProduct{\n    Title:       \"Premium\",\n    Description: \"Unlock all features for 30 days\",\n    Payload:     \"premium_1month\",\n    Stars:       100,\n}\n\nb.Handle(\"/buy\",             handlers.SendInvoice(product))\nb.Handle(telebot.OnCheckout, handlers.PreCheckout())\nb.Handle(telebot.OnPayment,  handlers.PaymentSuccess(b.db))\n```\n\nRequired table:\n\n```sql\nCREATE TABLE IF NOT EXISTS payments (\n    id                 SERIAL PRIMARY KEY,\n    user_id            BIGINT NOT NULL,\n    telegram_charge_id TEXT   NOT NULL UNIQUE,\n    payload            TEXT   NOT NULL,\n    stars              INT    NOT NULL,\n    created_at         TIMESTAMP DEFAULT NOW()\n);\n```\n\n---\n\n## Redis\n\n```go\nredis.Set(ctx, \"key\", \"value\", ttlSeconds) // 0 = no expiry\nvalue, _ := redis.Get(ctx, \"key\")\nredis.Del(ctx, \"key\")\nexists, _ := redis.Exists(ctx, \"key\")\n\nn, _ := redis.Incr(ctx, \"counter\")\n\nredis.HSet(ctx, \"user:42\", \"name\", \"Alice\")\nname, _ := redis.HGet(ctx, \"user:42\", \"name\")\nall, _ := redis.HGetAll(ctx, \"user:42\")\n```\n\nUse `redis.NewMockClient()` in tests - no Redis server required.\n\n---\n\n## Testing\n\n```bash\n# All tests (Redis mocked - no server needed)\ngo test ./...\n\n# With real Redis (integration tests)\ndocker-compose up -d redis\ngo test ./...\n```\n\n---\n\n## Docker\n\n```bash\ndocker compose up -d        # start bot + Redis\ndocker compose logs -f bot  # tail logs\ndocker compose down         # stop\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filyabrin%2Ftgbase","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Filyabrin%2Ftgbase","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filyabrin%2Ftgbase/lists"}