253 lines
6.6 KiB
Go
253 lines
6.6 KiB
Go
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
|
|
}
|