Newer
Older
status / bot.go
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()
}