"Avisame el 15 de marzo que vence la póliza de Acme, y todos los años" queda programado desde la conversación, visible y cancelable en el panel. Tres tools: programar, listar, cancelar. El aviso vuelve por donde se pidió — Telegram a ese chat, correo a esa casilla — o al dueño del espacio por correo y campanita. Nunca a una dirección que dicte la conversación: eso sería un cañón de spam con destinatario libre. Y las tools de aviso solo se le OFRECEN al modelo cuando la sesión es interna: el chat de prueba del panel (que corre autenticado) o un canal marcado como línea privada del dueño. En el widget público escribe cualquiera, y cualquiera no puede programarle recordatorios ni gastarle el plan a otro. El gate está en dos capas: la tool no se declara, y si igual la pide, el ejecutor la rechaza. Tres detalles que se pagan una sola vez: - El cron corre en memoria del proceso, sin lock distribuido: con dos instancias cada aviso saldría dos veces. El reclamo es un UPDATE condicional — la base ya es el árbitro, no hace falta traer otro. - Si el servidor estuvo caído, una repetición diaria se saltea los ciclos perdidos en vez de disparar diez avisos viejos de golpe. - Un fallo de SMTP devuelve el aviso a pendiente: una caída de correo no puede perder un vencimiento de póliza. Una fecha sin hora se entrega a las 9, no a medianoche, que es cuando nadie mira el teléfono. De paso: los archivos de los espacios uMind nunca se sirven por el estático de /uploads. El guard genérico solo sabe si hay sesión de panel, no de quién es el archivo — salen por su endpoint, que sí valida propiedad. Cerrado por construcción y no por acordarse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
3.5 KiB
Go
111 lines
3.5 KiB
Go
package models
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type WebSmsConfig struct {
|
|
gorm.Model
|
|
Username string `json:"username" gorm:"column:username;size:255;not null"`
|
|
ApiToken string `json:"api_token" gorm:"column:api_token;type:text;not null"`
|
|
Sender string `json:"sender" gorm:"column:sender;size:30"`
|
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
|
SecretKey string `json:"secret_key" gorm:"column:secret_key;size:64;uniqueIndex"`
|
|
}
|
|
|
|
func generateWebSmsSecret() string {
|
|
b := make([]byte, 24)
|
|
rand.Read(b)
|
|
return "sms_" + hex.EncodeToString(b)
|
|
}
|
|
|
|
func (WebSmsConfig) TableName() string { return "websms_config" }
|
|
|
|
func GetWebSmsConfig() (*WebSmsConfig, error) {
|
|
var item WebSmsConfig
|
|
if err := app.Http.Database.DB.Where("activo = ?", true).Order("id DESC").First(&item).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
func SaveWebSmsConfig(s WebSmsConfig) error {
|
|
app.Http.Database.DB.Model(&WebSmsConfig{}).
|
|
Where("activo = ?", true).
|
|
Update("activo", false)
|
|
s.Activo = true
|
|
if s.ID > 0 {
|
|
// preserve existing secret_key
|
|
var existing WebSmsConfig
|
|
app.Http.Database.DB.First(&existing, s.ID)
|
|
if existing.SecretKey == "" {
|
|
existing.SecretKey = generateWebSmsSecret()
|
|
}
|
|
return app.Http.Database.DB.Model(&s).Updates(map[string]interface{}{
|
|
"username": s.Username,
|
|
"api_token": s.ApiToken,
|
|
"sender": s.Sender,
|
|
"notas": s.Notas,
|
|
"activo": true,
|
|
"secret_key": existing.SecretKey,
|
|
}).Error
|
|
}
|
|
s.SecretKey = generateWebSmsSecret()
|
|
return app.Http.Database.DB.Create(&s).Error
|
|
}
|
|
|
|
// ─── Log de SMS enviados ────────────────────────────────────────────────────
|
|
|
|
type WebSmsLog struct {
|
|
gorm.Model
|
|
Para string `json:"para" gorm:"column:para;size:30;index"`
|
|
Mensaje string `json:"mensaje" gorm:"column:mensaje;type:text"`
|
|
Status string `json:"status" gorm:"column:status;size:20"`
|
|
MsgID string `json:"msg_id" gorm:"column:msg_id;size:100"`
|
|
Error string `json:"error" gorm:"column:error;type:text"`
|
|
}
|
|
|
|
func (WebSmsLog) TableName() string { return "websms_log" }
|
|
|
|
func CreateWebSmsLog(entry *WebSmsLog) error {
|
|
return app.Http.Database.DB.Create(entry).Error
|
|
}
|
|
|
|
func GetWebSmsLogs(limit int) ([]WebSmsLog, error) {
|
|
var logs []WebSmsLog
|
|
if err := app.Http.Database.DB.Order("id DESC").Limit(limit).Find(&logs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return logs, nil
|
|
}
|
|
|
|
// ─── Webhook ACK log ────────────────────────────────────────────────────────
|
|
|
|
type WebSmsWebhookLog struct {
|
|
gorm.Model
|
|
Tipo string `json:"tipo" gorm:"column:tipo;size:20;index"` // delivery | click | incoming
|
|
MsgID string `json:"msg_id" gorm:"column:msg_id;size:100;index"`
|
|
Para string `json:"para" gorm:"column:para;size:30"`
|
|
Status string `json:"status" gorm:"column:status;size:30"`
|
|
Raw string `json:"raw" gorm:"column:raw;type:text"`
|
|
}
|
|
|
|
func (WebSmsWebhookLog) TableName() string { return "websms_webhook_log" }
|
|
|
|
func CreateWebSmsWebhookLog(entry *WebSmsWebhookLog) error {
|
|
return app.Http.Database.DB.Create(entry).Error
|
|
}
|
|
|
|
func GetWebSmsWebhookLogs(limit int) ([]WebSmsWebhookLog, error) {
|
|
var logs []WebSmsWebhookLog
|
|
if err := app.Http.Database.DB.Order("id DESC").Limit(limit).Find(&logs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return logs, nil
|
|
}
|