package alerts import ( "bytes" "encoding/json" "fmt" "log" "net/http" "sync" "time" ) // Config holds Telegram bot settings from config.json. type Config struct { Enabled bool `json:"enabled"` BotToken string `json:"bot_token"` ChatID string `json:"chat_id"` } // Notifier sends operator alerts via Telegram Bot API. type Notifier struct { cfg Config client *http.Client mu sync.Mutex last time.Time } func New(cfg Config) *Notifier { return &Notifier{ cfg: cfg, client: &http.Client{ Timeout: 10 * time.Second, }, } } // Enabled reports whether Telegram alerts are configured. func (n *Notifier) Enabled() bool { return n != nil && n.cfg.Enabled && n.cfg.BotToken != "" && n.cfg.ChatID != "" } // Send posts a message to the configured chat (best-effort, non-blocking caller). func (n *Notifier) Send(text string) { if !n.Enabled() { return } go func() { if err := n.sendSync(text); err != nil { log.Printf("telegram alert: %v", err) } }() } func (n *Notifier) sendSync(text string) error { n.mu.Lock() if time.Since(n.last) < 500*time.Millisecond { n.mu.Unlock() time.Sleep(500 * time.Millisecond) n.mu.Lock() } n.last = time.Now() n.mu.Unlock() url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", n.cfg.BotToken) body, _ := json.Marshal(map[string]string{ "chat_id": n.cfg.ChatID, "text": text, "parse_mode": "Markdown", }) req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := n.client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 300 { return fmt.Errorf("telegram API status %d", resp.StatusCode) } return nil } // FleetEvent formats a fleet alert message. func FleetEvent(action, hostID, detail string) string { return fmt.Sprintf("🔧 *forge-mesh* · `%s`\nhost: `%s`\n%s", action, hostID, detail) } // CrucibleEvent formats a batch terminal alert. func CrucibleEvent(jobID string, hostCount int, cmd string) string { return fmt.Sprintf("⚗️ *crucible batch* · `%s`\nhosts: %d\ncmd: `%s`", jobID, hostCount, cmd) }