package main
import (
"context"
"fmt"
"log"
"sync"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// TelegramBotConfig - конфигурация для телеграм бота
type TelegramBotConfig struct {
Token string
ChatIDs []int64
}
// NotificationBot - бот для отправки уведомлений
type NotificationBot struct {
api *tgbotapi.BotAPI
chatIDs []int64
mu sync.RWMutex
lastSummaryMsg map[int64]int // chatID -> last summary message ID
db *DB
}
// NewNotificationBot создает новый экземпляр бота
func NewNotificationBot(cfg TelegramBotConfig, db *DB) (*NotificationBot, error) {
api, err := tgbotapi.NewBotAPI(cfg.Token)
if err != nil {
return nil, fmt.Errorf("failed to create bot: %w", err)
}
log.Printf("Telegram bot authorized as %s", api.Self.UserName)
return &NotificationBot{
api: api,
chatIDs: cfg.ChatIDs,
db: db,
lastSummaryMsg: make(map[int64]int),
}, nil
}
// Start запускает отправку сводок с заданным интервалом
func (b *NotificationBot) Start(ctx context.Context, intervalSeconds int) {
ticker := time.NewTicker(time.Duration(intervalSeconds) * time.Second)
defer ticker.Stop()
// Отправляем приветствие при старте
b.sendStartupMessage()
// Отправляем первую сводку сразу
b.SendSummary()
for {
select {
case <-ctx.Done():
log.Println("Telegram bot stopping...")
return
case <-ticker.C:
b.SendSummary()
}
}
}
func (b *NotificationBot) sendStartupMessage() {
msg := "🤖 *Status Monitor Bot Started*\n\n" +
"Sending status updates every 30 seconds.\n" +
"Messages will be updated automatically."
for _, chatID := range b.chatIDs {
b.sendMessage(chatID, msg, true)
}
}
// SendSummary отправляет текущую сводку по всем серверам
func (b *NotificationBot) SendSummary() {
servers, err := b.db.ListServers(true)
if err != nil {
log.Printf("Failed to list servers: %v", err)
return
}
// Получаем статистику за последние 5 минут
stats, err := b.db.ServerStats(time.Now().Add(-5 * time.Minute))
if err != nil {
log.Printf("Failed to get stats: %v", err)
return
}
// Формируем сообщение
message := b.formatSummary(servers, stats)
// Отправляем или обновляем сообщение в каждом чате
for _, chatID := range b.chatIDs {
b.updateOrSendMessage(chatID, message)
}
}
func (b *NotificationBot) formatSummary(servers []Server, stats map[int]ServerStat) string {
var upCount, downCount, degradedCount int
var totalResponseTime int
var serversWithData int
// Строим таблицу статусов
statusTable := ""
for _, sv := range servers {
var status string
var statusIcon string
var responseTime int
if st, ok := stats[sv.ID]; ok && st.Total > 0 {
uptime := float64(st.Successes) / float64(st.Total) * 100
responseTime = st.AvgTTFBMS
totalResponseTime += responseTime
serversWithData++
switch {
case uptime >= 99:
status = "UP"
statusIcon = "✅"
upCount++
case uptime >= 95:
status = "DEGRADED"
statusIcon = "⚠️"
degradedCount++
default:
status = "DOWN"
statusIcon = "❌"
downCount++
}
name := b.getShortName(sv)
statusTable += fmt.Sprintf("%s *%s* - %s (%.1f%%, %dms)\n",
statusIcon, name, status, uptime, responseTime)
} else {
statusTable += fmt.Sprintf("❓ *%s* - NO DATA\n", b.getShortName(sv))
downCount++
}
}
// Средний response time
avgResponseTime := 0
if serversWithData > 0 {
avgResponseTime = totalResponseTime / serversWithData
}
// Определяем общий статус
overallStatus := "HEALTHY"
overallIcon := "🟢"
if downCount > 0 {
overallStatus = "CRITICAL"
overallIcon = "🔴"
} else if degradedCount > 0 {
overallStatus = "DEGRADED"
overallIcon = "🟡"
}
// Формируем полное сообщение
message := fmt.Sprintf(`🏥 *STATUS MONITOR* %s
📊 *Status:* %s %s
📈 *Servers:* %d total | 🟢 %d | 🟡 %d | 🔴 %d
⏱️ *Avg Response:* %d ms
🕐 *Updated:* %s
━━━━━━━━━━━━━━━━━━━━━
*DETAILS:*
%s
━━━━━━━━━━━━━━━━━━━━━
_Updates every 30 seconds_`,
overallIcon,
overallStatus, overallIcon,
len(servers), upCount, degradedCount, downCount,
avgResponseTime,
time.Now().Format("15:04:05"),
statusTable,
)
return message
}
func (b *NotificationBot) getShortName(server Server) string {
if server.Name != "" {
if len(server.Name) > 30 {
return server.Name[:27] + "..."
}
return server.Name
}
if server.AnonName != "" {
if len(server.AnonName) > 30 {
return server.AnonName[:27] + "..."
}
return server.AnonName
}
url := server.URL
if len(url) > 40 {
url = url[:37] + "..."
}
return url
}
// updateOrSendMessage обновляет существующее сообщение или отправляет новое
func (b *NotificationBot) updateOrSendMessage(chatID int64, message string) {
b.mu.RLock()
lastMsgID, exists := b.lastSummaryMsg[chatID]
b.mu.RUnlock()
if exists && lastMsgID > 0 {
// Пытаемся отредактировать существующее сообщение
editMsg := tgbotapi.NewEditMessageText(chatID, lastMsgID, message)
editMsg.ParseMode = "Markdown"
_, err := b.api.Send(editMsg)
if err != nil {
// Если не удалось отредактировать, отправляем новое
log.Printf("Failed to edit message, sending new: %v", err)
sentMsg := b.sendMessage(chatID, message, true)
if sentMsg != nil {
b.mu.Lock()
b.lastSummaryMsg[chatID] = sentMsg.MessageID
b.mu.Unlock()
}
}
} else {
// Отправляем новое сообщение
sentMsg := b.sendMessage(chatID, message, true)
if sentMsg != nil {
b.mu.Lock()
b.lastSummaryMsg[chatID] = sentMsg.MessageID
b.mu.Unlock()
}
}
}
func (b *NotificationBot) sendMessage(chatID int64, text string, markdown bool) *tgbotapi.Message {
msg := tgbotapi.NewMessage(chatID, text)
if markdown {
msg.ParseMode = "Markdown"
}
sent, err := b.api.Send(msg)
if err != nil {
log.Printf("Failed to send message to chat %d: %v", chatID, err)
return nil
}
log.Printf("Sent message to chat %d", chatID)
return &sent
}
// NotifyServerAdded уведомляет о добавлении нового сервера
func (b *NotificationBot) NotifyServerAdded(server Server) {
message := fmt.Sprintf("➕ *Server Added*\n\n*Name:* %s\n*URL:* %s\n*Now monitoring*",
b.getShortName(server), server.URL)
for _, chatID := range b.chatIDs {
b.sendMessage(chatID, message, true)
}
// Сразу отправляем обновленную сводку
b.SendSummary()
}
// NotifyServerRemoved уведомляет об удалении сервера
func (b *NotificationBot) NotifyServerRemoved(serverID int, serverName, serverURL string) {
message := fmt.Sprintf("❌ *Server Removed*\n\n*Name:* %s\n*Monitoring stopped*", serverName)
for _, chatID := range b.chatIDs {
b.sendMessage(chatID, message, true)
}
// Сразу отправляем обновленную сводку
b.SendSummary()
}