176 methods. 1408 generated tests. Zero any in the public surface.
package main
import (
"context"
"log"
"os"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/client"
"github.com/lukaszraczylo/go-telegram/dispatch"
)
func main() {
bot, _ := client.NewRetryDoer(os.Getenv("BOT_TOKEN"), nil)
d := dispatch.New(bot)
d.OnMessage(func(ctx context.Context, msg *api.Message) {
bot.SendMessage(ctx, &api.SendMessageParams{
ChatID: api.ChatIDFromInt(msg.Chat.ID),
Text: msg.Text,
})
})
if err := d.Run(context.Background()); err != nil {
log.Fatal(err)
}
}
Built for correctness, composability, and production reliability
IR + emitter pipeline runs weekly; a PR opens automatically for any Telegram-side change.
any in the public APITyped unions — ChatID, MessageOrBool — sealed interfaces with marker methods and auto-decode.
Drop in fasthttp, sonic, or goccy/go-json with a one-line swap.
Retry middleware honouring retry_after, panic recovery, structured errors with sentinels.
Generic Handler[T], composable filters, conversation handler with pluggable storage, per-update goroutine pool.
Every regen runs scrape → audit → emit → 1408 generated tests. Nothing ships without passing.
How go-telegram compares to other Go Telegram bot libraries
HTML scraper · this library
JSON spec · popular library
Hand-coded
| Feature | go-telegram | gotgbot/v2 | telegram-bot-api/v5 |
|---|---|---|---|
| Generated from spec | ✓ HTML scraper | ✓ JSON spec | ✗ hand-coded |
Typed unions (no any) |
✓ ChatID, MessageOrBool, sealed interfaces | △ partial | ✗ |
| Auto-generated tests | ✓ 1,408 (8 scenarios/method) | ✗ | ✗ |
| Conversation handler | ✓ pluggable storage | ✓ | ✗ |
| Retry middleware | ✓ honours retry_after | △ user-implemented | ✗ |
| Pluggable JSON codec | ✓ | ✗ | ✗ |
One command. No CGO, no system deps.
Requires Go 1.21+
go get github.com/lukaszraczylo/go-telegram
Full API reference
Auto-generated, browse on GitHub
Upstream spec this library tracks
Echo bot — the minimal working example
package main
import (
"context"
"log"
"os"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/client"
"github.com/lukaszraczylo/go-telegram/dispatch"
)
func main() {
// NewRetryDoer wraps the default transport with retry middleware
// that honours Telegram's retry_after field automatically.
bot, err := client.NewRetryDoer(os.Getenv("BOT_TOKEN"), nil)
if err != nil {
log.Fatal(err)
}
d := dispatch.New(bot)
// Handler[T] is generic — the type parameter is the concrete update type.
d.OnMessage(func(ctx context.Context, msg *api.Message) {
_, err := bot.SendMessage(ctx, &api.SendMessageParams{
// ChatIDFromInt returns a typed ChatID — no interface{} here.
ChatID: api.ChatIDFromInt(msg.Chat.ID),
Text: msg.Text,
})
if err != nil {
log.Printf("send error: %v", err)
}
})
if err := d.Run(context.Background()); err != nil {
log.Fatal(err)
}
}
The emitter scrapes the live Telegram Bot API docs (HTML), builds an intermediate representation (api.json), then generates Go code and tests. An audit step validates every method signature against the IR before emission.
make snapshot && make regen
14 runnable bots covering the most common patterns
Minimal echo bot — get up and running in 30 lines.
Inline keyboard buttons and callback query handling.
Inline mode queries and result sets.
Multi-step conversation flows with pluggable state storage.
Per-user state machine pattern.
Admin commands — ban, kick, restrict members.
Composable middleware chain — logging, auth, rate limiting.
Upload and download photos, documents, audio.
Webhook server instead of long-polling.
Send polls and handle poll-answer updates.
Telegram Payments — invoices, pre-checkout, successful payment.
Paginated inline keyboards for long result sets.
Greet new members joining a group or channel.
Automated content moderation with filter chains.
Power-user patterns — expand to read
The conversation handler chains multiple message steps and stores state between them. Any storage backend implementing the
StateStorage interface works — in-memory, Redis, Postgres.
conv := conversation.New(storage)
conv.AddState("ask_name", func(ctx context.Context, msg *api.Message) (string, error) {
bot.SendMessage(ctx, &api.SendMessageParams{
ChatID: api.ChatIDFromInt(msg.Chat.ID),
Text: "What's your name?",
})
return "ask_age", nil
})
conv.AddState("ask_age", func(ctx context.Context, msg *api.Message) (string, error) {
name := conv.GetData(ctx, "name")
// ... handle age input
return conversation.Done, nil
})
d.OnMessage(conv.Handler("start"))
Filters compose with And, Or, Not.
A filter is just a function func(*api.Update) bool.
// Only handle private messages from admins
adminOnly := filters.And(
filters.IsPrivate,
filters.UserIDIn(adminIDs...),
)
d.OnMessage(func(ctx context.Context, msg *api.Message) {
// handler body
}, adminOnly)
// Custom filter — any function works
isLong := func(u *api.Update) bool {
return u.Message != nil && len(u.Message.Text) > 200
}
d.OnMessage(handleLongMsg, isLong)
Pass a transport.Options to swap the HTTP client or JSON codec.
Useful for squeezing throughput on high-volume bots.
import (
"github.com/lukaszraczylo/go-telegram/client"
"github.com/lukaszraczylo/go-telegram/transport"
jsoniter "github.com/json-iterator/go"
)
opts := &client.Options{
Transport: transport.NewFasthttpTransport(nil),
Codec: transport.JSONCodec(jsoniter.ConfigCompatibleWithStandardLibrary),
}
bot, err := client.NewRetryDoer(token, opts)