Hermes
Guide
Welcome to the Cookbook.
Hermes is a fast, small Telegram Bot API library and framework for modern Go. It prioritizes zero-allocation dispatch, broad update decoding, and a fully typed API surface.
This guide will take you step-by-step from creating your first bot to building advanced stateful applications with streaming uploads and webhooks.
Creating bot with BotFather
Before you can interact with the Telegram API in Go, you must register your bot with Telegram.
-
Open Telegram and search for the official
@BotFatheraccount. -
Send the
/newbotcommand to begin the creation process. - Follow the prompts to choose a display name and a unique username (ending in 'bot') for your application.
-
Once completed, BotFather will give you an HTTP API Token (e.g.,
123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11).
Keep this token secret. In our examples, we will read it from the
BOT_TOKEN environment variable.
Writing your first bot
Start a new Go module and install Hermes:
go mod init mybot
go get github.com/sidwiskers/hermes
Create a main.go file. We will initialize the bot,
apply basic recovery and timeout middleware, register a simple
/start command, and begin long polling.
package main
import (
"context"
"log"
"os"
"os/signal"
"time"
"github.com/sidwiskers/hermes"
)
func main() {
// Setup graceful shutdown context
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// Initialize the bot with your token
bot := hermes.New(os.Getenv("BOT_TOKEN"))
// Apply standard middleware for stability
bot.Use(
hermes.Recover(),
hermes.Timeout(15*time.Second),
)
// Register a simple command route
bot.Command("start", func(c *hermes.Context) error {
return c.Send("Hello from hermes.")
})
// Start long-polling
if err := bot.Run(ctx); err != nil {
log.Fatal(err)
}
}
Run your bot using
BOT_TOKEN=your_token go run main.go. When you message
your bot with /start on Telegram, it will reply
immediately.
Sending requests
Hermes makes it simple to send complex messages, such as those with inline keyboards, HTML formatting, and ephemeral (private) visibility.
This example demonstrates how to send an ephemeral message with an inline button, and how to route the subsequent callback query when the user taps that button.
// Register the /profile command
bot.Command("profile", func(c *hermes.Context) error {
return c.Ephemeral(
"<b>Private profile</b>\nOnly you can see this.",
hermes.HTML,
hermes.WithKeyboard(hermes.Keyboard(
hermes.Row(hermes.Button("Refresh", "profile:refresh")),
)),
)
})
// Route button clicks matching the "profile:" prefix
bot.CallbackPrefix("profile:", func(c *hermes.Context) error {
// Answer the callback to remove the loading state on the user's button
return c.Answer("Updated")
})
Webhooks
While long-polling is great for development, production bots should generally use Webhooks. Hermes includes a built-in webhook server that safely validates Telegram's secret headers and manages graceful shutdowns.
const path = "/telegram"
secret := os.Getenv("WEBHOOK_SECRET")
publicURL := "https://yourdomain.com"
// 1. Tell Telegram where to send updates
if err := bot.SetWebhook(ctx, hermes.SetWebhookParams{
URL: publicURL + path,
SecretToken: secret,
}); err != nil {
log.Fatal(err)
}
// 2. Start the HTTP server to receive those updates
if err := bot.ServeWebhook(ctx, ":8080", path, hermes.WebhookOptions{
Secret: secret,
}); err != nil {
log.Fatal(err)
}
Finite-state conversations
Hermes provides optional packages for typed sessions and finite-state machines, allowing you to easily build multi-step conversational flows.
import (
"github.com/sidwiskers/hermes/fsm"
"github.com/sidwiskers/hermes/session"
)
type state uint8
const (
idle state = iota
waitingForName
)
type profile struct { Name string }
// Setup the memory store and FSM
store := session.NewMemory[fsm.Snapshot[state, profile]](24 * time.Hour)
sessions := session.New(store, session.ByChatUser, session.WithNamespace("profile"))
flow := fsm.New(sessions, idle)
// Define valid state transitions
flow.Add(fsm.Rule[state, profile]{From: idle, Event: "begin", To: waitingForName})
// Inject the FSM middleware
bot.Use(flow.Middleware())
// Start the flow via a command
bot.Command("profile", flow.Then("begin", func(c *hermes.Context) error {
return c.Send("What should I call you?")
}))
// Handle input when the user is in the 'waitingForName' state
bot.On(flow.In(waitingForName), func(c *hermes.Context) error {
name := strings.TrimSpace(c.Text())
// Save state and transition back to idle
flow.Set(c, fsm.Snapshot[state, profile]{State: idle, Data: profile{Name: name}})
return c.Send("Saved, " + name + ".")
})
Uploading files
New files are streamed directly from an io.Reader.
The full file is never buffered in memory, ensuring your bot
maintains a low memory footprint even when sending massive videos
or documents.
bot.Command("upload", func(c *hermes.Context) error {
chatID, _ := c.ChatID()
file, err := os.Open("document.pdf")
if err != nil {
return err
}
defer file.Close()
// Stream the file directly to Telegram
_, err = c.Bot.SendDocumentUpload(c, hermes.SendDocumentParams{
ChatID: chatID,
Caption: "Streamed by Hermes",
}, "document.pdf", file)
return err
})
Inline Queries
Hermes fully supports the inline bot API surface, allowing you to
answer queries when a user invokes your bot via
@yourbot in any chat.
bot.On(hermes.UpdateIs(hermes.UpdateInlineQuery), func(c *hermes.Context) error {
query := c.Update.InlineQuery
return c.Bot.AnswerInlineQuery(c, hermes.AnswerInlineQueryParams{
InlineQueryID: query.ID,
CacheTime: 10,
Results: []hermes.InlineQueryResult{
hermes.InlineQueryResultArticle{
ID: "hello",
Title: "Say hello",
InputMessageContent: hermes.InputTextMessageContent{
MessageText: "Hello from Hermes.",
},
},
},
})
})