package controllers import ( "log" "strings" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // ─── Telegram ──────────────────────────────────────────────────────────────── type umindTgChat struct { ID int64 `json:"id"` } type umindTgFileRef struct { FileID string `json:"file_id"` FileName string `json:"file_name"` // solo lo traen los documentos } type umindTgMessage struct { Chat umindTgChat `json:"chat"` Text string `json:"text"` Voice *umindTgFileRef `json:"voice"` Audio *umindTgFileRef `json:"audio"` Photo []umindTgFileRef `json:"photo"` Document *umindTgFileRef `json:"document"` Caption string `json:"caption"` } type umindTgUpdate struct { Message *umindTgMessage `json:"message"` } // UmindTelegramWebhook recibe updates del bot de Telegram de un agente. // Ruta: POST /webhooks/umind-telegram/:webhook_secret // El webhook_secret es un identificador nuestro (no el bot token real) — // ver el comentario en models.UmindCanal. func UmindTelegramWebhook(c *fiber.Ctx) error { secret := c.Params("webhook_secret") canal, err := models.GetUmindCanalByWebhookSecret("telegram", secret) if err != nil { return c.SendStatus(fiber.StatusOK) // siempre 200 a Telegram, aunque no matchee } var update umindTgUpdate if err := c.BodyParser(&update); err != nil || update.Message == nil { return c.SendStatus(fiber.StatusOK) } msg := update.Message switch { case msg.Voice != nil && msg.Voice.FileID != "": err = services.ProcesarMediaTelegramUmind(canal, msg.Chat.ID, msg.Voice.FileID, "audio") case msg.Audio != nil && msg.Audio.FileID != "": err = services.ProcesarMediaTelegramUmind(canal, msg.Chat.ID, msg.Audio.FileID, "audio") case len(msg.Photo) > 0: err = services.ProcesarMediaTelegramUmind(canal, msg.Chat.ID, msg.Photo[len(msg.Photo)-1].FileID, "image") case msg.Document != nil && msg.Document.FileID != "": err = services.ProcesarMediaTelegramUmindConNombre(canal, msg.Chat.ID, msg.Document.FileID, "document", msg.Document.FileName, msg.Caption) default: texto := strings.TrimSpace(msg.Text) if texto == "" { return c.SendStatus(fiber.StatusOK) } err = services.ProcesarMensajeTelegramUmind(canal, msg.Chat.ID, texto) } if err != nil { log.Printf("[UMIND_TELEGRAM] canal %d: %v", canal.ID, err) models.RegistrarEventoUmind(canal.AgenteID, "error", "canal_telegram", "Error procesando un mensaje de Telegram", err.Error()) } return c.SendStatus(fiber.StatusOK) } // ─── WhatsApp ──────────────────────────────────────────────────────────────── type umindWaMediaRef struct { ID string `json:"id"` } type umindWaMessage struct { From string `json:"from"` Type string `json:"type"` Text struct { Body string `json:"body"` } `json:"text"` Image umindWaMediaRef `json:"image"` Audio umindWaMediaRef `json:"audio"` Document struct { ID string `json:"id"` Filename string `json:"filename"` Caption string `json:"caption"` } `json:"document"` } type umindWaValue struct { Metadata struct { PhoneNumberID string `json:"phone_number_id"` } `json:"metadata"` Messages []umindWaMessage `json:"messages"` } type umindWaChange struct { Value umindWaValue `json:"value"` } type umindWaEntry struct { Changes []umindWaChange `json:"changes"` } type umindWaPayload struct { Entry []umindWaEntry `json:"entry"` } // UmindWhatsAppVerify atiende el handshake de verificación que Meta hace al // configurar el webhook (hub.mode/hub.verify_token/hub.challenge). // Ruta: GET /webhooks/umind-whatsapp/:webhook_secret func UmindWhatsAppVerify(c *fiber.Ctx) error { secret := c.Params("webhook_secret") canal, err := models.GetUmindCanalByWebhookSecret("whatsapp", secret) if err != nil { return c.SendStatus(fiber.StatusForbidden) } credenciales, err := services.DescifrarCredencialesCanal(canal.CredencialesEnc) if err != nil { return c.SendStatus(fiber.StatusForbidden) } if c.Query("hub.mode") != "subscribe" || c.Query("hub.verify_token") != credenciales["verify_token"] || credenciales["verify_token"] == "" { return c.SendStatus(fiber.StatusForbidden) } return c.SendString(c.Query("hub.challenge")) } // UmindWhatsAppWebhook recibe mensajes entrantes de WhatsApp Business Cloud // API. La única autenticación real acá es la firma HMAC del body — el // webhook_secret en la URL identifica el canal, pero no alcanza solo. // Ruta: POST /webhooks/umind-whatsapp/:webhook_secret func UmindWhatsAppWebhook(c *fiber.Ctx) error { secret := c.Params("webhook_secret") canal, err := models.GetUmindCanalByWebhookSecret("whatsapp", secret) if err != nil { return c.SendStatus(fiber.StatusOK) } credenciales, err := services.DescifrarCredencialesCanal(canal.CredencialesEnc) if err != nil { log.Printf("[UMIND_WHATSAPP] canal %d: credenciales corruptas: %v", canal.ID, err) models.RegistrarEventoUmind(canal.AgenteID, "error", "canal_whatsapp", "Credenciales del canal corruptas", err.Error()) return c.SendStatus(fiber.StatusOK) } if !services.ValidarFirmaWhatsApp(credenciales["app_secret"], c.Body(), c.Get("X-Hub-Signature-256")) { log.Printf("[UMIND_WHATSAPP] canal %d: firma inválida", canal.ID) models.RegistrarEventoUmind(canal.AgenteID, "error", "canal_whatsapp", "Firma X-Hub-Signature-256 inválida en un webhook entrante", "") return c.SendStatus(fiber.StatusUnauthorized) } var payload umindWaPayload if err := c.BodyParser(&payload); err != nil { return c.SendStatus(fiber.StatusOK) } for _, entry := range payload.Entry { for _, change := range entry.Changes { for _, msg := range change.Value.Messages { var err error switch msg.Type { case "text": texto := strings.TrimSpace(msg.Text.Body) if texto == "" { continue } err = services.ProcesarMensajeWhatsAppUmind(canal, msg.From, texto) case "audio": if msg.Audio.ID == "" { continue } err = services.ProcesarMediaWhatsAppUmind(canal, msg.From, msg.Audio.ID, "audio") case "image": if msg.Image.ID == "" { continue } err = services.ProcesarMediaWhatsAppUmind(canal, msg.From, msg.Image.ID, "image") case "document": if msg.Document.ID == "" { continue } err = services.ProcesarMediaWhatsAppUmindConNombre(canal, msg.From, msg.Document.ID, "document", msg.Document.Filename, msg.Document.Caption) default: continue } if err != nil { log.Printf("[UMIND_WHATSAPP] canal %d: %v", canal.ID, err) models.RegistrarEventoUmind(canal.AgenteID, "error", "canal_whatsapp", "Error procesando un mensaje de WhatsApp", err.Error()) } } } } return c.SendStatus(fiber.StatusOK) }