package services import ( "fmt" "log" "strconv" "strings" "sync" "time" "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // Los avisos programados: el dueño le pide al agente que le recuerde algo y // el aviso vuelve por donde se pidió. var avisosEnCurso sync.Mutex // DespacharAvisos corre cada minuto y manda los que ya vencieron. func DespacharAvisos() { if !avisosEnCurso.TryLock() { return } defer avisosEnCurso.Unlock() avisos, err := models.GetUmindAvisosVencidos() if err != nil { log.Printf("[UMIND_AVISO] no se pudieron listar: %v", err) return } for _, a := range avisos { // El reclamo es el lock: si otra instancia lo tomó, este ciclo lo salta. if !models.ReclamarAviso(a.ID) { continue } if err := entregarAviso(a); err != nil { log.Printf("[UMIND_AVISO] aviso %d no se pudo entregar: %v", a.ID, err) models.RegistrarEventoUmind(a.AgenteID, "error", "aviso", "No se pudo entregar un aviso programado: "+a.Titulo, err.Error()) models.MarcarAvisoFallido(a.ID, err.Error()) continue } if err := models.ReprogramarAviso(&a); err != nil { log.Printf("[UMIND_AVISO] aviso %d entregado pero no se pudo reprogramar: %v", a.ID, err) } } } // entregarAviso manda el aviso por el canal de donde salió el pedido. El // destino se fijó cuando se programó — nunca lo elige el texto del aviso. func entregarAviso(a models.UmindAviso) error { texto := "⏰ " + a.Titulo if strings.TrimSpace(a.Detalle) != "" { texto += "\n\n" + a.Detalle } switch { case strings.HasPrefix(a.Destino, "tg:"): chatID, err := strconv.ParseInt(strings.TrimPrefix(a.Destino, "tg:"), 10, 64) if err != nil { return fmt.Errorf("destino de telegram inválido: %w", err) } canal, err := models.GetUmindCanalPorTipo(a.AgenteID, "telegram") if err != nil { return fmt.Errorf("el canal de telegram ya no existe") } creds, err := DescifrarCredencialesCanal(canal.CredencialesEnc) if err != nil { return err } return (&TelegramService{}).SendMessageWithToken(chatID, texto, creds["bot_token"]) case strings.HasPrefix(a.Destino, "mail:"): para := strings.TrimPrefix(a.Destino, "mail:") canal, err := models.GetUmindCanalPorTipo(a.AgenteID, "correo") if err != nil { // Sin canal de correo propio se manda por el SMTP del sistema: // perder el aviso sería peor que mandarlo desde otra dirección. return app.Http.Mail.Send(para, a.Titulo, cuerpoHTMLAviso(a)) } creds, err := DescifrarCredencialesCanal(canal.CredencialesEnc) if err != nil { return err } return EnviarPorSMTP(DatosSMTP{ Host: creds["smtp_host"], Encriptado: creds["smtp_encriptado"], Usuario: creds["usuario"], Password: creds["password"], Desde: creds["usuario"], DesdeNombre: "Recordatorio", Puerto: puertoDe(creds["smtp_puerto"], 587), }, para, a.Titulo, cuerpoHTMLAviso(a)) default: return avisarAlDuenoDelEspacio(a) } } // avisarAlDuenoDelEspacio es el destino por defecto: los usuarios del portal // del cliente dueño del espacio, por correo y por la campanita. func avisarAlDuenoDelEspacio(a models.UmindAviso) error { agente, err := models.GetUmindAgenteByID(a.AgenteID) if err != nil { return err } tenant, err := models.GetUmindTenantByID(agente.TenantID) if err != nil || tenant.ClienteID == nil { return fmt.Errorf("el espacio no tiene un cliente asignado a quién avisarle") } usuarios, err := models.GetPortalUsersByClienteID(*tenant.ClienteID) if err != nil || len(usuarios) == 0 { return fmt.Errorf("el espacio no tiene usuarios a quién avisarle") } enviadoAlguno := false for _, u := range usuarios { models.CreateSistemaNotif(&models.SistemaNotificacion{ TipoUsuario: "portal_user", UsuarioID: u.ID, Titulo: a.Titulo, Cuerpo: a.Detalle, Icono: "⏰", Url: GetPublicURL() + "/portal/studio", }) if u.Email == "" { continue } if err := app.Http.Mail.Send(u.Email, "⏰ "+a.Titulo, cuerpoHTMLAviso(a)); err != nil { log.Printf("[UMIND_AVISO] no se pudo avisar a %s: %v", u.Email, err) continue } enviadoAlguno = true } if !enviadoAlguno { return fmt.Errorf("no se pudo entregar a ningún destinatario") } return nil } func cuerpoHTMLAviso(a models.UmindAviso) string { detalle := "" if strings.TrimSpace(a.Detalle) != "" { detalle = fmt.Sprintf(`
%s
`, a.Detalle) } return fmt.Sprintf(`Recordatorio programado
Se lo pediste a tu asistente en uMind.