Skip to main content

Overview

The Telegram bot service is a standalone Go application that handles bidirectional communication between PriceSignal and Telegram users. It sends price alerts to users and captures chat IDs for new subscribers.

Architecture

Implemented in src/telegram-bot/main.go, the service runs as an independent microservice that communicates with the main application via NATS JetStream.

Dependencies

import (
    tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
    "github.com/joho/godotenv"
    "github.com/nats-io/nats.go"
    "github.com/nats-io/nats.go/jetstream"
)

Data Structures

ChatIDMessage

Sent to the main application when users register:
type ChatIDMessage struct {
    ChatID   int64  `json:"chat_id"`
    Username string `json:"username"`
    UserId   string `json:"user_id"`
}

Notification

Received from the main application to send alerts:
type Notification struct {
    ChatID  int64  `json:"chat_id"`
    Message string `json:"message"`
}

Configuration

The service requires two environment variables:
TELEGRAM_BOT_TOKEN=your_bot_token_here
NATS_URL=nats://localhost:4222

Loading Environment

_ = godotenv.Load()

botToken := os.Getenv("TELEGRAM_BOT_TOKEN")
if botToken == "" {
    log.Fatal("TELEGRAM_BOT_TOKEN must be set")
}

natsURL := os.Getenv("NATS_URL")
if natsURL == "" {
    log.Fatal("NATS_URL must be set")
}

NATS JetStream Setup

Connection and Stream Creation

// Connect to NATS server
nc, err := nats.Connect(natsURL)
if err != nil {
    log.Fatalf("Error connecting to NATS server: %v", err)
}
defer nc.Close()

// Create JetStream context
js, err := jetstream.New(nc)
if err != nil {
    log.Fatal(err)
}

ctx := context.Background()

// Create or update the notifications stream
s, err := js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
    Name:     "notifications",
    Subjects: []string{"notifications.>"},
})
if err != nil {
    log.Fatal(err)
}

Telegram Bot Initialization

bot, err := tgbotapi.NewBotAPI(botToken)
if err != nil {
    log.Fatalf("Error creating new bot: %v", err)
}

log.Printf("Authorized on account %s", bot.Self.UserName)

Notification Consumer

The service consumes notifications from NATS and sends them to Telegram users:
c, _ := s.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{
    Durable:       "telegram",
    FilterSubject: "notifications.telegram",
    AckPolicy:     jetstream.AckExplicitPolicy,
})

c.Consume(func(msg jetstream.Msg) {
    var notification Notification
    err := json.Unmarshal(msg.Data(), &notification)
    if err != nil {
        log.Printf("Error unmarshaling message: %v", err)
        return
    }

    // Send message to Telegram user
    telegramMsg := tgbotapi.NewMessage(notification.ChatID, notification.Message)
    _, err = bot.Send(telegramMsg)
    if err != nil {
        log.Printf("Error sending message: %v", err)
    }
    
    // Acknowledge message processing
    msg.Ack()
})

User Registration Flow

Handles incoming messages from users to capture their chat ID:
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)

for update := range updates {
    if update.Message == nil {
        continue
    }

    chatID := update.Message.Chat.ID
    username := update.Message.From.UserName
    userId := update.Message.CommandArguments()

    // Prepare chat ID message for main application
    chatIDMessage := ChatIDMessage{
        ChatID:   chatID,
        Username: username,
        UserId:   userId,
    }
    
    data, err := json.Marshal(chatIDMessage)
    if err != nil {
        log.Printf("Error marshaling message: %v", err)
        continue
    }

    // Publish to NATS for processing by main app
    _, err = js.PublishAsync("notifications.init.telegram", data)
    if err != nil {
        log.Printf("Error publishing message: %v", err)
    }

    // Respond to user
    msg := tgbotapi.NewMessage(chatID, "Hello, your chat ID has been recorded!")
    bot.Send(msg)
}

Graceful Shutdown

sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig

Message Flow

Outbound (Alerts to Users)

  1. Main application publishes notification to notifications.telegram
  2. Telegram bot consumes message from JetStream
  3. Bot sends message to user’s chat ID
  4. Message acknowledged in NATS

Inbound (User Registration)

  1. User sends message to bot
  2. Bot captures chat ID and username
  3. Bot publishes to notifications.init.telegram
  4. Main application processes and stores user info
  5. Bot confirms registration to user

Deployment

Building the Service

cd src/telegram-bot
go build -o telegram-bot main.go

Running the Service

export TELEGRAM_BOT_TOKEN=your_token
export NATS_URL=nats://localhost:4222
./telegram-bot

Docker Deployment

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o telegram-bot main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/telegram-bot .
CMD ["./telegram-bot"]

Error Handling

  • Failed message sends are logged but don’t stop the service
  • Malformed notifications are logged and skipped
  • Connection errors cause the service to exit (managed by orchestrator)

Build docs developers (and LLMs) love