diff --git a/pkg/models/telegram_agent.go b/pkg/models/telegram_agent.go
index 845bce0..bc1e032 100644
--- a/pkg/models/telegram_agent.go
+++ b/pkg/models/telegram_agent.go
@@ -1,6 +1,8 @@
package models
import (
+ "fmt"
+
"github.com/sujit-baniya/fiber-boilerplate/app"
"gorm.io/gorm"
)
@@ -81,3 +83,15 @@ func CreateAgentAuth(chatID int64, nombre string) error {
func DeleteAgentAuth(id uint) error {
return app.Http.Database.DB.Delete(&TelegramAgentAuth{}, id).Error
}
+
+// GetAgentTelegramConfig retorna el TelegramConfig vinculado al agente bot activo.
+func GetAgentTelegramConfig() (*TelegramConfig, error) {
+ var ai AiConfig
+ if err := app.Http.Database.DB.Where("es_agente_bot = ? AND is_active = ?", true, true).First(&ai).Error; err != nil {
+ return nil, fmt.Errorf("no hay agente bot configurado")
+ }
+ if ai.TelegramConfigID == nil {
+ return nil, fmt.Errorf("el agente no tiene Telegram configurado")
+ }
+ return GetTelegramConfigByID(*ai.TelegramConfigID)
+}
diff --git a/rest/controllers/coolify_controller.go b/rest/controllers/coolify_controller.go
index 6a0b322..d80a82d 100644
--- a/rest/controllers/coolify_controller.go
+++ b/rest/controllers/coolify_controller.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "log"
"net/http"
"net/url"
"strconv"
@@ -12,6 +13,7 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
+ "github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
// filterQueryParam elimina un parámetro específico del querystring.
@@ -346,12 +348,137 @@ func CoolifyTeamMembers(c *fiber.Ctx) error {
return coolifyProxy(c, http.MethodGet, "/teams/current/members")
}
-// CoolifyWebhook recibe notificaciones push de Coolify (deployments, etc.)
-// Acepta el payload y devuelve 200; el procesamiento puede extenderse aquí.
+// CoolifyWebhook recibe notificaciones push de Coolify y las reenvía a Telegram.
+// Configurar en Coolify → Settings → Notifications → Add Notification → Webhook
+// URL: https://admin.u-site.app/webhooks/coolify/:config_id
func CoolifyWebhook(c *fiber.Ctx) error {
+ body := c.Body()
+ log.Printf("[COOLIFY_WEBHOOK] config_id=%s payload=%s", c.Params("config_id"), string(body))
+
+ if len(body) == 0 {
+ return c.SendStatus(fiber.StatusOK)
+ }
+
+ // Identificar la instancia Coolify si viene en la ruta
+ var instanceName string
+ if idStr := c.Params("config_id"); idStr != "" {
+ if id, err := strconv.ParseUint(idStr, 10, 32); err == nil {
+ if cfg, err := models.GetCoolifyConfigByID(uint(id)); err == nil {
+ instanceName = cfg.Nombre
+ }
+ }
+ }
+
+ go sendCoolifyTelegramNotif(body, instanceName)
return c.SendStatus(fiber.StatusOK)
}
+// sendCoolifyTelegramNotif parsea el payload de Coolify y lo envía a Telegram.
+func sendCoolifyTelegramNotif(body []byte, instanceName string) {
+ // Intentar parsear como JSON
+ var payload map[string]interface{}
+ isJSON := json.Unmarshal(body, &payload) == nil
+
+ // Extraer campos con múltiples nombres posibles (Coolify cambia formato según versión)
+ get := func(keys ...string) string {
+ if !isJSON {
+ return ""
+ }
+ for _, k := range keys {
+ if v, ok := payload[k]; ok {
+ if s, ok := v.(string); ok && s != "" {
+ return s
+ }
+ }
+ }
+ return ""
+ }
+
+ appName := get("application_name", "name", "app_name", "resource_name")
+ appUUID := get("application_uuid", "uuid", "resource_uuid")
+ status := get("status", "type", "event", "deployment_status")
+ message := get("message", "description", "text", "body")
+ fqdn := get("fqdn", "url", "application_url", "application_fqdn")
+ serverName := get("server_name", "server")
+
+ // Si no pudimos parsear como JSON, tratar el body entero como mensaje
+ if !isJSON {
+ message = strings.TrimSpace(string(body))
+ if len(message) > 500 {
+ message = message[:500] + "…"
+ }
+ }
+
+ // Emoji según estado
+ emoji := "🔔"
+ statusLower := strings.ToLower(status + " " + message)
+ switch {
+ case strings.Contains(statusLower, "success") || strings.Contains(statusLower, "exitoso") || strings.Contains(statusLower, "finished"):
+ emoji = "✅"
+ case strings.Contains(statusLower, "fail") || strings.Contains(statusLower, "error") || strings.Contains(statusLower, "fallo"):
+ emoji = "❌"
+ case strings.Contains(statusLower, "running") || strings.Contains(statusLower, "deploying") || strings.Contains(statusLower, "start") || strings.Contains(statusLower, "building"):
+ emoji = "🚀"
+ case strings.Contains(statusLower, "stop") || strings.Contains(statusLower, "detenido"):
+ emoji = "⏹"
+ case strings.Contains(statusLower, "restart") || strings.Contains(statusLower, "reinici"):
+ emoji = "🔄"
+ }
+
+ // Construir el mensaje
+ lines := []string{fmt.Sprintf("%s Coolify", emoji)}
+ if instanceName != "" {
+ lines = append(lines, fmt.Sprintf("Instancia: %s", instanceName))
+ }
+ if appName != "" {
+ lines = append(lines, fmt.Sprintf("App: %s", appName))
+ }
+ if appUUID != "" && appUUID != appName {
+ lines = append(lines, fmt.Sprintf("UUID: %s", appUUID))
+ }
+ if serverName != "" {
+ lines = append(lines, fmt.Sprintf("Servidor: %s", serverName))
+ }
+ if status != "" {
+ lines = append(lines, fmt.Sprintf("Estado: %s", status))
+ }
+ if message != "" && message != status {
+ if len(message) > 300 {
+ message = message[:300] + "…"
+ }
+ lines = append(lines, fmt.Sprintf("ℹ️ %s", message))
+ }
+ if fqdn != "" {
+ lines = append(lines, fmt.Sprintf("🌐 %s", fqdn))
+ }
+
+ text := strings.Join(lines, "\n")
+
+ // Obtener el bot del agente (TelegramConfig ID que tenga agent bot activo)
+ tgCfg, err := models.GetAgentTelegramConfig()
+ if err != nil {
+ log.Printf("[COOLIFY_WEBHOOK] Sin config Telegram para notificar: %v", err)
+ return
+ }
+
+ // Enviar a todos los chats autorizados del agente
+ auths, _ := models.GetAllAgentAuth()
+ if len(auths) == 0 {
+ log.Printf("[COOLIFY_WEBHOOK] Sin chats autorizados para notificar")
+ return
+ }
+
+ svc := &services.TelegramService{BotToken: tgCfg.BotToken}
+ for _, auth := range auths {
+ if !auth.Activo {
+ continue
+ }
+ if err := svc.SendMessage(auth.ChatID, text); err != nil {
+ log.Printf("[COOLIFY_WEBHOOK] Error enviando a chat %d: %v", auth.ChatID, err)
+ }
+ }
+}
+
// ─── CRUD de instancias Coolify ───────────────────────────────────────────────
func CoolifyListConfigs(c *fiber.Ctx) error {
diff --git a/rest/routes/publicas.go b/rest/routes/publicas.go
index 5dd3ac9..bedf8d7 100755
--- a/rest/routes/publicas.go
+++ b/rest/routes/publicas.go
@@ -24,7 +24,10 @@ func RutasPublicas(web fiber.Router) {
web.Post("/webhooks/saas-in/:token", controllers.SaasWebhookInHandler)
web.Get("/webhooks/saas-in/:token", controllers.SaasWebhookInHandler) // algunos SaaS verifican con GET
// Coolify notifica eventos de deployment/restart aquí (configurar en Coolify → Settings → Notifications → Webhook)
+ // Ruta genérica
web.Post("/webhooks/coolify", controllers.CoolifyWebhook)
+ // Ruta por instancia: incluye el config_id para identificar de qué Coolify viene
+ web.Post("/webhooks/coolify/:config_id", controllers.CoolifyWebhook)
// ─── Página de confirmación de pago ───────────────────────────────────
web.Get("/pago-exitoso", apiControllers.PagoExitosoPage)