Files
soft_usite/rest/controllers/telegram_agent_controller.go
T
Lizandro GDandClaude Sonnet 5 5607836261 feat: pantalla para administrar chats autorizados del agente de Telegram
Antes la whitelist de telegram_agent_auth solo se podía tocar con curl/Postman
contra /api/v2/agent/auth. Se agrega /app/agente/chats-autorizados con un
CRUD simple, más un botón "buscar mensajes recientes" (UpdatesRecientesDelBot,
vía getUpdates) para descubrir el chat_id de alguien que le acaba de escribir
al bot sin tener que pedírselo por otro medio. Entrada nueva en el menú lateral
del módulo Automatización IA.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 02:37:40 +00:00

266 lines
8.5 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.StageTelegramAttachment(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, services.FormatearParaTelegram(text))
}
// ─── CRUD de chats autorizados ────────────────────────────────────────────────
// AgentAuthView renderiza la pantalla de administración de chats autorizados.
func AgentAuthView(c *fiber.Ctx) error {
return c.Render("automatizacion/chats_autorizados", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
// AgentAuthRecientes consulta los últimos mensajes recibidos por el bot del
// agente (getUpdates) y devuelve los remitentes distintos que todavía no están
// autorizados, para que el admin pueda autorizarlos con un clic sin tener que
// buscar el chat_id a mano.
func AgentAuthRecientes(c *fiber.Ctx) error {
tgCfg, err := models.GetAgentTelegramConfig()
if err != nil || tgCfg.BotToken == "" {
return c.Status(400).JSON(fiber.Map{"error": "No hay un bot de Telegram configurado para el agente"})
}
autorizados, _ := models.GetAllAgentAuth()
yaAutorizado := map[int64]bool{}
for _, a := range autorizados {
yaAutorizado[a.ChatID] = true
}
remitentes, err := services.UpdatesRecientesDelBot(tgCfg.BotToken)
if err != nil {
return c.Status(502).JSON(fiber.Map{"error": err.Error()})
}
type item struct {
ChatID int64 `json:"chat_id"`
Nombre string `json:"nombre"`
Mensaje string `json:"mensaje"`
}
seen := map[int64]bool{}
out := make([]item, 0, len(remitentes))
for _, r := range remitentes {
if seen[r.ChatID] || yaAutorizado[r.ChatID] {
continue
}
seen[r.ChatID] = true
out = append(out, item{ChatID: r.ChatID, Nombre: r.Nombre, Mensaje: r.Mensaje})
}
return c.JSON(fiber.Map{"items": out})
}
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})
}