diff --git a/migrations/migrate.go b/migrations/migrate.go index e2b4542..63e67ad 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -441,6 +441,7 @@ func SeedAutomatizacionIA() { // 2. Definir submódulos entries := []struct{ title, desc, url string }{ {"Asistente", "Chat propio: mismo motor y tools que el bot de Telegram", "/app/asistente"}, + {"Chats autorizados", "Quién puede hablarle al bot de Telegram y ejecutar acciones", "/app/agente/chats-autorizados"}, {"Plantillas de Documento", "Fuente de verdad de cotización, contrato, acta y cuenta de cobro", "/app/automatizacion/plantillas"}, {"Tarifas", "Valor por hora, licencias, VMs y márgenes usados al cotizar", "/app/automatizacion/tarifas"}, } diff --git a/pkg/services/telegram_service.go b/pkg/services/telegram_service.go index 88092cb..fa164e1 100755 --- a/pkg/services/telegram_service.go +++ b/pkg/services/telegram_service.go @@ -84,3 +84,65 @@ func (ts *TelegramService) SendMessageWithToken(chatID interface{}, message, bot svc := &TelegramService{BotToken: botToken} return svc.SendMessage(chatID, message) } + +// RemitenteReciente es alguien que le escribió al bot recientemente, útil para +// descubrir su chat_id sin tener que pedírselo por otro medio. +type RemitenteReciente struct { + ChatID int64 + Nombre string + Mensaje string +} + +// UpdatesRecientesDelBot consulta getUpdates y devuelve, más reciente primero, +// los remitentes que le han escrito al bot (hasta los últimos 100 updates que +// Telegram todavía tenga en cola). +func UpdatesRecientesDelBot(botToken string) ([]RemitenteReciente, error) { + if botToken == "" { + return nil, fmt.Errorf("bot token vacío") + } + resp, err := http.Get(fmt.Sprintf("https://api.telegram.org/bot%s/getUpdates?limit=100", botToken)) //nolint:noctx + if err != nil { + return nil, fmt.Errorf("no se pudo consultar Telegram: %w", err) + } + defer resp.Body.Close() + + var result struct { + OK bool `json:"ok"` + Result []struct { + Message struct { + From struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` + } `json:"from"` + Chat struct { + ID int64 `json:"id"` + } `json:"chat"` + Text string `json:"text"` + } `json:"message"` + } `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || !result.OK { + return nil, fmt.Errorf("Telegram no devolvió updates válidos") + } + + out := make([]RemitenteReciente, 0, len(result.Result)) + for i := len(result.Result) - 1; i >= 0; i-- { + m := result.Result[i].Message + if m.Chat.ID == 0 { + continue + } + nombre := m.From.FirstName + if m.From.LastName != "" { + nombre += " " + m.From.LastName + } + if m.From.Username != "" { + nombre += " (@" + m.From.Username + ")" + } + if nombre == "" { + nombre = fmt.Sprintf("Chat %d", m.Chat.ID) + } + out = append(out, RemitenteReciente{ChatID: m.Chat.ID, Nombre: nombre, Mensaje: m.Text}) + } + return out, nil +} diff --git a/resources/views/automatizacion/chats_autorizados.html b/resources/views/automatizacion/chats_autorizados.html new file mode 100644 index 0000000..ad45a0f --- /dev/null +++ b/resources/views/automatizacion/chats_autorizados.html @@ -0,0 +1,195 @@ +
+
+ Cargando... +
+ +
+
+
+

Chats autorizados del agente

+

Quién puede hablarle al bot de Telegram y pedirle acciones (crear cotizaciones, deploys, contabilidad, etc.)

+
+ +
+ +
+ Cualquiera puede encontrar y escribirle al bot en Telegram, pero solo los chats de esta lista reciben respuesta y pueden ejecutar acciones. Si la lista está vacía, solo responde al chat configurado en /app/telegram. +
+ +
+ + + + + + + + + + + + + + + +
NombreChat IDEstado
Nadie autorizado todavía — el bot solo responde al chat configurado en /app/telegram
+
+
+ + +
+
+

Autorizar chat

+

Pide a la persona que le escriba cualquier mensaje al bot en Telegram y luego busca su chat entre los mensajes recientes.

+ +
+ +
+ +
+

No hay mensajes nuevos de nadie sin autorizar todavía.

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+

¿Quitar el acceso de este chat?

+

Dejará de poder usar al agente por Telegram.

+
+ + +
+
+
+ +
+
+ + diff --git a/rest/controllers/telegram_agent_controller.go b/rest/controllers/telegram_agent_controller.go index 3409c30..8006e9b 100644 --- a/rest/controllers/telegram_agent_controller.go +++ b/rest/controllers/telegram_agent_controller.go @@ -170,6 +170,51 @@ func sendAgentReply(botToken string, chatID int64, text string) error { // ─── 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 { diff --git a/rest/routes/hermes.go b/rest/routes/hermes.go index 2c75525..b3bc9ce 100644 --- a/rest/routes/hermes.go +++ b/rest/routes/hermes.go @@ -401,6 +401,7 @@ func AdminApiRoutes(api fiber.Router) { // ─── Agente Telegram: chats autorizados ────────────────────────────────── h.Get("/agent/auth", controllers.AgentAuthList) + h.Get("/agent/auth/recientes", controllers.AgentAuthRecientes) h.Post("/agent/auth", controllers.AgentAuthCreate) h.Delete("/agent/auth/:id", controllers.AgentAuthDelete) h.Delete("/agent/history/:chat_id", controllers.AgentHistoryClear) diff --git a/rest/routes/renovaciones.go b/rest/routes/renovaciones.go index 3ad6e02..b5445b9 100644 --- a/rest/routes/renovaciones.go +++ b/rest/routes/renovaciones.go @@ -76,6 +76,13 @@ func RenovacionesRoutes(protected fiber.Router) { protected.Post("/api/asistente/chat", controllers.PostAsistenteChat) protected.Delete("/api/asistente/historial", controllers.DeleteAsistenteHistorial) + // ─── Automatización IA: Chats autorizados del agente (Telegram) ──── + protected.Get("/agente/chats-autorizados", middlewares.MenuMiddleware, controllers.AgentAuthView) + protected.Get("/api/agent/auth", controllers.AgentAuthList) + protected.Get("/api/agent/auth/recientes", controllers.AgentAuthRecientes) + protected.Post("/api/agent/auth", controllers.AgentAuthCreate) + protected.Delete("/api/agent/auth/:id", controllers.AgentAuthDelete) + // ─── Automatización IA: Plantillas de documento ──────────────────── protected.Get("/automatizacion/plantillas", middlewares.MenuMiddleware, controllers.PlantillasDocumentoView) protected.Get("/api/plantillas-documento", controllers.GetPlantillasDocumento)