Skip to content

Middleware Bot ​

MTGo has two middleware layers: invoker middleware wraps outgoing RPC calls, and handler middleware wraps incoming update dispatch. This example demonstrates both.

Full Code ​

go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/mtgo-labs/middlewares/floodwait"
	"github.com/mtgo-labs/middlewares/ratelimit"
	"github.com/mtgo-labs/mtgo/telegram"
	"github.com/mtgo-labs/mtgo/telegram/params"
	"github.com/mtgo-labs/mtgo/telegram/types"
)

func main() {
	apiID := mustEnv("API_ID")
	apiHash := mustEnv("API_HASH")
	botToken := mustEnv("BOT_TOKEN")

	client, err := telegram.NewClient(mustAtoi(apiID), apiHash, &telegram.Config{
		BotToken:    botToken,
		SessionName: "middleware_bot",
		SavePeers:   true,
		ParseMode:   telegram.HTML,
	})
	if err != nil {
		log.Fatalf("new client: %v", err)
	}

	// ── Invoker middleware: rate limiting ──────────────────────────
	// Allow at most 20 RPC calls/sec with a burst of 5.
	limiter := ratelimit.New(20, 5)
	client.UseInvokerMiddleware(limiter.Middleware())

	// ── Invoker middleware: flood-wait retry ───────────────────────
	// Automatically sleeps and retries when Telegram returns FLOOD_WAIT.
	waiter := floodwait.New()
	waiter.OnWait(func(d time.Duration) {
		log.Printf("flood wait: sleeping %v", d)
	})
	waiter.WithMaxWait(60 * time.Second)
	client.UseInvokerMiddleware(waiter.Middleware())

	// ── Handler middleware: logging (priority -10, runs first) ─────
	client.UseMiddleware(func(next telegram.Handler) telegram.Handler {
		return &telegram.FuncHandler{Fn: func(ctx *telegram.Context) {
			if ctx.Message != nil {
				from := ctx.Message.ChatID
				text := ctx.Message.Text
				log.Printf("[%d] %s", from, text)
			}
			next.Handle(ctx)
		}}
	}, -10)

	// ── Handler middleware: admin guard (priority 0) ──────────────
	adminID := int64(0)
	if envAdmin := os.Getenv("ADMIN_ID"); envAdmin != "" {
		adminID = mustAtoi64(envAdmin)
	}
	if adminID != 0 {
		client.UseMiddleware(func(next telegram.Handler) telegram.Handler {
			return &telegram.FuncHandler{Fn: func(ctx *telegram.Context) {
				if ctx.Message != nil && ctx.Message.FromID != adminID {
					ctx.Reply("Unauthorized")
					ctx.Stopped = true
					return
				}
				next.Handle(ctx)
			}}
		})
	}

	// ── Handlers ───────────────────────────────────────────────────
	client.OnMessage(func(client *telegram.Client, msg *types.Message) {
		if msg == nil || msg.Text == "" {
			return
		}

		switch msg.Text {
		case "/start":
			msg.Reply(
				"<b>Middleware Bot</b>\n\n"+
					"This bot demonstrates mtgo middleware:\n"+
					"• Rate limiting (invoker middleware)\n"+
					"• Flood-wait retry (invoker middleware)\n"+
					"• Request logging (handler middleware)\n"+
					"• Admin guard (handler middleware)\n\n"+
					"Commands: /start /ping /stats",
				&params.SendMessage{ParseMode: params.ParseModeHTML},
			)

		case "/ping":
			msg.Reply("pong")

		case "/stats":
			info := fmt.Sprintf(
				"Rate limiter: 20 tokens/sec, burst 5\nFlood-wait: max wait %v, max retries 5",
				60*time.Second,
			)
			msg.Reply(info)
		}
	}, telegram.Private)

	if err := client.Connect(0); err != nil {
		log.Fatalf("connect: %v", err)
	}
	defer client.Stop()

	bot, err := client.GetMe(context.Background())
	if err != nil {
		log.Fatalf("get me: %v", err)
	}
	fmt.Printf("@%s is running — press Ctrl+C to stop\n", bot.Username)

	client.Idle()
}

func mustEnv(key string) string {
	v := os.Getenv(key)
	if v == "" {
		log.Fatalf("environment variable %s is required", key)
	}
	return v
}

func mustAtoi(s string) int32 {
	var n int32
	if _, err := fmt.Sscanf(s, "%d", &n); err != nil {
		log.Fatalf("invalid integer %q: %v", s, err)
	}
	return n
}

func mustAtoi64(s string) int64 {
	var n int64
	if _, err := fmt.Sscanf(s, "%d", &n); err != nil {
		log.Fatalf("invalid integer %q: %v", s, err)
	}
	return n
}

How It Works ​

Two Middleware Types ​

MTGo provides two distinct middleware layers:

TypeWrapsUse for
Invoker middlewareOutgoing RPC callsRate limiting, flood wait retry, logging, metrics
Handler middlewareIncoming update dispatchAuth, logging, request transformation

Invoker Middleware ​

Invoker middleware wraps every outgoing Telegram API call:

go
client.UseInvokerMiddleware(limiter.Middleware())
client.UseInvokerMiddleware(waiter.Middleware())

Invoker middlewares are chained in registration order. Each receives the next invoker as a parameter, so it can inspect/modify the request and response.

Rate limiter (ratelimit.New(rate, burst)): throttles outgoing calls to rate per second with a burst allowance. Prevents hitting Telegram's API limits.

Flood wait (floodwait.New()): catches FLOOD_WAIT errors and automatically retries after the required delay. OnWait logs the delay. WithMaxWait sets the maximum wait time before giving up.

Handler Middleware ​

Handler middleware wraps the update dispatch chain:

go
client.UseMiddleware(func(next telegram.Handler) telegram.Handler {
    return &telegram.FuncHandler{Fn: func(ctx *telegram.Context) {
        // before
        next.Handle(ctx)
        // after
    }}
}, priority)
  • Wraps next handler in a new handler
  • Can inspect/modify ctx before calling next.Handle(ctx)
  • Can short-circuit by not calling next.Handle(ctx) or setting ctx.Stopped = true
  • priority controls execution order (lower runs first, same as handler groups)

Combining Both Layers ​

Incoming Update
  → Handler middleware (logging)     // -10
  → Handler middleware (auth guard)  // 0
  → Handler (command logic)         // 10
      ↓ makes RPC calls ↓
  → Invoker middleware (rate limit)
  → Invoker middleware (flood wait)
  → Telegram API

Running ​

bash
export API_ID=12345
export API_HASH=your_api_hash
export BOT_TOKEN=your_bot_token
export ADMIN_ID=your_telegram_user_id  # optional: enables admin guard

go run main.go

All incoming messages are logged. RPC calls are rate-limited to 20/sec and automatically retried on flood wait. If ADMIN_ID is set, only that user can interact with the bot.

Released under the Apache-2.0 License.