A Go client library for the Telegram Bot API with auto-generated types and methods.
Built with fasthttp and sonic for high performance.
- Auto-generated from the official Telegram Bot API spec
- Fully typed — every API type is a Go struct, every method is a typed function
- Middleware framework — composable middleware chain with filters
- Typed handlers —
OnMessage,OnCallbackQuery, etc. - Bound methods —
ctx.Reply,ctx.ReplyWithPhoto,ctx.EditMessageText, etc. - Webhook support — adapters for net/http, Fiber, Gin, Echo
- Session management — pluggable storage with typed helpers
- Rate limiting — token bucket, sliding window, fixed window
- i18n — translations with pluralization support
- File uploads — supports file_id, URL, and multipart upload
- Error handling — structured
APIErrorwith error codes and retry info - High performance — fasthttp + sonic for minimal allocations
package main
import (
"context"
"log"
"os"
"os/signal"
"github.com/pageton/botx/tg"
)
func main() {
bot := tg.NewBot(os.Getenv("BOT_TOKEN"))
bot.On(tg.OnCommand("start"), func(ctx *tg.Context) {
ctx.Reply("Welcome!")
})
bot.On(tg.OnText(), func(ctx *tg.Context) {
ctx.Reply("Echo: " + ctx.MessageText())
})
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
bot.Start(ctx)
}Use typed handlers for cleaner code with guaranteed non-nil objects:
bot.OnMessage(func(ctx *tg.Context, msg *tg.Message) {
// msg is guaranteed non-nil
ctx.Reply("Got message: " + *msg.Text)
})
bot.OnCallbackQuery(func(ctx *tg.Context, cb *tg.CallbackQuery) {
// cb is guaranteed non-nil
data := ""
if cb.Data != nil {
data = *cb.Data
}
ctx.AnswerCallback("You clicked: " + data, false)
})
bot.OnInlineQuery(func(ctx *tg.Context, q *tg.InlineQuery) { ... })
bot.OnPoll(func(ctx *tg.Context, p *tg.Poll) { ... })
bot.OnChatMember(func(ctx *tg.Context, m *tg.ChatMemberUpdated) { ... })Context methods that automatically use the current chat/message:
// Messages
ctx.Reply("text")
ctx.ReplyWithPhoto(photo)
ctx.ReplyWithDocument(document)
ctx.ReplyWithVideo(video)
ctx.ReplyWithAudio(audio)
ctx.ReplyWithVoice(voice)
ctx.ReplyWithAnimation(animation)
ctx.ReplyWithVideoNote(videoNote)
ctx.ReplyWithMediaGroup(media)
ctx.ReplyWithLocation(lat, lng)
ctx.ReplyWithVenue(lat, lng, title, address)
ctx.ReplyWithContact(phone, name)
ctx.ReplyWithPoll(question, options)
ctx.ReplyWithDice()
ctx.ReplyWithChatAction("typing")
// Message operations
ctx.ForwardMessage(toChatID, msgID)
ctx.CopyMessage(toChatID, msgID)
ctx.EditMessageText("new text")
ctx.EditMessageCaption(tg.EditCaptionParams{Caption: "new"})
ctx.EditMessageMedia(media)
ctx.EditMessageReplyMarkup(keyboard)
ctx.DeleteMessage()
// Chat management
ctx.BanChatMember(userID)
ctx.UnbanChatMember(userID)
ctx.RestrictChatMember(userID, permissions)
ctx.PromoteChatMember(userID)
ctx.SetChatPermissions(permissions)
ctx.ExportChatInviteLink()
ctx.SetChatPhoto(photo)
ctx.SetChatTitle(title)
ctx.SetChatDescription(desc)
ctx.PinChatMessage(msgID)
ctx.UnpinChatMessage(msgID)
ctx.LeaveChat()
ctx.GetChat()
ctx.GetChatMemberCount()
ctx.GetChatMember(userID)
// Forum topics
ctx.CreateForumTopic(name)
ctx.EditForumTopic(threadID)
ctx.CloseForumTopic(threadID)
ctx.ReopenForumTopic(threadID)
ctx.DeleteForumTopic(threadID)ctx.UpdateID() // update identifier
ctx.ChatID() // current chat ID
ctx.User() // *User from any update type
ctx.Message() // *Message from any message update
ctx.MessageText()import (
"net/http"
"github.com/pageton/botx/tg"
)
bot := tg.NewBot(token)
bot.On(tg.OnCommand("start"), func(ctx *tg.Context) {
ctx.Reply("Hello!")
})
handler := tg.HTTPHandler(bot, tg.WebhookOptions{
SecretToken: "my_secret",
})
http.Handle("/webhook", handler)
http.ListenAndServe(":8080", nil)import "github.com/gofiber/fiber/v2"
app := fiber.New()
handler := tg.FiberHandler(bot, tg.WebhookOptions{})
app.Post("/webhook", handler)
app.Listen(":8080")import "github.com/gin-gonic/gin"
r := gin.Default()
handler := tg.GinHandler(bot, tg.WebhookOptions{})
r.POST("/webhook", handler)
r.Run(":8080")import "github.com/labstack/echo/v4"
e := echo.New()
handler := tg.EchoHandler(bot, tg.WebhookOptions{})
e.POST("/webhook", handler)
e.Start(":8080")bot.SetWebhook("https://example.com/webhook", &tg.SetWebhookParams{
SecretToken: "my_secret",
})import "github.com/pageton/botx/tg"
// In-memory storage (dev only)
storage := tg.NewMemoryStorage()
bot := tg.NewBot(token)
bot.Use(tg.Session(storage))
type MySession struct {
Count int `json:"count"`
Name string `json:"name"`
}
bot.On(tg.OnText(), func(ctx *tg.Context) {
var sess MySession
tg.GetSession(ctx, &sess)
sess.Count++
tg.SetSession(ctx, sess)
ctx.Reply(fmt.Sprintf("Count: %d", sess.Count))
})// JSON file storage
import jsonstore "github.com/pageton/botx/ext/session/json"
storage, _ := jsonstore.New("./sessions")
// Redis storage
import redisstore "github.com/pageton/botx/ext/session/redis"
storage := redisstore.New(redisClient, redisstore.WithPrefix("bot:"))
// SQLite storage
import sqlitestore "github.com/pageton/botx/ext/session/sqlite"
storage, _ := sqlitestore.New("./sessions.db")
// MongoDB storage
import mongostore "github.com/pageton/botx/ext/session/mongo"
storage := mongostore.New(mongoClient, "mydb", "sessions")bot.Use(tg.Session(storage, tg.WithSessionKey(func(ctx *tg.Context) string {
user := ctx.User()
if user != nil {
return fmt.Sprintf("user:%d", user.ID)
}
return ""
})))import "github.com/pageton/botx/ext/middleware/ratelimit"
// Token bucket: 1 token/sec, burst of 5
limiter := ratelimit.NewTokenBucket(1, 5)
// Sliding window: 10 requests per minute
limiter := ratelimit.NewSlidingWindow(10, time.Minute)
// Fixed window: 30 requests per minute
limiter := ratelimit.NewFixedWindow(30, time.Minute)
// Apply middleware
bot.Use(ratelimit.Middleware(limiter,
ratelimit.WithKeyFunc(ratelimit.ByUserID()),
ratelimit.WithOnLimit(func(ctx *tg.Context) {
ctx.Reply("Slow down!")
}),
))import "github.com/pageton/botx/ext/middleware/i18n"
// Create translator
tr := i18n.New(i18n.WithFallback("en"))
// Load translations
tr.LoadDir("./locales")
// Add middleware
bot.Use(i18n.Middleware(tr, i18n.ByUserLanguage("en")))
// Use in handlers
bot.On(tg.OnCommand("start"), func(ctx *tg.Context) {
t := i18n.TFunc(ctx)
ctx.Reply(t("welcome", map[string]string{
"name": ctx.User().FirstName,
}))
})
// Pluralization
bot.On(tg.OnCommand("items"), func(ctx *tg.Context) {
p := i18n.PluralFunc(ctx)
ctx.Reply(p("items", 5))
})
// Change locale
bot.On(tg.OnCommand("lang"), func(ctx *tg.Context) {
i18n.SetLocale(ctx, "ru")
ctx.Reply(i18n.T(ctx, "lang_changed"))
})locales/en.json:
{
"welcome": "Hello, {{name}}!",
"items_zero": "
7B98
span>No items",
"items_one": "{{count}} item",
"items_other": "{{count}} items"
}locales/ru.json:
{
"welcome": "Привет, {{name}}!",
"items_zero": "Нет элементов",
"items_one": "{{count}} элемент",
"items_few": "{{count}} элемента",
"items_many": "{{count}} элементов"
}OnMessage()— matches messagesOnEditedMessage()— matches edited messagesOnChannelPost()— matches channel postsOnCallbackQuery()— matches callback queriesOnText()— matches text messagesOnCommand("start")— matches /start commandsOnCallbackData("btn")— matches callback queries with specific dataOnPhoto()— matches photo messagesOnInlineQuery()— matches inline queriesOnPoll()— matches pollsOnChatMember()— matches chat member updatesAnd(f1, f2)— logical ANDOr(f1, f2)— logical ORNot(f)— logical NOT
If you don't need the framework, use the API client directly:
client := tg.New("YOUR_BOT_TOKEN")
user, err := client.GetMe()
if err != nil {
log.Fatal(err)
}make generatecmd/tg-gen/ CLI tool for code generation
internal/model/ Intermediate representation
internal/parser/ HTML spec parser
internal/generator/ Go code generator
tg/ Public API: client + generated types/methods + bot framework
ext/
├── session/
│ ├── json/ JSON file session storage
│ ├── redis/ Redis session storage
│ ├── sqlite/ SQLite session storage
│ └── mongo/ MongoDB session storage
└── middleware/
├── ratelimit/ Rate limiting middleware
└── i18n/ Internationalization middleware
examples/ Example bots
make testMIT