"Responder a: todos los que escriban / solo a estos remitentes" — direcciones completas o dominios, separados por coma. Un dominio (@miempresa.com) habilita a toda esa empresa; lo que no pasa el filtro se marca leído y no se contesta: decisión tomada, no correo pendiente que se relee en cada revisión. El match de dominio es contra el dominio entero, nunca por sufijo: "empresa.com" no habilita a alguien de "malaempresa.com" ni de un subdominio. Ese es el clásico filtro que parece cerrado y no lo está, y acá decide a quién le habla el agente en nombre del negocio — hay un test por cada agujero. El filtro y el intervalo también se pueden cambiar por el update del canal sin recrearlo, porque recrearlo pide la contraseña de la casilla de nuevo. De paso quedó verificado que el módulo de soporte sigue intacto tras la unificación del SMTP: los catorce tests de soporte (auto-respuesta, parseo IMAP, clasificador, resúmenes) pasan en verde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
5.1 KiB
Go
126 lines
5.1 KiB
Go
package models
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// UmindCanal es un canal de mensajería adicional (Telegram, WhatsApp) que
|
|
// alimenta al mismo agente del tenant que ya atiende el widget web. Los
|
|
// secretos reales (bot token, access token de WhatsApp, etc.) viven cifrados
|
|
// en CredencialesEnc (ver pkg/services/umind_secrets.go) — el modelo solo
|
|
// persiste el string ya cifrado, no conoce la clave.
|
|
//
|
|
// WebhookSecret es un identificador público generado por nosotros, distinto
|
|
// del secreto real del proveedor, usado SOLO para enrutar el webhook
|
|
// entrante al canal correcto (va en la URL que se registra en
|
|
// Telegram/Meta). Evita que el token real del proveedor termine en logs de
|
|
// acceso o de un proxy intermedio.
|
|
type UmindCanal struct {
|
|
gorm.Model
|
|
AgenteID uint `json:"agente_id" gorm:"column:agente_id;index"`
|
|
Tipo string `json:"tipo" gorm:"column:tipo;size:20;not null"` // telegram | whatsapp
|
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
|
WebhookSecret string `json:"webhook_secret" gorm:"column:webhook_secret;uniqueIndex;size:40;not null"`
|
|
CredencialesEnc string `json:"-" gorm:"column:credenciales_enc;type:text"`
|
|
UltimoError string `json:"ultimo_error" gorm:"column:ultimo_error;type:text"`
|
|
// Si están activos, los mensajes de voz/audio e imágenes que llegan por
|
|
// este canal se transcriben (Whisper) o se les extrae el texto (OCR)
|
|
// antes de pasarlos al agente, en vez de ignorarse.
|
|
UsarWhisperAudio bool `json:"usar_whisper_audio" gorm:"column:usar_whisper_audio;default:false"`
|
|
UsarOcrImagenes bool `json:"usar_ocr_imagenes" gorm:"column:usar_ocr_imagenes;default:false"`
|
|
// Documentos adjuntos (PDF, Word, texto): se les extrae el contenido y se
|
|
// le pasa al agente como si el cliente lo hubiera escrito.
|
|
UsarArchivosDocs bool `json:"usar_archivos_docs" gorm:"column:usar_archivos_docs;default:false"`
|
|
// Solo para el canal correo: cada cuántos minutos se revisa la casilla.
|
|
// Lo elige el cliente — una inmobiliaria quiere 2, un estudio contable con
|
|
// 30 le sobra. Telegram y WhatsApp no lo usan: a ellos les llega webhook.
|
|
IntervaloMinutos int `json:"intervalo_minutos" gorm:"column:intervalo_minutos;default:5"`
|
|
// UltimaRevision es lo que decide si ya le toca — en la base y no en
|
|
// memoria, por la misma razón que el aviso de tope: los despliegues borran
|
|
// la RAM varias veces por día.
|
|
UltimaRevision *time.Time `json:"ultima_revision" gorm:"column:ultima_revision"`
|
|
// FiltroRemitentes acota a quién le responde el canal correo: direcciones
|
|
// o dominios separados por coma. Vacío = responde a todos. Un correo que
|
|
// no pasa el filtro se marca leído y no se contesta — el agente decidió,
|
|
// no está pendiente.
|
|
FiltroRemitentes string `json:"filtro_remitentes" gorm:"column:filtro_remitentes;type:text"`
|
|
}
|
|
|
|
// MarcarRevisionCanal deja constancia de que la casilla se acaba de revisar.
|
|
func MarcarRevisionCanal(id uint) {
|
|
ahora := time.Now()
|
|
app.Http.Database.DB.Model(&UmindCanal{}).Where("id = ?", id).
|
|
Update("ultima_revision", &ahora)
|
|
}
|
|
|
|
// GetCanalesCorreoPendientes devuelve los canales de correo activos a los que
|
|
// ya les toca revisión según su propio intervalo.
|
|
func GetCanalesCorreoPendientes() ([]UmindCanal, error) {
|
|
var items []UmindCanal
|
|
err := app.Http.Database.DB.
|
|
Where("tipo = ? AND activo = ?", "correo", true).
|
|
Where("ultima_revision IS NULL OR ultima_revision < NOW() - (intervalo_minutos || ' minutes')::interval").
|
|
Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
func (UmindCanal) TableName() string { return "umind_canales" }
|
|
|
|
func GenerarWebhookSecret() (string, error) {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("no se pudo generar el webhook_secret: %w", err)
|
|
}
|
|
return "umc_" + hex.EncodeToString(b), nil
|
|
}
|
|
|
|
func CreateUmindCanal(c *UmindCanal) error {
|
|
if c.WebhookSecret == "" {
|
|
secret, err := GenerarWebhookSecret()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.WebhookSecret = secret
|
|
}
|
|
return app.Http.Database.DB.Create(c).Error
|
|
}
|
|
|
|
func GetUmindCanalesByAgente(agenteID uint) ([]UmindCanal, error) {
|
|
var items []UmindCanal
|
|
err := app.Http.Database.DB.Where("agente_id = ?", agenteID).Order("id DESC").Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
func GetUmindCanalByID(id uint) (*UmindCanal, error) {
|
|
var c UmindCanal
|
|
if err := app.Http.Database.DB.First(&c, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// GetUmindCanalByWebhookSecret resuelve el canal a partir del identificador
|
|
// público que viene en la URL del webhook. Solo matchea si está activo.
|
|
func GetUmindCanalByWebhookSecret(tipo, webhookSecret string) (*UmindCanal, error) {
|
|
var c UmindCanal
|
|
err := app.Http.Database.DB.Where("tipo = ? AND webhook_secret = ? AND activo = ?", tipo, webhookSecret, true).First(&c).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func UpdateUmindCanal(id uint, updates map[string]interface{}) error {
|
|
return app.Http.Database.DB.Model(&UmindCanal{}).Where("id = ?", id).Updates(updates).Error
|
|
}
|
|
|
|
func DeleteUmindCanal(id uint) error {
|
|
return app.Http.Database.DB.Delete(&UmindCanal{}, id).Error
|
|
}
|