El bot solo procesaba mensajes de texto: un documento o foto enviado al chat se ignoraba por completo (Telegram lo manda en message.document/photo con caption, no en el campo text). Ahora el webhook detecta el adjunto, lo descarga desde la API de Telegram y lo deja pendiente para el chat; el agente usa la nueva tool adjuntar_factura para asociarlo a un cliente (preguntando cliente/monto si el caption no los trae) y crear la Factura con el archivo ya vinculado, igual que si se subiera desde el dashboard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
221 lines
6.9 KiB
Go
221 lines
6.9 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
|
)
|
|
|
|
// ─── Tipos del update de Telegram ────────────────────────────────────────────
|
|
|
|
type tgUser struct {
|
|
ID int64 `json:"id"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
type tgChat struct {
|
|
ID int64 `json:"id"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type tgDocument struct {
|
|
FileID string `json:"file_id"`
|
|
FileName string `json:"file_name"`
|
|
MimeType string `json:"mime_type"`
|
|
}
|
|
|
|
type tgPhotoSize struct {
|
|
FileID string `json:"file_id"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
}
|
|
|
|
type tgMessage struct {
|
|
MessageID int `json:"message_id"`
|
|
From tgUser `json:"from"`
|
|
Chat tgChat `json:"chat"`
|
|
Text string `json:"text"`
|
|
Caption string `json:"caption"`
|
|
Document *tgDocument `json:"document"`
|
|
Photo []tgPhotoSize `json:"photo"`
|
|
}
|
|
|
|
type tgUpdate struct {
|
|
UpdateID int `json:"update_id"`
|
|
Message *tgMessage `json:"message"`
|
|
}
|
|
|
|
// ─── Webhook handler ──────────────────────────────────────────────────────────
|
|
|
|
// TelegramAgentWebhook recibe updates del bot administrador.
|
|
// Telegram llama aquí cuando alguien escribe al bot.
|
|
// Ruta: POST /webhooks/telegram-agent/:bot_token
|
|
func TelegramAgentWebhook(c *fiber.Ctx) error {
|
|
botToken := c.Params("bot_token")
|
|
if botToken == "" {
|
|
return c.SendStatus(400)
|
|
}
|
|
|
|
// Parsear el update
|
|
var update tgUpdate
|
|
if err := c.BodyParser(&update); err != nil {
|
|
return c.SendStatus(200) // siempre 200 a Telegram
|
|
}
|
|
if update.Message == nil {
|
|
return c.SendStatus(200)
|
|
}
|
|
|
|
msg := update.Message
|
|
chatID := msg.Chat.ID
|
|
text := strings.TrimSpace(msg.Text)
|
|
caption := strings.TrimSpace(msg.Caption)
|
|
hasAttachment := msg.Document != nil || len(msg.Photo) > 0
|
|
if text == "" && caption == "" && !hasAttachment {
|
|
return c.SendStatus(200)
|
|
}
|
|
|
|
// Buscar el config del agente que tenga este bot token
|
|
ai, tgCfg, err := models.GetAgenteBotConfig()
|
|
if err != nil || tgCfg == nil || tgCfg.BotToken != botToken {
|
|
log.Printf("[AGENT_WEBHOOK] Token no corresponde a ningún agente activo")
|
|
return c.SendStatus(200)
|
|
}
|
|
|
|
// Verificar autorización del chat (si hay whitelist configurada)
|
|
if !models.IsAgentAuthChat(chatID) {
|
|
// Si no hay ningún auth configurado, solo responder al chat_id del config
|
|
tgChatIDStr := strings.TrimSpace(tgCfg.ChatID)
|
|
tgChatID, _ := strconv.ParseInt(tgChatIDStr, 10, 64)
|
|
if tgChatID != 0 && chatID != tgChatID {
|
|
sendAgentReply(tgCfg.BotToken, chatID, "No tienes autorización para usar este agente.")
|
|
return c.SendStatus(200)
|
|
}
|
|
}
|
|
|
|
// Comandos de control (sincrónicos, respuesta inmediata)
|
|
if text == "/reset" || text == "/limpiar" || text == "/start" {
|
|
_ = models.ClearAgentHistory(chatID)
|
|
sendAgentReply(tgCfg.BotToken, chatID, "Historial borrado. Empezamos de cero.")
|
|
return c.SendStatus(200)
|
|
}
|
|
|
|
// Documento o foto adjunto: se descarga y queda "pendiente" para que el
|
|
// agente lo asocie a una factura (tool adjuntar_factura) según lo que diga
|
|
// el caption o lo que responda el usuario a continuación.
|
|
if hasAttachment {
|
|
fileID, fileName, mimeType := "", "documento", ""
|
|
switch {
|
|
case msg.Document != nil:
|
|
fileID = msg.Document.FileID
|
|
if msg.Document.FileName != "" {
|
|
fileName = msg.Document.FileName
|
|
}
|
|
mimeType = msg.Document.MimeType
|
|
case len(msg.Photo) > 0:
|
|
// Telegram manda varias resoluciones de la misma foto; la última es la de mayor calidad.
|
|
best := msg.Photo[len(msg.Photo)-1]
|
|
fileID = best.FileID
|
|
fileName = "foto.jpg"
|
|
mimeType = "image/jpeg"
|
|
}
|
|
if fileID != "" {
|
|
path, dlErr := services.DescargarDocumentoTelegram(tgCfg.BotToken, fileID, fileName, mimeType)
|
|
if dlErr != nil {
|
|
log.Printf("[AGENT_WEBHOOK] Error descargando adjunto: %v", dlErr)
|
|
sendAgentReply(tgCfg.BotToken, chatID, "No pude descargar el archivo que enviaste, intenta de nuevo.")
|
|
return c.SendStatus(200)
|
|
}
|
|
services.StageFacturaAttachment(chatID, path, fileName, mimeType)
|
|
nota := fmt.Sprintf("[Documento adjunto recibido: %s]", fileName)
|
|
switch {
|
|
case caption != "":
|
|
text = nota + " " + caption
|
|
case text == "":
|
|
text = nota + " ¿A qué cliente corresponde y cuál es el monto de la factura?"
|
|
default:
|
|
text = nota + " " + text
|
|
}
|
|
}
|
|
}
|
|
|
|
// Procesar en goroutine para responder 200 inmediatamente a Telegram
|
|
go func() {
|
|
response, err := services.ProcessAgentMessage(chatID, text, ai)
|
|
if err != nil {
|
|
log.Printf("[AGENT] Error procesando mensaje: %v", err)
|
|
response = fmt.Sprintf("Error interno: %s", err.Error())
|
|
}
|
|
if response == "" {
|
|
return
|
|
}
|
|
if sendErr := sendAgentReply(tgCfg.BotToken, chatID, response); sendErr != nil {
|
|
log.Printf("[AGENT] Error enviando respuesta: %v", sendErr)
|
|
}
|
|
}()
|
|
|
|
return c.SendStatus(200)
|
|
}
|
|
|
|
func sendAgentReply(botToken string, chatID int64, text string) error {
|
|
svc := &services.TelegramService{BotToken: botToken}
|
|
return svc.SendMessage(chatID, text)
|
|
}
|
|
|
|
// ─── CRUD de chats autorizados ────────────────────────────────────────────────
|
|
|
|
func AgentAuthList(c *fiber.Ctx) error {
|
|
items, err := models.GetAllAgentAuth()
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"items": items})
|
|
}
|
|
|
|
func AgentAuthCreate(c *fiber.Ctx) error {
|
|
type Req struct {
|
|
ChatID int64 `json:"chat_id"`
|
|
Nombre string `json:"nombre"`
|
|
}
|
|
var req Req
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
if req.ChatID == 0 {
|
|
return c.Status(400).JSON(fiber.Map{"error": "chat_id requerido"})
|
|
}
|
|
if err := models.CreateAgentAuth(req.ChatID, req.Nombre); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.Status(201).JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func AgentAuthDelete(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "id inválido"})
|
|
}
|
|
if err := models.DeleteAgentAuth(uint(id)); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// AgentHistoryClear borra el historial de conversación de un chat.
|
|
func AgentHistoryClear(c *fiber.Ctx) error {
|
|
chatID, err := strconv.ParseInt(c.Params("chat_id"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "chat_id inválido"})
|
|
}
|
|
if err := models.ClearAgentHistory(chatID); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|