Files
soft_usite/pkg/services/notif_dispatch.go
T
Lizandro GuarnizoandClaude Opus 5 d1bbb4a508 feat(soporte): avisar por Telegram cuando entra un correo, con resumen si es largo
Los tickets del portal ya avisaban por Telegram; los que entran por correo solo
mandaban un mail al admin, que es justo el canal que nadie mira a tiempo. Ahora
pasan por el mismo despachador de notificaciones, así que se prenden y apagan
desde /app/notif-config como cualquier otro evento — nuevo ticket y respuesta
del cliente por separado.

El cuerpo del aviso es un resumen cuando el correo pasa los 700 caracteres
(hilos reenviados, texto pegado); abajo de eso va tal cual, porque resumir tres
renglones es gastar una llamada de IA para decir lo mismo. Si la IA falla, se
manda recortado. El ticket guarda siempre el texto completo.

DispatchTicketNuevo ahora acepta portalUser nil: un correo no tiene usuario de
portal detrás y no por eso hay que dejar de avisar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:12:36 -05:00

454 lines
14 KiB
Go

package services
import (
"fmt"
"log"
"net/url"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
// ─── DispatchTicketNuevo ──────────────────────────────────────────────────────
// Notifica al admin cuando un portal user crea un ticket.
// portalUser puede ser nil: los tickets que entran por correo no tienen usuario
// de portal detrás, y no por eso hay que dejar de avisar.
func DispatchTicketNuevo(ticket *models.ProyectoTicket, portalUser *models.PortalUser, proyectoNombre string) {
if ticket == nil {
return
}
_ = portalUser
cfg := models.GetNotifConfig("ticket_nuevo", "admin")
if cfg == nil {
return
}
ticketPath := adminTicketPath(ticket.ID)
ticketURL := absAppURL(ticketPath)
titulo := fmt.Sprintf("Nuevo ticket: %s", ticket.Titulo)
cuerpo := fmt.Sprintf("Cliente: %s\nProyecto: %s\n%s", ticket.AutorNombre, proyectoNombre, ticket.Descripcion)
if cfg.CanalSistema {
_ = models.CreateSistemaNotif(&models.SistemaNotificacion{
TipoUsuario: "admin",
UsuarioID: 0,
Titulo: titulo,
Cuerpo: cuerpo,
Url: ticketPath,
Icono: "🎫",
})
}
if cfg.CanalEmail {
if adminEmail := getAdminEmail(); adminEmail != "" {
go SendTicketNuevoAdmin(adminEmail, proyectoNombre, ticket.AutorNombre, ticket.Titulo, ticket.Descripcion, ticketURL)
}
}
if cfg.CanalTelegram {
msg := fmt.Sprintf("🎫 <b>Nuevo ticket</b>\nProyecto: <b>%s</b>\nCliente: %s\nTítulo: <b>%s</b>\n\n%s\n\n🔗 %s",
escapeTelegramHTML(proyectoNombre),
escapeTelegramHTML(ticket.AutorNombre),
escapeTelegramHTML(ticket.Titulo),
escapeTelegramHTML(ticket.Descripcion),
telegramHTMLLink(ticketURL, "Ver ticket"))
sendTelegramAdmin(msg)
}
}
// ─── DispatchTicketRespuestaCliente ──────────────────────────────────────────
// Notifica al admin cuando el portal user responde un ticket.
func DispatchTicketRespuestaCliente(ticket *models.ProyectoTicket, contenido string, proyectoNombre string) {
if ticket == nil {
return
}
cfg := models.GetNotifConfig("ticket_respuesta_cliente", "admin")
if cfg == nil {
return
}
ticketPath := adminTicketPath(ticket.ID)
ticketURL := absAppURL(ticketPath)
titulo := fmt.Sprintf("Respuesta de cliente en: %s", ticket.Titulo)
cuerpo := fmt.Sprintf("Cliente: %s\nProyecto: %s\n%s", ticket.AutorNombre, proyectoNombre, contenido)
if cfg.CanalSistema {
_ = models.CreateSistemaNotif(&models.SistemaNotificacion{
TipoUsuario: "admin",
UsuarioID: 0,
Titulo: titulo,
Cuerpo: cuerpo,
Url: ticketPath,
Icono: "💬",
})
}
if cfg.CanalEmail {
if adminEmail := getAdminEmail(); adminEmail != "" {
go SendTicketRespuestaAdmin(adminEmail, proyectoNombre, ticket.AutorNombre, ticket.Titulo, contenido, ticketURL)
}
}
if cfg.CanalTelegram {
msg := fmt.Sprintf("💬 <b>Respuesta de cliente</b>\nProyecto: <b>%s</b>\nCliente: %s\n\n%s\n\n🔗 %s",
escapeTelegramHTML(proyectoNombre),
escapeTelegramHTML(ticket.AutorNombre),
escapeTelegramHTML(contenido),
telegramHTMLLink(ticketURL, "Ver ticket"))
sendTelegramAdmin(msg)
}
}
// ─── DispatchTicketRespuestaAdmin ─────────────────────────────────────────────
// Notifica al portal user cuando el admin responde un ticket.
func DispatchTicketRespuestaAdmin(ticket *models.ProyectoTicket, contenido string, proyectoNombre string) {
if ticket == nil {
return
}
proyectoSlug := ""
if ticket.ProyectoID != nil {
if proy, err := models.GetProyectoByID(*ticket.ProyectoID); err == nil {
proyectoSlug = proy.Slug
proyectoNombre = proy.Nombre
}
}
// Si no está asociado a un portal user (ej: ticket por email), responder por email directo
if ticket.PortalUserID == nil {
if ticket.EmailFrom != "" {
go SendTicketRespuestaPortalUser(ticket.EmailFrom, ticket.AutorNombre, proyectoNombre, ticket.Titulo, contenido, "")
}
return
}
portalUser, err := models.GetPortalUserByID(*ticket.PortalUserID)
if err != nil || portalUser == nil {
log.Printf("[Notif] PortalUser %d no encontrado: %v", *ticket.PortalUserID, err)
return
}
cfg := models.GetNotifConfig("ticket_respuesta_admin", "portal_user")
if cfg == nil {
return
}
portalPath := portalTicketPath(proyectoSlug, ticket.ID)
portalURL := absAppURL(portalPath)
titulo := fmt.Sprintf("Respuesta en tu ticket: %s", ticket.Titulo)
cuerpo := fmt.Sprintf("El equipo respondió: %s", contenido)
if cfg.CanalSistema {
_ = models.CreateSistemaNotif(&models.SistemaNotificacion{
TipoUsuario: "portal_user",
UsuarioID: portalUser.ID,
Titulo: titulo,
Cuerpo: cuerpo,
Url: portalPath,
Icono: "💬",
})
}
if cfg.CanalEmail && portalUser.Email != "" {
go SendTicketRespuestaPortalUser(portalUser.Email, portalUser.Nombre, proyectoNombre, ticket.Titulo, contenido, portalURL)
}
if cfg.CanalTelegram && portalUser.TelegramChatID != "" {
msg := fmt.Sprintf("💬 <b>El equipo respondió tu ticket</b>\nProyecto: <b>%s</b>\nTicket: <b>%s</b>\n\n%s\n\n🔗 %s",
escapeTelegramHTML(proyectoNombre),
escapeTelegramHTML(ticket.Titulo),
escapeTelegramHTML(contenido),
telegramHTMLLink(portalURL, "Ver en el portal"))
if err := sendTelegramPortalUser(portalUser.TelegramChatID, msg); err != nil {
log.Printf("[Notif] Error telegram portal_user %d: %v", portalUser.ID, err)
}
}
}
// ─── DispatchFacturaSubida ────────────────────────────────────────────────────
// Notifica a los portal_users del cliente cuando se sube una factura.
func DispatchFacturaSubida(factura *models.Factura) {
if factura == nil || factura.ClienteID == 0 {
return
}
cfg := models.GetNotifConfig("factura_subida", "portal_user")
if cfg == nil {
return
}
portalUsers, err := models.GetPortalUsersByClienteID(factura.ClienteID)
if err != nil || len(portalUsers) == 0 {
return
}
clienteNombre := factura.Cliente.Empresa
if clienteNombre == "" {
clienteNombre = factura.Cliente.Nombre
}
portalPath := "/portal/dashboard"
if factura.ProyectoID != nil && factura.Proyecto != nil && factura.Proyecto.Slug != "" {
portalPath = fmt.Sprintf("/portal/proyecto/%s?tab=Facturas", url.PathEscape(factura.Proyecto.Slug))
}
portalURL := absAppURL(portalPath)
for _, u := range portalUsers {
u := u
titulo := fmt.Sprintf("Nueva factura disponible: %s", factura.Numero)
cuerpo := fmt.Sprintf("Factura %s por %s %.2f", factura.Numero, factura.Moneda, factura.Monto)
if cfg.CanalSistema {
_ = models.CreateSistemaNotif(&models.SistemaNotificacion{
TipoUsuario: "portal_user",
UsuarioID: u.ID,
Titulo: titulo,
Cuerpo: cuerpo,
Url: portalPath,
Icono: "🧾",
})
}
if cfg.CanalEmail && u.Email != "" {
go SendFacturaSubidaPortalUser(u.Email, u.Nombre, clienteNombre, factura.Numero, factura.Monto, factura.Moneda, portalURL)
}
if cfg.CanalTelegram && u.TelegramChatID != "" {
msg := fmt.Sprintf("🧾 <b>Nueva factura disponible</b>\nNúmero: <b>%s</b>\nMonto: %s %.2f\n\n🔗 %s",
escapeTelegramHTML(factura.Numero),
escapeTelegramHTML(factura.Moneda),
factura.Monto,
telegramHTMLLink(portalURL, "Ver en el portal"))
if err := sendTelegramPortalUser(u.TelegramChatID, msg); err != nil {
log.Printf("[Notif] Error telegram portal_user %d: %v", u.ID, err)
}
}
}
log.Printf("[Notif] DispatchFacturaSubida factura=%d clientes notificados=%d", factura.ID, len(portalUsers))
}
// ─── helpers ─────────────────────────────────────────────────────────────────
func getAdminEmail() string {
cfg, err := models.GetSmtpConfig()
if err != nil || cfg == nil {
return ""
}
return cfg.FromAddress
}
func getAdminBotToken() string {
configs, err := models.GetAllTelegramConfigs()
if err != nil {
return ""
}
for _, c := range configs {
if c.Activo {
return c.BotToken
}
}
return ""
}
// sendTelegramPortalUser intenta enviar usando todos los bots activos hasta que uno funcione.
// Esto evita fallos cuando el usuario se vinculó con un bot distinto al primer bot activo.
func sendTelegramPortalUser(chatID string, mensaje string) error {
configs, err := models.GetAllTelegramConfigs()
if err != nil {
return err
}
var lastErr error
for _, cfg := range configs {
if !cfg.Activo || cfg.BotToken == "" {
continue
}
ts := &TelegramService{BotToken: cfg.BotToken}
if err := ts.SendMessage(chatID, mensaje); err != nil {
lastErr = err
continue
}
return nil
}
if lastErr != nil {
return lastErr
}
return fmt.Errorf("no hay bots de Telegram activos para enviar")
}
// NotificarReniceAction envía Telegram cuando el agente aplica un renice automático.
func NotificarReniceAction(servidorNombre, accion string) {
msg := fmt.Sprintf("🔧 <b>Renice automático aplicado</b>\nServidor: <b>%s</b>\n%s\n\n<i>CPU sostenida por encima del umbral.</i>",
escapeTelegramHTML(servidorNombre),
escapeTelegramHTML(accion))
sendTelegramAdmin(msg)
}
// ─── Tareas ──────────────────────────────────────────────────────────────────
func NotificarTareaAsignada(t *models.Tarea) {
if t == nil || t.AsignadoID == nil {
return
}
cfg := models.GetNotifConfig("tarea_asignada", "admin")
if cfg == nil {
return
}
asignado := ""
if t.Asignado != nil {
asignado = t.Asignado.Name
}
titulo := fmt.Sprintf("📋 Nueva tarea asignada: %s", t.Titulo)
cuerpo := fmt.Sprintf("Asignado a: %s | Prioridad: %s\n%s", asignado, t.Prioridad, t.Descripcion)
tareaPath := "/app/tareas"
if cfg.CanalSistema {
_ = models.CreateSistemaNotif(&models.SistemaNotificacion{
TipoUsuario: "admin",
UsuarioID: 0,
Titulo: titulo,
Cuerpo: cuerpo,
Url: tareaPath,
Icono: "📋",
})
}
if cfg.CanalEmail {
tareaURL := absAppURL(tareaPath)
if t.Asignado != nil && t.Asignado.Email != "" {
go SendTareaAsignadaEmail(t.Asignado.Email, t.Asignado.Name, t.Titulo, t.Prioridad, t.Descripcion, tareaURL)
} else if adminEmail := getAdminEmail(); adminEmail != "" {
go SendTareaNotifAdmin(adminEmail, titulo, cuerpo, tareaURL)
}
}
if cfg.CanalTelegram {
msg := fmt.Sprintf("📋 <b>Nueva tarea asignada</b>\n<b>%s</b>\nAsignado a: %s\nPrioridad: %s\n\n%s",
escapeTelegramHTML(t.Titulo),
escapeTelegramHTML(asignado),
escapeTelegramHTML(t.Prioridad),
escapeTelegramHTML(t.Descripcion))
sendTelegramAdmin(msg)
// También enviar al chat del usuario asignado si tiene TelegramChatID
if t.Asignado != nil && t.Asignado.TelegramChatID != "" {
go sendTelegramToUser(t.Asignado.TelegramChatID, msg)
}
}
}
func NotificarTareaEstado(t *models.Tarea) {
if t == nil {
return
}
cfg := models.GetNotifConfig("tarea_estado", "admin")
if cfg == nil {
return
}
etiquetas := map[string]string{
"por_hacer": "📥 Por hacer",
"en_progreso": "🔄 En progreso",
"revision": "🔍 En revisión",
"hecho": "✅ Hecho",
}
label := etiquetas[t.Estado]
if label == "" {
label = t.Estado
}
asignado := ""
asignadoSuffix := ""
if t.Asignado != nil {
asignado = t.Asignado.Name
asignadoSuffix = "\nAsignado: " + asignado
}
titulo := fmt.Sprintf("🔀 Tarea → %s: %s", label, t.Titulo)
cuerpo := fmt.Sprintf("Estado: %s%s", label, asignadoSuffix)
tareaPath := "/app/tareas"
if cfg.CanalSistema {
_ = models.CreateSistemaNotif(&models.SistemaNotificacion{
TipoUsuario: "admin",
UsuarioID: 0,
Titulo: titulo,
Cuerpo: cuerpo,
Url: tareaPath,
Icono: "🔀",
})
}
if cfg.CanalEmail {
if adminEmail := getAdminEmail(); adminEmail != "" {
go SendTareaNotifAdmin(adminEmail, titulo, cuerpo, absAppURL(tareaPath))
}
}
if cfg.CanalTelegram {
msg := fmt.Sprintf("🔀 <b>Tarea movida → %s</b>\n<b>%s</b>%s",
label,
escapeTelegramHTML(t.Titulo),
escapeTelegramHTML(asignadoSuffix))
sendTelegramAdmin(msg)
}
}
func NotificarTareaComentario(t *models.Tarea, contenido string) {
if t == nil {
return
}
cfg := models.GetNotifConfig("tarea_comentario", "admin")
if cfg == nil {
return
}
titulo := fmt.Sprintf("💬 Nuevo comentario en tarea: %s", t.Titulo)
tareaPath := "/app/tareas"
if cfg.CanalSistema {
_ = models.CreateSistemaNotif(&models.SistemaNotificacion{
TipoUsuario: "admin",
UsuarioID: 0,
Titulo: titulo,
Cuerpo: contenido,
Url: tareaPath,
Icono: "💬",
})
}
if cfg.CanalEmail {
if adminEmail := getAdminEmail(); adminEmail != "" {
go SendTareaNotifAdmin(adminEmail, titulo, contenido, absAppURL(tareaPath))
}
}
if cfg.CanalTelegram {
msg := fmt.Sprintf("💬 <b>Nuevo comentario en tarea</b>\n<b>%s</b>\n\n%s",
escapeTelegramHTML(t.Titulo),
escapeTelegramHTML(contenido))
sendTelegramAdmin(msg)
}
}
func sendTelegramAdmin(mensaje string) {
configs, err := models.GetAllTelegramConfigs()
if err != nil {
log.Printf("[Notif] Error obteniendo configs telegram: %v", err)
return
}
for _, cfg := range configs {
if !cfg.Activo {
continue
}
ts := &TelegramService{BotToken: cfg.BotToken}
sendErr := ts.SendMessage(cfg.ChatID, mensaje)
logEntry := &models.TelegramLog{
TelegramConfigID: cfg.ID,
Titulo: "Notificación del sistema",
Mensaje: mensaje,
Estado: "ok",
}
if sendErr != nil {
log.Printf("[Notif] Error enviando telegram admin (cfg %d): %v", cfg.ID, sendErr)
logEntry.Estado = "failed"
logEntry.ErrorMsg = sendErr.Error()
}
_ = models.CreateTelegramLog(logEntry)
}
}
// sendTelegramToUser envía un mensaje al chat de un usuario usando el primer bot config activo.
func sendTelegramToUser(chatID string, mensaje string) {
configs, err := models.GetAllTelegramConfigs()
if err != nil || len(configs) == 0 {
log.Printf("[Notif] No hay configs telegram para enviar a usuario %s", chatID)
return
}
for _, cfg := range configs {
if !cfg.Activo {
continue
}
ts := &TelegramService{BotToken: cfg.BotToken}
if err := ts.SendMessage(chatID, mensaje); err != nil {
log.Printf("[Notif] Error enviando telegram a %s: %v", chatID, err)
}
return
}
}