Files
go-telegram/docs/superpowers/specs/2026-05-08-go-telegram-design.md
T
lukaszraczylo 9072e9eafb Initial release of go-telegram
A fully-generated, strongly-typed Go client for the Telegram Bot API.

* 176 methods + 301 types generated from Bot API v10.0
* 1408 auto-generated tests (8 scenarios per method)
* Typed unions throughout — no 'any' in the public surface
* Pluggable HTTP transport and JSON codec (default goccy/go-json)
* Built-in retry middleware honouring Telegram's retry_after
* Generic dispatcher with filters and conversation handlers
* Self-verifying codegen pipeline (regen → audit → emit → run tests)
* 14 example bots covering common patterns
2026-05-09 13:09:27 +01:00

21 KiB
Raw Blame History

go-telegram — Design Spec

  • Date: 2026-05-08
  • Module: github.com/lukaszraczylo/go-telegram
  • License: MIT
  • Status: Approved for planning
  • Author: Lukasz Raczylo (with Claude assistance)

1. Purpose & scope

A Go library for the Telegram Bot API, primarily a portfolio piece showcasing:

  • Codegen-driven full API coverage (parsed from https://core.telegram.org/bots/api)
  • Pragmatic Go generics
  • Pluggable HTTP transport and JSON codec for resource-conscious deployments
  • Long-poll and webhook update delivery behind a unified interface
  • A typed dispatcher/router for handlers, commands, and callbacks
  • Comprehensive testify-based unit tests with golden fixtures for codegen

Out of scope for v1: the daily auto-regen GitHub Action (deferred — see §11).

2. Non-goals

  • Competing on completeness/maturity with mymmrac/telego or go-telegram-bot-api/telegram-bot-api. We optimise for clarity and design.
  • Bot-framework features beyond the dispatcher (no plugin marketplace, no FSM, no scenes).
  • A documentation website. pkg.go.dev + README is sufficient.

3. Requirements

Functional

  1. Cover all Telegram Bot API methods and types via codegen.
  2. Support long-poll and webhook update delivery, both implementing one Updater interface.
  3. Allow the user to swap the HTTP client (e.g. valyala/fasthttp) and JSON codec (e.g. goccy/go-json).
  4. Provide a typed dispatcher with command, text-regex, callback, and inline-query matching plus generic middleware.
  5. Provide unit tests using stretchr/testify covering happy paths and explicit edge cases.
  6. Provide an optional integration test suite gated by build tag and env vars.

Non-functional

  • Lean dependency footprint (stdlib + golang.org/x/net/html + testify).
  • Deterministic, reproducible codegen (go test ./... -run TestGen is hermetic).
  • Generated files committed to the repo so consumers do not need to run codegen.
  • Doc comments on every exported symbol; generated types carry Telegram's verbatim prose.

4. Architecture

Two-stage codegen with a JSON intermediate representation:

HTML page    cmd/scrape    api.json (IR)    cmd/genapi    api/*.gen.go

The IR is committed; PRs from a future regen workflow show the diff against the previous IR, providing a readable changelog of Telegram-side changes.

4.1 Repository layout

go-telegram/
├── api/                  GENERATED  types + method wrappers (typed param structs)
│   ├── types.gen.go
│   ├── methods.gen.go
│   └── enums.gen.go
├── client/               HAND       Bot client, request building, error handling
│   ├── client.go
│   ├── codec.go          Codec interface + encoding/json default
│   ├── httpclient.go     HTTPDoer interface + net/http default
│   ├── errors.go         Typed APIError, NetworkError, ParseError
│   └── result.go         generic Result[T] decode
├── transport/            HAND       Updater abstraction
│   ├── updater.go        Updater interface
│   ├── longpoll.go       LongPoller
│   └── webhook.go        WebhookServer
├── dispatch/             HAND       Handler router
│   ├── router.go         Router, OnCommand/OnCallback/OnText
│   ├── middleware.go     Generic Middleware[T]
│   └── context.go        Per-update context
├── internal/
│   └── spec/             Shared IR types
│       ├── ir.go         Types describing parsed Telegram API
│       └── api.json      Committed golden IR (regenerated by scraper)
├── cmd/
│   ├── scrape/           HTML → api.json
│   └── genapi/           api.json → api/*.gen.go
├── examples/
│   ├── echo/             Long-poll echo bot
│   └── webhook/          Webhook bot with command router
├── testdata/
│   ├── html/             Golden HTML snapshots for scraper
│   ├── golden/           Expected api.json + emitted Go for codegen tests
│   └── responses/        Canned Telegram JSON responses
├── .github/workflows/
│   └── ci.yml            lint + test + codegen-clean check
├── Makefile              regen, test, lint targets
├── go.mod                module github.com/lukaszraczylo/go-telegram
├── LICENSE               MIT
└── README.md

5. Core types and client (client/)

5.1 Pluggability interfaces

// Codec is the JSON encoder/decoder. Default impl wraps encoding/json.
type Codec interface {
    Marshal(v any) ([]byte, error)
    Unmarshal(data []byte, v any) error
}

// HTTPDoer is the HTTP transport. Default is *http.Client.
type HTTPDoer interface {
    Do(req *http.Request) (*http.Response, error)
}

// Logger is a slog-shaped interface; nil-safe default writes nowhere.
type Logger interface {
    Debug(msg string, attrs ...any)
    Info(msg string, attrs ...any)
    Warn(msg string, attrs ...any)
    Error(msg string, attrs ...any)
}

5.2 Bot client

type Bot struct {
    token  string
    base   string  // https://api.telegram.org
    http   HTTPDoer
    codec  Codec
    logger Logger
}

type Option func(*Bot)
func WithHTTPClient(c HTTPDoer) Option
func WithCodec(c Codec) Option
func WithBaseURL(url string) Option
func WithLogger(l Logger) Option

func New(token string, opts ...Option) *Bot

Constructor-level functional options only; per-call params are typed structs (codegen-friendly).

5.3 Result envelope and call helper

type Result[T any] struct {
    OK          bool                `json:"ok"`
    Result      T                   `json:"result,omitempty"`
    ErrorCode   int                 `json:"error_code,omitempty"`
    Description string              `json:"description,omitempty"`
    Parameters  *ResponseParameters `json:"parameters,omitempty"`
}

// Single point for marshalling, URL signing, decoding, error mapping.
// Used by every generated method wrapper.
func call[Req, Resp any](ctx context.Context, b *Bot, method string, req Req) (Resp, error)

Generated wrappers stay thin:

func (b *Bot) SendMessage(ctx context.Context, p *SendMessageParams) (*Message, error) {
    return call[*SendMessageParams, *Message](ctx, b, "sendMessage", p)
}

5.4 Errors

type APIError struct {
    Code        int
    Description string
    Parameters  *ResponseParameters // retry_after, migrate_to_chat_id
}
func (e *APIError) Error() string
func (e *APIError) IsRetryable() bool        // 429, 5xx
func (e *APIError) RetryAfter() time.Duration

type NetworkError struct{ Err error }
type ParseError struct{ Err error; Body []byte }

Sentinel errors via errors.Is: ErrUnauthorized, ErrChatNotFound, ErrMessageNotModified, ErrTooManyRequests. Mapped from error_code plus description-prefix matching.

6. Codegen pipeline

6.1 Stage 1 — cmd/scrape/

  • Input: live URL https://core.telegram.org/bots/api (default) or a local HTML fixture (-input flag).
  • Parser: golang.org/x/net/html (no goquery).
  • Walk strategy: traverse <h4> headings sequentially. Lowercase first letter → method. Uppercase → type.
  • Following <p> until next heading → description.
  • Following <table> → fields/params (columns: Field|Type|Required|Description for types; Parameter|Type|Required|Description for methods).
  • Return type extracted by regex on description: Returns *X* on success, Returns an Array of X, Returns True on success.
  • Italic markers in the type column denote optional, array depth, and union-member candidates.
  • "Recent changes" section parsed for current API version.

6.2 Intermediate representation (internal/spec/ir.go)

type API struct {
    Version string
    Types   []TypeDecl
    Methods []MethodDecl
}
type TypeDecl struct {
    Name   string
    Doc    string
    Fields []Field
    OneOf  []string  // unions (InputMedia, ChatMember, …)
}
type MethodDecl struct {
    Name     string  // sendMessage
    Doc      string
    Params   []Field
    Returns  TypeRef
    HasFiles bool    // forces multipart
}
type Field struct {
    Name     string
    JSONName string
    Type     TypeRef
    Required bool
    Doc      string
}
type Kind int
const (
    KindPrimitive Kind = iota
    KindNamed
    KindArray
    KindOneOf
)
type TypeRef struct {
    Kind     Kind
    Name     string
    ElemType *TypeRef
    Variants []string
}

internal/spec/api.json is committed. Marshalling is stable (sorted fields, deterministic JSON output) so diffs read as a Telegram changelog.

6.3 Stage 2 — cmd/genapi/

  • Reads api.json.
  • Emits Go via text/template, finalised with go/format.
  • Templates:
    • types.tmpl → struct per TypeDecl. Optional fields are pointers (or omitempty for slices/maps). Doc comments verbatim from API.
    • enums.tmpl → string consts for known enumerations (parse modes, chat types, etc., extracted from doc prose).
    • oneof.tmpl → union types as interface { isFooBar() } plus concrete impls and a UnmarshalJSON that switches on a discriminator field (typically type or source).
    • methods.tmpl → param struct + thin Bot.<MethodName> wrapper using call[…].
    • multipart.tmpl → for methods with HasFiles, custom request builder using mime/multipart.
  • Header on every emitted file: // Code generated by cmd/genapi. DO NOT EDIT. + //go:build !ignore_autogenerated.

6.4 Makefile contract

The Makefile owns the codegen entry points; tools and CI call make, never raw go run:

Target What it does
make snapshot curl -fsSL https://core.telegram.org/bots/api > testdata/html/snapshot_<date>.html and update a latest.html symlink.
make regen Run scraper against testdata/html/latest.html, then run emitter. Writes internal/spec/api.json and api/*.gen.go.
make regen-from-fixture Same as make regen but pinned to testdata/html/snapshot_2026-05-08.html for deterministic CI checks.
make test go test -race ./...
make test-update-golden go test -run TestGen -update ./... to refresh golden fixtures.
make lint go vet + staticcheck.
make integration go test -tags=integration ./test/integration/... (requires env).

7. Transport (transport/)

type Updater interface {
    Updates() <-chan api.Update
    Run(ctx context.Context) error
    Stop(ctx context.Context) error
}

7.1 LongPoller

type LongPoller struct {
    Bot          *client.Bot
    Timeout      int               // seconds, default 30
    Limit        int               // 1..100, default 100
    AllowedTypes []api.UpdateType
    Backoff      BackoffStrategy
}

Calls getUpdates in a loop, tracks offset, applies exponential backoff on transient errors via BackoffStrategy.

7.2 WebhookServer

type WebhookServer struct {
    Bot         *client.Bot
    SecretToken string  // verify X-Telegram-Bot-Api-Secret-Token
    BufferSize  int
}
func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request)
func (w *WebhookServer) ListenAndServe(ctx context.Context, addr string) error

ServeHTTP lets users mount on their own router. ListenAndServe is a convenience for standalone use.

8. Dispatcher (dispatch/)

type Context struct {
    Ctx    context.Context
    Bot    *client.Bot
    Update *api.Update
    Values map[string]any  // matched groups, command args
}

type Handler[T any] func(ctx *Context, payload T) error
type Middleware[T any] func(Handler[T]) Handler[T]

type Router struct{ /* … */ }

func New(bot *client.Bot) *Router

func (r *Router) OnCommand(cmd string, h Handler[*api.Message])
func (r *Router) OnText(pattern string, h Handler[*api.Message])
func (r *Router) OnCallback(pattern string, h Handler[*api.CallbackQuery])
func (r *Router) OnInlineQuery(h Handler[*api.InlineQuery])
func (r *Router) OnEditedMessage(h Handler[*api.Message])
func (r *Router) Use(mw Middleware[*api.Update])

func (r *Router) Run(ctx context.Context, u transport.Updater) error

Matchers run in registration order; first match wins. A panic-recovery middleware is registered automatically. Generic Handler[T] keeps payloads precisely typed in user code.

9. Testing strategy

9.1 Unit tests (every package, fast, no network)

  • testify/require for assertions, testify/mock on client.HTTPDoer.
  • Edge cases explicitly covered:
    • API error decode for 429 with retry_after*APIError.IsRetryable()=true, RetryAfter()=N.
    • Network error from transport → wrapped *NetworkError.
    • Malformed JSON → *ParseError.
    • Void-result methods (setWebhook returning bool).
    • Optional pointer fields nil round-trip.
    • OneOf union types unmarshal via discriminator.
    • Multipart upload path for methods with InputFile.
    • Context cancellation mid-call returns ctx.Err().
    • Long-poller backoff after transient error.
    • Webhook secret-token mismatch → 401.
    • Webhook handles oversized body, malformed JSON, wrong content-type.
    • Router: command match with/without bot mention (/start@MyBot), regex matchers, panic recovery, middleware ordering, no-match update.

9.2 Codegen golden tests

testdata/html/
  ├── snapshot_2026-05-08.html       full Bots API page snapshot
  └── small_fixture.html             hand-crafted minimal page (1 type, 1 method)
testdata/golden/
  ├── api.json                       expected IR from snapshot
  └── *.gen.go                       expected emitted Go
  • cmd/scrape test: parse fixture → compare to golden api.json.
  • cmd/genapi test: read golden api.json → compare emitted Go to golden *.gen.go.
  • -update flag (custom in internal/testutil) regenerates goldens deliberately.

9.3 Optional integration suite

  • Build tag //go:build integration.
  • Skipped by default go test ./....
  • Activated by go test -tags=integration ./test/integration/....
  • Requires TELEGRAM_BOT_TOKEN (and TELEGRAM_TEST_CHAT_ID where applicable).
  • Covers getMe, sendMessage, setWebhook/deleteWebhook, getUpdates loop with short timeout.
  • Not part of default CI to avoid flakes.

10. CI

.github/workflows/ci.yml (every push and PR):

  • actions/setup-go matrix: 1.23, 1.24
  • go vet ./...
  • staticcheck ./...
  • go test -race -coverprofile=coverage.out ./...
  • Codegen-clean check: make regen-from-fixture + git diff --exit-code to assert generated files match the committed IR for the snapshot fixture (deterministic).
  • Upload coverage artifact.

11. Handling API changes & test maintenance

Telegram ships changes to the Bot API roughly every 13 months: new methods, new types, added optional fields, occasional removals or renames, occasional union-variant additions. The design must absorb these with minimum manual work.

11.1 Test-suite invariants under change

Tests are layered so that the cost of an API change is bounded.

Test layer Affected by API change? Cost
client/ unit tests (call helper, error mapping, codec, multipart builder) No — they target call[Req,Resp], not specific methods. Zero.
transport/ unit tests (long-poll loop, webhook server) No — they target update plumbing, not payload fields. Zero.
dispatch/ unit tests (matchers, middleware, router) No — generic over *api.Update. Zero.
Codegen golden tests (cmd/scrape, cmd/genapi) Yes — golden api.json and *.gen.go will diff. Refresh goldens deliberately (go test -run TestGen -update).
Codegen "shape" smoke tests Only if a code-generation pattern changes. One test per pattern, not per method.
Examples (examples/echo, examples/webhook) Only if they reference a removed/renamed symbol. Hand-fix; rare.
Integration suite (build tag integration) Only for the ~5 methods it touches. Hand-fix on removal/rename; rare.

The deliberate invariant: we do not write a test per generated method. All ~100+ generated wrappers funnel through call[Req,Resp] and the multipart builder; coverage of those two paths covers them all. Each generated method is then sanity-checked by being type-checked at compile time against the IR.

11.2 Shape smoke tests

In api/api_test.go, one test per code-generation pattern, hitting a representative method through a mocked HTTPDoer:

  • SimplegetMe (no params, scalar response).
  • Typed-struct paramsendMessage (struct in, object out).
  • Optional fieldssendMessage with only required fields set; verify omitted fields do not appear in the request body.
  • Array resultgetUpdates (array of Update).
  • Bool resultsetWebhook.
  • Multipart uploadsendDocument with an InputFile (verify content-type, boundary, field names).
  • OneOf union responsegetChatMember (returns ChatMember union).
  • OneOf union requestsendMediaGroup (accepts []InputMedia union).

If new code-generation patterns appear (Telegram introducing a new shape we have not seen), one new shape test is added — not one per affected method.

11.3 Categories of change and how each is absorbed

Change Pipeline effect Test effect
New type Appears in api.json, emitted into types.gen.go. Golden diff only. Refresh.
New optional field Same. Golden diff only.
New required field Same. Breaking for users who construct that struct literally. Golden diff only; example code may need update.
Removed type/method/field Disappears from emitted Go. Breaking for users referring to it. Golden diff. Integration test or example may break — fix or skip.
Renamed field Old name disappears, new appears. Same as above; no automatic rename (we treat as remove + add).
New method Wrapper generated; no new test required (shape tests cover the call path). Golden diff.
Return type changed for an existing method Wrapper signature changes. Breaking. Golden diff; integration test for that method may break.
New OneOf variant UnmarshalJSON switch grows a case. Golden diff. If a brand-new variant style appears, may require scraper work and a new shape test.
Telegram doc layout change Scraper may misparse. Scraper unit test against the new HTML fixture should be added before regenerating.

11.4 The change procedure

When the scraper output changes:

  1. Run make regen-from-fixture against the current testdata/html/snapshot_*.html (deterministic check) — confirm zero unrelated diffs.
  2. Capture a fresh HTML snapshot: make snapshot (writes testdata/html/snapshot_<date>.html).
  3. Run make regen against the new snapshot → IR diff appears in internal/spec/api.json.
  4. Review the IR diff as a Telegram changelog. This is the human read-through; it is the entire point of having an IR.
  5. Run go test ./... → expect golden codegen diffs.
  6. Refresh goldens: go test -run TestGen -update.
  7. Re-run go test ./... → green.
  8. If shape tests reveal a new code-generation pattern (e.g. a never-before-seen union shape), extend templates and add a shape test before refreshing goldens.
  9. If examples/ or integration tests reference a removed symbol, fix them.
  10. Commit with a clear message: chore(api): regenerate from Telegram Bot API vX.Y plus a bullet list extracted from the IR diff (added/changed/removed types and methods).

This is the same procedure the future auto-regen workflow will run; doing it by hand first ensures the workflow has nothing surprising to do.

11.5 Versioning policy

  • Library SemVer is decoupled from Telegram's API version.
  • Telegram-side additions → minor bump.
  • Telegram-side removals or signature changes → major bump (we do not preserve removed symbols as deprecated stubs; the breaking change ships).
  • Bug fixes in hand-written code → patch bump.
  • Each release records the Telegram API version it was generated against in the release notes and in a // Generated from Bot API vX.Y constant in api/version.gen.go.

12. Future work (deferred)

  • Auto-regen workflow — daily cron + workflow_dispatch that runs cmd/scrape against the live URL, regenerates code, opens a PR with a diff summary, and auto-merges on green CI. Implementation sketch retained in design discussion; not part of v1 acceptance.
  • Release workflow — tag-triggered goreleaser pipeline producing GH Releases. SemVer for the library; Telegram's API version recorded in release notes.
  • Additional codecs/HTTP adapters as separate sub-packages or contrib modules so users can opt in without bringing transitive deps into the core.

13. Dependency policy

Production:

  • Go standard library
  • golang.org/x/net/html (scraper only)

Test-only:

  • github.com/stretchr/testify

Explicit non-deps: goquery, cobra, third-party logging, third-party HTTP clients, third-party JSON codecs (these are user-supplied via HTTPDoer and Codec).

14. Acceptance criteria

v1 is done when:

  1. go test ./... passes on a clean checkout with no env vars.
  2. make regen produces zero diff against the committed api.json and api/*.gen.go when run against the committed HTML fixture.
  3. examples/echo and examples/webhook build and the echo example runs end-to-end against a real bot when TELEGRAM_BOT_TOKEN is set.
  4. go vet, staticcheck, and go test -race are clean.
  5. Every exported symbol in hand-written packages has a doc comment.
  6. README covers Why, Install, Quick Start, Custom HTTP/JSON, Webhooks, Dispatcher, Updating, Contributing.
  7. The integration test suite (-tags=integration) runs cleanly when env is provided.

15. Open questions

None at sign-off. (Auto-regen behaviour intentionally deferred.)