From 20da2a560fff6c381abb8df95b081641c71f1f33 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Fri, 12 Jun 2026 11:13:47 +0200 Subject: [PATCH] chore: build --- backend/gestion/db/db_notifications.go | 24 +++---- backend/gestion/db/db_settings.go | 12 ++++ backend/gestion/handlers/settings.go | 1 + backend/gestion/main.go | 1 + backend/gestion/models/settings.go | 13 +++- backend/gestion/services/telegram.go | 69 +++++++++++++++++++ frontend-admin/src/api/api_admin.ts | 6 ++ .../src/screens/admin/SettingsScreen.tsx | 64 ++++++++++++++++- 8 files changed, 174 insertions(+), 16 deletions(-) diff --git a/backend/gestion/db/db_notifications.go b/backend/gestion/db/db_notifications.go index 86292b80..e9e2e99c 100644 --- a/backend/gestion/db/db_notifications.go +++ b/backend/gestion/db/db_notifications.go @@ -24,11 +24,11 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() { + if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() { if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok { go func(id int64, msg string) { - if err := services.LBTelegram.SendNotification(id, fmt.Sprintf("🔔 Notification\n\n%s", msg)); err != nil { - log.Printf("⚠️ [LB] notification client échouée pour %s: %v", username, err) + if err := services.TelegramBot.SendNotif(id, fmt.Sprintf("🔔 Notification\n\n%s", msg)); err != nil { + log.Printf("⚠️ [NOTIF] notification client échouée pour %s: %v", username, err) } }(chatID, message) } @@ -54,11 +54,11 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() { + if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() { if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok { go func(id int64, msg string) { - if err := services.LBTelegram.SendNotification(id, fmt.Sprintf("🔔 Notification\n\n%s", msg)); err != nil { - log.Printf("⚠️ [LB] notification livreur échouée pour %s: %v", username, err) + if err := services.TelegramBot.SendNotif(id, fmt.Sprintf("🔔 Notification\n\n%s", msg)); err != nil { + log.Printf("⚠️ [NOTIF] notification livreur échouée pour %s: %v", username, err) } }(chatID, message) } @@ -95,13 +95,13 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() { + if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() { if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok { capturedChatID := chatID capturedMsg := msg go func(id int64, m string) { - if err := services.LBTelegram.SendNotification(id, fmt.Sprintf("🔔 Nouvelle commande\n\n%s", m)); err != nil { - log.Printf("⚠️ [LB] notification admin/cabine échouée: %v", err) + if err := services.TelegramBot.SendNotif(id, fmt.Sprintf("🔔 Nouvelle commande\n\n%s", m)); err != nil { + log.Printf("⚠️ [NOTIF] notification admin/cabine échouée: %v", err) } }(capturedChatID, capturedMsg) } @@ -138,13 +138,13 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() { + if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() { if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok { capturedChatID := chatID capturedBody := body go func(id int64, m string) { - if err := services.LBTelegram.SendNotification(id, fmt.Sprintf("🚨 Alerte livreur\n\n%s", m)); err != nil { - log.Printf("⚠️ [LB] notification alerte échouée: %v", err) + if err := services.TelegramBot.SendNotif(id, fmt.Sprintf("🚨 Alerte livreur\n\n%s", m)); err != nil { + log.Printf("⚠️ [NOTIF] notification alerte échouée: %v", err) } }(capturedChatID, capturedBody) } diff --git a/backend/gestion/db/db_settings.go b/backend/gestion/db/db_settings.go index 996bf74e..8dd5fca1 100644 --- a/backend/gestion/db/db_settings.go +++ b/backend/gestion/db/db_settings.go @@ -154,6 +154,11 @@ func (d *Database) GetSettings() (models.AppSettings, error) { settings.TelegramBotUsername = row.Value case "telegram_notifications_enabled": settings.TelegramNotificationsEnabled = row.Value == "true" + case "telegram_notif_bots": + var bots []models.TelegramBotConfig + if err := json.Unmarshal([]byte(row.Value), &bots); err == nil { + settings.TelegramNotifBots = bots + } case "delivery_mode": var mode models.DeliveryModeConfig if err := json.Unmarshal([]byte(row.Value), &mode); err == nil { @@ -249,6 +254,13 @@ func (d *Database) UpdateSettings(s models.AppSettings) error { {"telegram_bot_token", s.TelegramBotToken}, {"telegram_bot_username", s.TelegramBotUsername}, {"telegram_notifications_enabled", boolStr(s.TelegramNotificationsEnabled)}, + {"telegram_notif_bots", func() string { + if s.TelegramNotifBots == nil { + s.TelegramNotifBots = []models.TelegramBotConfig{} + } + b, _ := json.Marshal(s.TelegramNotifBots) + return string(b) + }()}, {"telegram_2fa_enabled", boolStr(s.Telegram2FAEnabled)}, {"delivery_mode", string(deliveryModeJSON)}, {"shop_name", s.ShopName}, diff --git a/backend/gestion/handlers/settings.go b/backend/gestion/handlers/settings.go index 3482d2cb..e5453c3e 100644 --- a/backend/gestion/handlers/settings.go +++ b/backend/gestion/handlers/settings.go @@ -85,6 +85,7 @@ func UpdateSettings(c *gin.Context) { if services.TelegramBot != nil { services.TelegramBot.Reload(req.TelegramBotToken, req.TelegramBotUsername) services.TelegramBot.SetNotificationsEnabled(req.TelegramNotificationsEnabled) + services.TelegramBot.ReloadNotifBots(req.TelegramNotifBots) if req.TelegramBotToken != "" { log.Printf("✅ [SETTINGS] Service Telegram rechargé (username: %s)", req.TelegramBotUsername) if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" { diff --git a/backend/gestion/main.go b/backend/gestion/main.go index 1c5c65b5..4fed49f1 100644 --- a/backend/gestion/main.go +++ b/backend/gestion/main.go @@ -62,6 +62,7 @@ func main() { if dbSettings, err := database.GetSettings(); err == nil { telegramService.Reload(dbSettings.TelegramBotToken, dbSettings.TelegramBotUsername) telegramService.SetNotificationsEnabled(dbSettings.TelegramNotificationsEnabled) + telegramService.ReloadNotifBots(dbSettings.TelegramNotifBots) if dbSettings.TelegramBotToken != "" { log.Printf("✅ [TELEGRAM] Config chargée depuis la DB (username: %s)", dbSettings.TelegramBotUsername) if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" { diff --git a/backend/gestion/models/settings.go b/backend/gestion/models/settings.go index d0e6b9b0..d8188f30 100644 --- a/backend/gestion/models/settings.go +++ b/backend/gestion/models/settings.go @@ -1,5 +1,11 @@ package models +// TelegramBotConfig représente un bot de notification configuré par l'admin +type TelegramBotConfig struct { + Token string `json:"token"` + Username string `json:"username"` +} + type PostalZone struct { Name string `json:"name"` MinAmount float64 `json:"min_amount"` @@ -92,9 +98,10 @@ type AppSettings struct { NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptées (ex: ["btc","eth","ltc"]) DeliverySchedule DeliverySchedule `json:"delivery_schedule"` // horaires de livraison par jour PostalZones []PostalZone `json:"postal_zones"` // zones de livraison avec minimum de commande - TelegramBotToken string `json:"telegram_bot_token"` // token du bot Telegram (BotFather) - TelegramBotUsername string `json:"telegram_bot_username"` // username du bot (sans @) - TelegramNotificationsEnabled bool `json:"telegram_notifications_enabled"` // activer/désactiver les notifications Telegram + TelegramBotToken string `json:"telegram_bot_token"` // token du bot Telegram (BotFather) — pour le webhook de liaison + TelegramBotUsername string `json:"telegram_bot_username"` // username du bot (sans @) + TelegramNotificationsEnabled bool `json:"telegram_notifications_enabled"` // activer/désactiver les notifications Telegram + TelegramNotifBots []TelegramBotConfig `json:"telegram_notif_bots"` // bots de notification avec failover automatique DeliveryMode DeliveryModeConfig `json:"delivery_mode"` // mode d'assignation des livreurs ShopName string `json:"shop_name"` // nom affiché dans la sidebar du site client Telegram2FAEnabled bool `json:"telegram_2fa_enabled"` // activer/désactiver l'authentification à deux facteurs diff --git a/backend/gestion/services/telegram.go b/backend/gestion/services/telegram.go index 6a035ab4..e3bf368d 100644 --- a/backend/gestion/services/telegram.go +++ b/backend/gestion/services/telegram.go @@ -4,9 +4,11 @@ import ( "bytes" "encoding/json" "fmt" + "gestion/models" "log" "net/http" "os" + "sync" "time" ) @@ -17,6 +19,8 @@ type TelegramService struct { webhookSecret string BotUsername string notificationsEnabled bool + notifBots []models.TelegramBotConfig + notifMu sync.RWMutex } func NewTelegramService() *TelegramService { @@ -41,6 +45,71 @@ 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 != "" { diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index 1d55ee3f..72344b98 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -1050,11 +1050,17 @@ export interface AppSettings { telegram_bot_username: string; telegram_notifications_enabled: boolean; telegram_2fa_enabled: boolean; + telegram_notif_bots: TelegramBotConfig[]; delivery_mode: DeliveryModeConfig; shop_name: string; contact_telegram: string; } +export type TelegramBotConfig = { + token: string; + username: string; +}; + export const getSettings = async (): Promise<{ success: boolean; settings?: AppSettings; diff --git a/frontend-admin/src/screens/admin/SettingsScreen.tsx b/frontend-admin/src/screens/admin/SettingsScreen.tsx index e89f79ba..a33da0cb 100644 --- a/frontend-admin/src/screens/admin/SettingsScreen.tsx +++ b/frontend-admin/src/screens/admin/SettingsScreen.tsx @@ -16,7 +16,7 @@ import { Ionicons } from "@expo/vector-icons"; import { spacing, fontSize, borderRadius } from "../../theme"; import { useTheme } from "../../context/ThemeContext"; import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, getAllProductsAdmin, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin"; -import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin"; +import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, DaySchedule, DeliverySchedule, PostalZone, TelegramBotConfig } from "../../api/api_admin"; import type { Product } from "../../api/types"; import AlertModal from "../../components/ui/AlertModal"; import { useAlert } from "../../hooks/useAlert"; @@ -1032,6 +1032,7 @@ export default function SettingsScreen() { telegram_bot_username: "", telegram_notifications_enabled: false, telegram_2fa_enabled: false, + telegram_notif_bots: [], delivery_mode: { mode: "single" as const, category_routes: [] }, shop_name: "Milieu-Nantais", contact_telegram: "", @@ -1903,6 +1904,67 @@ export default function SettingsScreen() { )} + + {/* Bots de notification (failover) */} + + Bots de notification (failover) + + Les notifications sont envoyées par le premier bot. En cas de rate-limit ou d'erreur, le suivant prend le relais. + Si aucun bot n'est configuré ici, le bot principal est utilisé. + + {(settings.telegram_notif_bots ?? []).map((bot, idx) => ( + + + setSettings((p) => { + const bots = [...(p.telegram_notif_bots ?? [])]; + bots[idx] = { ...bots[idx], token: v }; + return { ...p, telegram_notif_bots: bots }; + })} + autoCapitalize="none" + autoCorrect={false} + secureTextEntry + /> + setSettings((p) => { + const bots = [...(p.telegram_notif_bots ?? [])]; + bots[idx] = { ...bots[idx], username: v }; + return { ...p, telegram_notif_bots: bots }; + })} + autoCapitalize="none" + autoCorrect={false} + /> + + setSettings((p) => ({ + ...p, + telegram_notif_bots: (p.telegram_notif_bots ?? []).filter((_, i) => i !== idx), + }))} + style={{ padding: spacing.s, marginTop: spacing.xs }} + > + + + + ))} + setSettings((p) => ({ + ...p, + telegram_notif_bots: [...(p.telegram_notif_bots ?? []), { token: "", username: "" }], + }))} + style={{ flexDirection: "row", alignItems: "center", gap: spacing.s, paddingVertical: spacing.s }} + > + + Ajouter un bot de notification + +