package services import ( "bytes" "encoding/json" "fmt" "gestion/models" "log" "net/http" "os" "sync" "time" ) var TelegramBot *TelegramService type TelegramService struct { botToken string webhookSecret string BotUsername string notificationsEnabled bool notifBots []models.TelegramBotConfig notifMu sync.RWMutex } func NewTelegramService() *TelegramService { svc := &TelegramService{ botToken: os.Getenv("TELEGRAM_BOT_TOKEN"), webhookSecret: os.Getenv("TELEGRAM_WEBHOOK_SECRET"), BotUsername: os.Getenv("TELEGRAM_BOT_USERNAME"), } TelegramBot = svc return svc } func (t *TelegramService) IsConfigured() bool { return t.botToken != "" } func (t *TelegramService) IsNotificationsEnabled() bool { return t.notificationsEnabled } func (t *TelegramService) SetNotificationsEnabled(enabled bool) { t.notificationsEnabled = enabled } // ReloadNotifBots met à jour la liste des bots de notification (thread-safe). func (t *TelegramService) ReloadNotifBots(bots []models.TelegramBotConfig) { t.notifMu.Lock() defer t.notifMu.Unlock() t.notifBots = bots log.Printf("✅ [TELEGRAM] %d bot(s) de notification chargé(s)", len(bots)) } // SendNotif envoie un message HTML en essayant chaque bot de notification dans l'ordre. // En cas de rate-limit (429) ou d'erreur sur le bot courant, il passe au suivant. func (t *TelegramService) SendNotif(chatID int64, text string) error { t.notifMu.RLock() bots := make([]models.TelegramBotConfig, len(t.notifBots)) copy(bots, t.notifBots) t.notifMu.RUnlock() if len(bots) == 0 { // Fallback : utiliser le bot webhook principal return t.SendMessage(chatID, text) } var lastErr error for i, bot := range bots { if err := t.sendWithToken(bot.Token, chatID, text); err != nil { log.Printf("⚠️ [TELEGRAM] bot %d (%s) échoué: %v — essai suivant", i+1, bot.Username, err) lastErr = err continue } return nil } return fmt.Errorf("tous les bots de notification ont échoué: %w", lastErr) } // sendWithToken envoie un message HTML avec un token arbitraire. func (t *TelegramService) sendWithToken(token string, chatID int64, text string) error { payload := map[string]interface{}{ "chat_id": chatID, "text": text, "parse_mode": "HTML", } body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("marshal: %w", err) } url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token) req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return fmt.Errorf("création requête: %w", err) } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return fmt.Errorf("envoi: %w", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusTooManyRequests { return fmt.Errorf("rate limit (429)") } if resp.StatusCode != http.StatusOK { return fmt.Errorf("status %d", resp.StatusCode) } return nil } // Reload met à jour le token et le username (appelé après UpdateSettings) func (t *TelegramService) Reload(token, username string) { if token != "" { t.botToken = token } if username != "" { t.BotUsername = username } } func (t *TelegramService) ValidateWebhookSecret(header string) bool { if t.webhookSecret == "" { return true // pas de secret configuré = accepté } return header == t.webhookSecret } // SendMessage envoie un message texte (HTML) à un chat Telegram func (t *TelegramService) SendMessage(chatID int64, text string) error { if !t.IsConfigured() { return fmt.Errorf("telegram non configuré") } payload := map[string]interface{}{ "chat_id": chatID, "text": text, "parse_mode": "HTML", } body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("marshal: %w", err) } url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", t.botToken) req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return fmt.Errorf("création requête: %w", err) } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return fmt.Errorf("envoi: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("telegram API status %d", resp.StatusCode) } return nil } // SendMessageWithButtons envoie un message HTML avec des boutons inline (URL buttons). // buttons est une liste de paires [texte, url]. func (t *TelegramService) SendMessageWithButtons(chatID int64, text string, buttons [][2]string) error { if !t.IsConfigured() { return fmt.Errorf("telegram non configuré") } row := make([]map[string]string, 0, len(buttons)) for _, b := range buttons { row = append(row, map[string]string{"text": b[0], "url": b[1]}) } payload := map[string]interface{}{ "chat_id": chatID, "text": text, "parse_mode": "HTML", "reply_markup": map[string]interface{}{ "inline_keyboard": [][]map[string]string{row}, }, } body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("marshal: %w", err) } url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", t.botToken) req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return fmt.Errorf("création requête: %w", err) } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return fmt.Errorf("envoi: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("telegram API status %d", resp.StatusCode) } return nil } // SetWebhook enregistre l'URL webhook auprès de Telegram func (t *TelegramService) SetWebhook(webhookURL string) error { if !t.IsConfigured() { return fmt.Errorf("telegram non configuré") } payload := map[string]interface{}{ "url": webhookURL, "allowed_updates": []string{"message"}, } if t.webhookSecret != "" { payload["secret_token"] = t.webhookSecret } body, err := json.Marshal(payload) if err != nil { return err } url := fmt.Sprintf("https://api.telegram.org/bot%s/setWebhook", t.botToken) req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("setWebhook status %d", resp.StatusCode) } log.Printf("✅ [TELEGRAM] Webhook enregistré: %s", webhookURL) return nil }