// Package notify sends operational alerts via Telegram and Resend email. // Credentials are read exclusively from environment variables — never from // the config file or source code. package notify import ( "bytes" "encoding/json" "fmt" "net/http" "os" "time" "forge.secuaas.ovh/olivier/claude-failover/internal/config" ) // Notifier dispatches alerts to configured sinks. // All methods are safe to call when credentials are absent — they become no-ops. type Notifier struct { telegramToken string telegramChatID string resendAPIKey string notifyEmail string httpClient *http.Client telegramBaseURL string // override in tests resendBaseURL string // override in tests } // New creates a Notifier, reading credentials from the environment variables // named in cfg.Notifications. func New(cfg *config.Config) *Notifier { n := &Notifier{ httpClient: &http.Client{Timeout: 10 * time.Second}, telegramBaseURL: "https://api.telegram.org", resendBaseURL: "https://api.resend.com", } if cfg.Notifications.TelegramTokenEnv != "" { n.telegramToken = os.Getenv(cfg.Notifications.TelegramTokenEnv) } if cfg.Notifications.TelegramChatIDEnv != "" { n.telegramChatID = os.Getenv(cfg.Notifications.TelegramChatIDEnv) } if cfg.Notifications.ResendAPIKeyEnv != "" { n.resendAPIKey = os.Getenv(cfg.Notifications.ResendAPIKeyEnv) } n.notifyEmail = "admin@example.com" if cfg.Notifications.NotifyEmailEnv != "" { if v := os.Getenv(cfg.Notifications.NotifyEmailEnv); v != "" { n.notifyEmail = v } } return n } // Telegram sends msg to the configured Telegram chat. // Returns nil (no-op) when token or chat ID is not configured. func (n *Notifier) Telegram(msg string) error { if n.telegramToken == "" || n.telegramChatID == "" { return nil } url := fmt.Sprintf("%s/bot%s/sendMessage", n.telegramBaseURL, n.telegramToken) body := map[string]string{ "chat_id": n.telegramChatID, "text": msg, "parse_mode": "HTML", } data, _ := json.Marshal(body) resp, err := n.httpClient.Post(url, "application/json", bytes.NewReader(data)) if err != nil { return fmt.Errorf("telegram: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("telegram: HTTP %d", resp.StatusCode) } return nil } // Email sends an HTML email via the Resend API. // Returns nil (no-op) when the API key is not configured. func (n *Notifier) Email(subject, htmlBody string) error { if n.resendAPIKey == "" { return nil } type emailReq struct { From string `json:"from"` To []string `json:"to"` Subject string `json:"subject"` HTML string `json:"html"` } payload := emailReq{ From: "Orchestrator ", To: []string{n.notifyEmail}, Subject: subject, HTML: htmlBody, } data, _ := json.Marshal(payload) req, err := http.NewRequest(http.MethodPost, n.resendBaseURL+"/emails", bytes.NewReader(data)) if err != nil { return fmt.Errorf("email: build request: %w", err) } req.Header.Set("Authorization", "Bearer "+n.resendAPIKey) req.Header.Set("Content-Type", "application/json") resp, err := n.httpClient.Do(req) if err != nil { return fmt.Errorf("email: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("email: HTTP %d", resp.StatusCode) } return nil }