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

500 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```go
// 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
```go
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
```go
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:
```go
func (b *Bot) SendMessage(ctx context.Context, p *SendMessageParams) (*Message, error) {
return call[*SendMessageParams, *Message](ctx, b, "sendMessage", p)
}
```
### 5.4 Errors
```go
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`)
```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/`)
```go
type Updater interface {
Updates() <-chan api.Update
Run(ctx context.Context) error
Stop(ctx context.Context) error
}
```
### 7.1 LongPoller
```go
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
```go
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/`)
```go
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`:
- **Simple** — `getMe` (no params, scalar response).
- **Typed-struct param** — `sendMessage` (struct in, object out).
- **Optional fields** — `sendMessage` with only required fields set; verify omitted fields do not appear in the request body.
- **Array result** — `getUpdates` (array of `Update`).
- **Bool result** — `setWebhook`.
- **Multipart upload** — `sendDocument` with an `InputFile` (verify content-type, boundary, field names).
- **OneOf union response** — `getChatMember` (returns `ChatMember` union).
- **OneOf union request** — `sendMediaGroup` (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.)