This commit is contained in:
Lizandro Guarnizo
2026-02-03 21:30:55 -05:00
parent 44423adb32
commit 1aae148396
89 changed files with 8581 additions and 7687 deletions
-555
View File
@@ -1,555 +0,0 @@
# 🚀 Arquitectura Mejorada - WhatsApp Bot Manager
## 📋 Tabla de Contenidos
- [Introducción](#introducción)
- [Arquitectura General](#arquitectura-general)
- [Componentes Clave](#componentes-clave)
- [Flujo de Mensajes](#flujo-de-mensajes)
- [Instalación y Configuración](#instalación-y-configuración)
- [Deployment](#deployment)
- [Monitoreo](#monitoreo)
- [Troubleshooting](#troubleshooting)
---
## Introducción
Esta arquitectura implementa las **mejores prácticas** para chatbots de WhatsApp Business API en PHP, optimizando:
**Respuesta rápida al webhook** (< 1 segundo)
**Procesamiento asíncrono** con colas Redis
**Rate limiting automático** (respeta límites de WhatsApp)
**Logging estructurado** con Monolog
**Gestión de estados** en Redis (conversaciones)
**Escalabilidad horizontal** con workers
---
## Arquitectura General
```
┌─────────────────┐
│ WhatsApp API │
└────────┬────────┘
│ POST
┌─────────────────────┐
│ webhook_optimized │ ← Responde 200 OK < 1 seg
│ .php │
└────────┬────────────┘
│ push
┌─────────────────────┐
│ Redis Queue │ ← Cola FIFO con prioridad
│ (messages, media) │
└────────┬────────────┘
│ pop (BRPOP)
┌─────────────────────┐
│ worker.php │ ← Procesa mensajes async
│ (3+ procesos) │
└────────┬────────────┘
┌─────────────────────┐
│ BotService │ ← Lógica del bot
│ WhatsAppService │
└─────────────────────┘
```
### Ventajas sobre arquitectura anterior
| Antes | Ahora |
|-------|-------|
| Webhook procesa todo síncronamente | Webhook solo encola (< 100ms) |
| Riesgo de timeout si bot tarda > 5s | Worker procesa sin límite de tiempo |
| Sin control de límites de envío | Rate limiting automático (80 msg/s) |
| Logs dispersos en archivos | Logging centralizado con Monolog |
| Estados en BD (lentos) | Estados en Redis (rápido) |
| Difícil escalar | Múltiples workers en paralelo |
---
## Componentes Clave
### 1. **RedisQueue** (`queue/RedisQueue.php`)
Gestiona colas de mensajes con soporte para:
- ✅ Prioridades (alta, normal, baja)
- ✅ Retry con backoff exponencial
- ✅ Dead Letter Queue (DLQ) para mensajes fallidos
- ✅ Rate limiting integrado
- ✅ Delayed messages (programar envíos)
**Ejemplo de uso:**
```php
use WhatsApp\Queue\RedisQueue;
$queue = new RedisQueue();
// Encolar mensaje
$queue->push('messages', [
'user' => $user,
'messageText' => 'Hola',
'messageType' => 'text'
], 0); // Prioridad alta
// Obtener mensaje (bloqueante)
$message = $queue->pop(['messages', 'media'], 5); // timeout 5s
// Re-encolar con retry si falla
if (!$success) {
$queue->retry('messages', $message, 3); // max 3 intentos
}
// Estadísticas
$stats = $queue->getStats();
// ['messages' => ['pending' => 5, 'delayed' => 2, 'failed' => 1]]
```
### 2. **ConversationState** (`queue/ConversationState.php`)
Gestiona estados de conversación en Redis (rápido):
```php
use WhatsApp\Queue\ConversationState;
$state = new ConversationState();
// Establecer estado
$state->setState($userId, 'waiting_name', [
'step' => 1,
'retries' => 0
], 3600); // Expira en 1 hora
// Obtener estado
$current = $state->getState($userId);
// ['state' => 'waiting_name', 'context' => [...], 'updated_at' => ...]
// Actualizar contexto
$state->updateContext($userId, ['name' => 'Juan']);
// Limpiar al finalizar
$state->clearState($userId);
// Datos temporales (para formularios multi-paso)
$state->setTemporaryData($userId, 'email', 'juan@example.com');
$email = $state->getTemporaryData($userId, 'email');
```
### 3. **Worker** (`worker.php`)
Procesa mensajes de forma asíncrona:
```bash
# Ejecutar worker manualmente
php worker.php messages
# Worker daemon (loop infinito)
php worker.php --daemon
# Worker específico para media
php worker.php media --daemon
```
**Características:**
- ✅ Procesa colas: `messages`, `media`, `notifications`
- ✅ Shutdown graceful (SIGTERM, SIGINT)
- ✅ Auto-restart si excede memoria (128MB)
- ✅ Retry automático con backoff
- ✅ Logging detallado
### 4. **Webhook Optimizado** (`api/webhook_optimized.php`)
Nuevo webhook que **responde inmediatamente**:
```php
// Flujo optimizado:
1. Leer payload
2. Responder 200 OK inmediatamente (< 100ms)
3. Forzar envío con fastcgi_finish_request()
4. Procesar y encolar sin presión de tiempo
```
**Tiempos medidos:**
- ⚡ Respuesta al cliente: **50-100ms**
- ⏱️ Procesamiento total: **200-500ms**
- 📊 vs. webhook anterior: **5x más rápido**
### 5. **Rate Limiting** (`services/WhatsAppServiceWithRateLimit.php`)
Respeta límites de WhatsApp automáticamente:
- 🚦 **80 mensajes/segundo**
- 🚦 **1,000 mensajes/hora**
- 🚦 **10,000 mensajes/día**
```php
use WhatsAppServiceWithRateLimit;
$whatsapp = new WhatsAppServiceWithRateLimit();
// Envía si está dentro del límite, encola si no
$result = $whatsapp->sendTextMessage($phone, 'Hola');
if ($result['queued'] ?? false) {
// Mensaje encolado por rate limit
}
// Ver estadísticas
$stats = $whatsapp->getRateLimitStats();
/*
[
'per_second' => ['current' => 45, 'limit' => 80, 'remaining' => 35],
'per_hour' => ['current' => 320, 'limit' => 1000, 'remaining' => 680],
'per_day' => ['current' => 5200, 'limit' => 10000, 'remaining' => 4800]
]
*/
```
### 6. **Logging Estructurado** (`classes/LoggerFactory.php`)
```php
use LoggerFactory;
// Loggers por canal
$logger = LoggerFactory::webhook();
$logger->info("Webhook received", ['entries' => 2]);
// Helper global (compatible con código existente)
writeLog('INFO', 'Message processed', ['user_id' => 123], 'bot');
// Estadísticas
$stats = LoggerFactory::getStats();
// ['total_files' => 12, 'total_size_mb' => 45.2, 'by_channel' => [...]]
// Limpieza de logs antiguos (ejecutar con cron)
LoggerFactory::cleanup(30); // Elimina logs > 30 días
```
---
## Flujo de Mensajes
### 1. **Mensaje entrante de WhatsApp**
```
Usuario → WhatsApp API → webhook_optimized.php
↓ (< 100ms)
Responde 200 OK
Encola en Redis (queue:messages)
worker.php (BRPOP)
BotService.processMessage()
WhatsAppService.sendTextMessage()
↓ (con rate limit)
WhatsApp API → Usuario
```
### 2. **Envío de mensajes salientes**
```php
// Sin rate limit (envío inmediato)
WhatsAppService::sendTextMessage($phone, 'Hola');
// Con rate limit (puede encolar automáticamente)
WhatsAppServiceWithRateLimit::sendTextMessage($phone, 'Hola');
```
### 3. **Procesamiento de media**
```
Usuario envía imagen → webhook detecta media
Encola en queue:media
worker.php procesa
MediaService.fetchAndStoreFromGraph()
Descarga y guarda en /uploads/
Actualiza conversations.local_file
```
---
## Instalación y Configuración
### 1. **Instalar dependencias**
```bash
cd /ruta/a/tu/proyecto
composer install
```
### 2. **Configurar .env**
```bash
cp .env.example .env
nano .env
```
Agregar configuración de Redis:
```env
# Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Rate Limiting
ENABLE_RATE_LIMIT=true
# Logging
LOG_LEVEL=INFO
LOG_PATH=logs
```
### 3. **Instalar y configurar Redis**
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install redis-server -y
sudo systemctl enable redis-server
sudo systemctl start redis-server
# Verificar
redis-cli ping
# Respuesta: PONG
```
### 4. **Configurar Webhook en WhatsApp**
Actualizar URL del webhook a `webhook_optimized.php`:
```
https://tudominio.com/api/webhook_optimized.php
```
### 5. **Iniciar Workers**
#### Opción A: Manual (desarrollo)
```bash
php worker.php --daemon &
```
#### Opción B: Con Supervisor (producción)
```bash
# Instalar supervisor
sudo apt install supervisor -y
# Copiar configuración
sudo cp supervisor-whatsapp-worker.conf /etc/supervisor/conf.d/
# Actualizar rutas en el archivo
sudo nano /etc/supervisor/conf.d/supervisor-whatsapp-worker.conf
# Recargar supervisor
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start whatsapp-worker:*
# Ver estado
sudo supervisorctl status
```
---
## Deployment
### Checklist de producción
- [ ] Redis instalado y funcionando
- [ ] Composer dependencies instaladas
- [ ] .env configurado correctamente
- [ ] Workers iniciados con Supervisor
- [ ] Webhook actualizado en WhatsApp
- [ ] Logs con permisos correctos (755 en `/logs`)
- [ ] Cron configurado para logs cleanup
### Cron recomendado
```bash
# Editar crontab
crontab -e
# Agregar estas líneas:
# Procesar mensajes delayed cada minuto
* * * * * cd /ruta/proyecto && php -r "require 'vendor/autoload.php'; use WhatsApp\Queue\RedisQueue; \$q = new RedisQueue(); \$q->processDelayed('messages');"
# Limpiar logs antiguos cada día a las 3 AM
0 3 * * * cd /ruta/proyecto && php -r "require 'classes/LoggerFactory.php'; LoggerFactory::cleanup(30);"
# Monitorear workers (reiniciar si están caídos)
*/5 * * * * supervisorctl status whatsapp-worker:* | grep -q RUNNING || supervisorctl restart whatsapp-worker:*
```
---
## Monitoreo
### 1. **Ver estadísticas de colas**
```php
use WhatsApp\Queue\RedisQueue;
$queue = new RedisQueue();
$stats = $queue->getStats();
print_r($stats);
```
### 2. **Ver logs en tiempo real**
```bash
# Webhook
tail -f logs/webhook.log
# Worker
tail -f logs/worker.log
# Bot
tail -f logs/bot.log
# Todos los errores
tail -f logs/*.log | grep ERROR
```
### 3. **Verificar estado de workers**
```bash
sudo supervisorctl status whatsapp-worker:*
```
### 4. **Monitorear Redis**
```bash
# Conexión
redis-cli
# Ver todas las colas
KEYS whatsapp:queue:*
# Ver longitud de cola
LLEN whatsapp:queue:messages
# Ver rate limits
KEYS whatsapp:ratelimit:*
```
---
## Troubleshooting
### ❌ Worker no procesa mensajes
**Solución:**
```bash
# Verificar si worker está corriendo
ps aux | grep worker.php
# Ver logs
tail -f logs/worker.log
# Reiniciar workers
sudo supervisorctl restart whatsapp-worker:*
```
### ❌ Redis no conecta
**Solución:**
```bash
# Verificar Redis
sudo systemctl status redis-server
# Reiniciar
sudo systemctl restart redis-server
# Verificar puerto
netstat -an | grep 6379
```
### ❌ Rate limit excedido constantemente
**Solución:**
```php
// Ver estadísticas actuales
$whatsapp = new WhatsAppServiceWithRateLimit();
print_r($whatsapp->getRateLimitStats());
// Resetear límites (solo desarrollo)
$whatsapp->resetRateLimits();
// Aumentar número de workers
// Editar supervisor-whatsapp-worker.conf:
// numprocs=5 (en lugar de 3)
```
### ❌ Logs muy grandes
**Solución:**
```bash
# Limpiar manualmente
cd logs
find . -name "*.log*" -mtime +7 -delete
# O ejecutar cleanup:
php -r "require 'classes/LoggerFactory.php'; echo LoggerFactory::cleanup(7) . ' archivos eliminados';"
```
---
## Performance Benchmarks
### Antes vs. Ahora
| Métrica | Antes | Ahora | Mejora |
|---------|-------|-------|--------|
| Respuesta webhook | 3-8s | 50-150ms | **50x más rápido** |
| Mensajes/segundo | ~10 | 80 (límite API) | **8x más rápido** |
| Timeout en webhook | 5-10% | 0% | **100% confiable** |
| Memoria worker | N/A | 20-40 MB | **Eficiente** |
| Reintentos exitosos | Manual | Automático | **100% automatizado** |
---
## Próximos pasos
1.**Implementado:** Arquitectura con colas
2.**Implementado:** Rate limiting
3.**Implementado:** Logging estructurado
4. 🔜 **Recomendado:** Monitoreo con Prometheus/Grafana
5. 🔜 **Recomendado:** Alertas por email/Slack cuando worker cae
6. 🔜 **Recomendado:** Dashboard web para ver estadísticas en tiempo real
---
## Soporte
Para dudas o problemas:
1. Revisar logs en `/logs`
2. Ver [troubleshooting](#troubleshooting)
3. Consultar documentación de WhatsApp API
4. Verificar configuración de Redis y workers
---
**Última actualización:** Enero 2026
**Versión:** 2.0
-359
View File
@@ -1,359 +0,0 @@
# 🚀 Guía de Despliegue en Servidor
## 📋 Requisitos del Servidor
### Mínimos
- **PHP**: 8.0 o superior
- **MySQL/MariaDB**: 5.7 o superior
- **Servidor Web**: Apache/Nginx
- **SSL**: Certificado válido (HTTPS obligatorio para WhatsApp)
- **Memoria RAM**: Mínimo 512MB
- **Espacio**: 1GB libre
### Extensiones PHP Requeridas
```bash
php-pdo
php-pdo-mysql
php-curl
php-json
php-mbstring
php-openssl
```
## 🌍 Configuración por Tipo de Servidor
### 🔹 **VPS/Servidor Dedicado (Ubuntu/CentOS)**
#### 1. Instalar Dependencias
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install apache2 mysql-server php8.0 php8.0-mysql php8.0-curl php8.0-json php8.0-mbstring
# CentOS/RHEL
sudo yum install httpd mysql-server php php-mysql php-curl php-json php-mbstring
```
#### 2. Configurar Apache
```apache
# /etc/apache2/sites-available/whatsapp-bot.conf
<VirtualHost *:80>
ServerName tudominio.com
DocumentRoot /var/www/html/bot
<Directory /var/www/html/bot>
AllowOverride All
Require all granted
</Directory>
# Redirigir HTTP a HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
<VirtualHost *:443>
ServerName tudominio.com
DocumentRoot /var/www/html/migrador
SSLEngine on
SSLCertificateFile /path/to/your/certificate.crt
SSLCertificateKeyFile /path/to/your/private.key
<Directory /var/www/html/migrador>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```
#### 3. Subir Archivos
```bash
# Crear directorio
sudo mkdir -p /var/www/html/migrador
# Subir archivos (usando SCP/SFTP)
scp -r bot/* usuario@servidor:/var/www/html/bot/
# Configurar permisos
sudo chown -R www-data:www-data /var/www/html/bot
sudo chmod -R 755 /var/www/html/bot
```
### 🔹 **Hosting Compartido (cPanel/Plesk)**
#### 1. Subir Archivos
- Accede a tu **File Manager** en cPanel
- Ve a la carpeta `public_html`
- Crea carpeta `migrador` (o sube directamente a raíz)
- Sube todos los archivos del proyecto
#### 2. Configurar Base de Datos
- Ve a **MySQL Databases** en cPanel
- Crea nueva base de datos: `tuusuario_whatsapp`
- Crea usuario y asigna privilegios
- Anota: host, usuario, contraseña, base de datos
#### 3. Editar Configuración
```php
// config/config.php
define('DB_HOST', 'localhost'); // O tu host específico
define('DB_NAME', 'tuusuario_whatsapp');
define('DB_USER', 'tuusuario_dbuser');
define('DB_PASS', 'tu_password_db');
```
### 🔹 **Servicios en la Nube**
#### **AWS EC2**
```bash
# 1. Conectar por SSH
ssh -i tu-clave.pem ec2-user@tu-ip-publica
# 2. Instalar LAMP stack
sudo yum update -y
sudo amazon-linux-extras install -y lamp-mariadb10.2-php7.2 php7.2
# 3. Configurar y seguir pasos de VPS
```
#### **DigitalOcean Droplet**
```bash
# 1. Crear droplet con LAMP stack preinstalado
# 2. Conectar por SSH
# 3. Subir archivos a /var/www/html/
```
#### **Google Cloud Platform**
```bash
# 1. Crear VM con Ubuntu
# 2. Instalar LAMP stack
# 3. Configurar firewall para puertos 80/443
```
## 🔧 Configuración Específica del Sistema
### 1. Editar config/config.php
```php
<?php
// === CONFIGURACIÓN DE PRODUCCIÓN ===
// Base de datos de producción
define('DB_HOST', 'tu-servidor-mysql');
define('DB_NAME', 'whatsapp_production');
define('DB_USER', 'tu_usuario_db');
define('DB_PASS', 'password_super_seguro');
// URL de producción
define('APP_URL', 'https://tudominio.com/migrador');
// Deshabilitar errores en producción
error_reporting(0);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
// Configuración de seguridad
define('SECURE_MODE', true);
?>
```
### 2. Crear .htaccess (Apache)
```apache
# .htaccess en raíz del proyecto
RewriteEngine On
# Forzar HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Proteger archivos sensibles
<Files "config.php">
Require all denied
</Files>
<Files "*.sql">
Require all denied
</Files>
# Cache headers
<IfModule mod_expires.c>
ExpiresActive on
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/jpg "access plus 1 year"
</IfModule>
```
### 3. Configurar SSL (Let's Encrypt)
```bash
# Instalar Certbot
sudo apt install certbot python3-certbot-apache
# Obtener certificado
sudo certbot --apache -d tudominio.com
# Auto-renovación
sudo crontab -e
# Agregar: 0 12 * * * /usr/bin/certbot renew --quiet
```
## 🔗 Configuración de WhatsApp Webhook
### 1. URL del Webhook
```
https://tudominio.com/bot/api/webhook.php
```
### 2. Configurar en Facebook Developers
1. Ve a [developers.facebook.com](https://developers.facebook.com)
2. Selecciona tu app de WhatsApp Business
3. Ve a **WhatsApp > Configuration**
4. En **Webhooks**:
- **Callback URL**: `https://tudominio.com/migrador/api/webhook.php`
- **Verify Token**: `mi_token_secreto_123` (o cambia en config.php)
- **Webhook fields**: ✅ `conversations`
### 3. Verificar Webhook
```bash
# Probar webhook manualmente
curl -X GET "https://tudominio.com/migrador/api/webhook.php?hub.mode=subscribe&hub.challenge=CHALLENGE_ACCEPTED&hub.verify_token=mi_token_secreto_123"
```
## ⚙️ Configuración Post-Instalación
### 1. Ejecutar Instalador
```
https://tudominio.com/migrador/install.php
```
### 2. Verificar Sistema
```
https://tudominio.com/migrador/test.php
```
### 3. Configurar Panel
```
https://tudominio.com/migrador/index.php
```
## 🔒 Seguridad en Producción
### 1. Configuraciones PHP
```ini
# php.ini ajustes
expose_php = Off
display_errors = Off
log_errors = On
allow_url_fopen = Off
```
### 2. Permisos de Archivos
```bash
# Archivos: 644
sudo find /var/www/html/migrador -type f -exec chmod 644 {} \;
# Directorios: 755
sudo find /var/www/html/migrador -type d -exec chmod 755 {} \;
# Config solo lectura
sudo chmod 600 /var/www/html/migrador/config/config.php
```
### 3. Backup Automático
```bash
#!/bin/bash
# backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
mysqldump -u usuario -p password whatsapp_db > /backups/whatsapp_$DATE.sql
tar -czf /backups/files_$DATE.tar.gz /var/www/html/migrador
```
## 📊 Monitoreo y Logs
### 1. Logs de Apache
```bash
tail -f /var/log/apache2/error.log
tail -f /var/log/apache2/access.log
```
### 2. Logs de PHP
```bash
tail -f /var/log/php_errors.log
```
### 3. Logs del Sistema
```bash
# Ver logs desde el panel
https://tudominio.com/migrador/index.php#logs
```
## 🚀 Optimizaciones de Rendimiento
### 1. Cache de PHP (OPcache)
```ini
# php.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=1000
```
### 2. Compresión GZIP
```apache
# En .htaccess
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/javascript
</IfModule>
```
### 3. Optimización MySQL
```sql
-- my.cnf
[mysqld]
innodb_buffer_pool_size = 256M
query_cache_type = 1
query_cache_size = 64M
```
## 📞 URLs de Producción
Una vez configurado:
- **🏠 Panel**: https://tudominio.com/migrador/index.php
- **🔗 API**: https://tudominio.com/migrador/api/
- **📱 Webhook**: https://tudominio.com/migrador/api/webhook.php
- **📊 Stats**: https://tudominio.com/migrador/api/get_stats.php
## ⚠️ Troubleshooting
### Error 500
- Revisar logs de Apache/PHP
- Verificar permisos de archivos
- Comprobar configuración de base de datos
### Webhook no funciona
- Verificar SSL válido
- Comprobar URL pública
- Revisar token de verificación
### Base de datos no conecta
- Verificar credenciales
- Comprobar host/puerto
- Revisar permisos de usuario MySQL
## 📝 Checklist Final
- [ ] ✅ Servidor con PHP 8.0+ y MySQL
- [ ] ✅ SSL/HTTPS configurado
- [ ] ✅ Archivos subidos con permisos correctos
- [ ] ✅ Base de datos creada y configurada
- [ ] ✅ config.php editado con datos reales
- [ ] ✅ Instalador ejecutado sin errores
- [ ] ✅ Webhook configurado en Facebook
- [ ] ✅ Sistema probado y funcionando
- [ ] ✅ Backup automático configurado
¡Tu sistema WhatsApp Bot Manager estará listo para producción! 🎉
-245
View File
@@ -1,245 +0,0 @@
# Guía de Uso - Docker Compose Development
## 🚀 Inicio Rápido
### Levantar entorno de desarrollo
```bash
docker-compose -f docker-compose.dev.yml up -d
```
### Ver logs en tiempo real
```bash
docker-compose -f docker-compose.dev.yml logs -f
```
### Detener entorno
```bash
docker-compose -f docker-compose.dev.yml down
```
### Reconstruir contenedores
```bash
docker-compose -f docker-compose.dev.yml build --no-cache
docker-compose -f docker-compose.dev.yml up -d
```
---
## 🛠️ Herramientas Disponibles
| Servicio | URL | Descripción |
|----------|-----|-------------|
| **Aplicación** | http://localhost:8080 | WhatsApp Bot principal |
| **XDebug** | Puerto 9003 | Debugger para IDE (VSCode, PHPStorm) |
| **Redis Commander** | http://localhost:8082 | Interface web para Redis |
| **MailHog** | http://localhost:8025 | Captura de emails de prueba |
| **Adminer** | http://localhost:8083 | Gestor de base de datos |
---
## 🔧 Características de Desarrollo
### ✅ Hot Reload Activado
Los cambios en archivos PHP/JS/CSS se reflejan **inmediatamente** sin reiniciar:
- ✅ Volumen mount: `./:/var/www/html`
- ✅ OPcache desactivado
- ✅ No requiere rebuild
### 🐛 Debugging con XDebug
#### VSCode Configuration (`.vscode/launch.json`)
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www/html": "${workspaceFolder}"
}
}
]
}
```
#### PHPStorm Configuration
1. Settings → PHP → Debug → XDebug
2. Port: `9003`
3. Path mappings: `/var/www/html``/Users/lizandro/Documents/GitHub/whatsapp`
### 📊 Logs en Tiempo Real
```bash
# Ver logs de todos los servicios
docker-compose -f docker-compose.dev.yml logs -f
# Ver logs de un servicio específico
docker-compose -f docker-compose.dev.yml logs -f app
# Ver logs PHP-FPM
docker-compose -f docker-compose.dev.yml exec app tail -f /var/www/html/logs/php-fpm-error.log
# Ver logs de XDebug
docker-compose -f docker-compose.dev.yml exec app tail -f /var/www/html/logs/xdebug.log
```
---
## 📧 Testing de Emails con MailHog
MailHog captura todos los emails enviados desde la aplicación:
1. Configura PHP para usar MailHog:
```php
ini_set('SMTP', 'mailhog');
ini_set('smtp_port', 1025);
```
2. Abre http://localhost:8025 para ver emails capturados
---
## 🔍 Inspeccionar Redis
### Opción 1: Redis Commander (Web UI)
- URL: http://localhost:8082
- Ver claves, valores, TTL
- Ejecutar comandos Redis
### Opción 2: Redis CLI
```bash
docker-compose -f docker-compose.dev.yml exec redis redis-cli
# Comandos útiles:
KEYS * # Ver todas las claves
GET clave # Ver valor de una clave
TTL clave # Ver tiempo de vida
MONITOR # Ver comandos en tiempo real
```
---
## 🗄️ Acceso a Base de Datos
### Adminer (http://localhost:8083)
- **Server**: Tu DB_HOST del .env
- **Username**: Tu DB_USER del .env
- **Password**: Tu DB_PASS del .env
- **Database**: Tu DB_NAME del .env
### MySQL CLI desde contenedor
```bash
docker-compose -f docker-compose.dev.yml exec app mysql -h ${DB_HOST} -u ${DB_USER} -p${DB_PASS} ${DB_NAME}
```
---
## 🧪 Comandos Útiles
### Ejecutar comandos en el contenedor
```bash
# Bash interactivo
docker-compose -f docker-compose.dev.yml exec app bash
# PHP CLI
docker-compose -f docker-compose.dev.yml exec app php -v
# Composer
docker-compose -f docker-compose.dev.yml exec app composer install
# Verificar configuración PHP
docker-compose -f docker-compose.dev.yml exec app php -i | grep xdebug
```
### Reiniciar servicios
```bash
# Reiniciar un servicio específico
docker-compose -f docker-compose.dev.yml restart app
# Reiniciar todos
docker-compose -f docker-compose.dev.yml restart
```
### Limpiar todo (cuidado!)
```bash
# Detener y eliminar contenedores + volúmenes
docker-compose -f docker-compose.dev.yml down -v
# Limpiar imágenes no utilizadas
docker system prune -a
```
---
## 🚨 Troubleshooting
### Problema: Cambios no se reflejan
```bash
# 1. Verificar que el volumen esté montado
docker-compose -f docker-compose.dev.yml exec app ls -la /var/www/html
# 2. Verificar que OPcache esté desactivado
docker-compose -f docker-compose.dev.yml exec app php -i | grep opcache.enable
# 3. Limpiar caché del navegador (Cmd+Shift+R)
```
### Problema: XDebug no funciona
```bash
# Verificar que XDebug esté instalado
docker-compose -f docker-compose.dev.yml exec app php -m | grep xdebug
# Ver configuración de XDebug
docker-compose -f docker-compose.dev.yml exec app php -i | grep xdebug
# Ver logs de XDebug
docker-compose -f docker-compose.dev.yml exec app tail -f /var/www/html/logs/xdebug.log
```
### Problema: Redis no conecta
```bash
# Verificar que Redis esté corriendo
docker-compose -f docker-compose.dev.yml ps
# Test de conexión
docker-compose -f docker-compose.dev.yml exec app php -r "try { \$r = new Redis(); \$r->connect('redis', 6379); echo 'OK'; } catch (Exception \$e) { echo \$e->getMessage(); }"
```
---
## ⚠️ Diferencias con Producción
| Característica | Desarrollo | Producción |
|----------------|-----------|------------|
| OPcache | ❌ Desactivado | ✅ Activado |
| Error Display | ✅ Activado | ❌ Desactivado |
| XDebug | ✅ Activado | ❌ Desactivado |
| Log Level | DEBUG | INFO/WARNING |
| Workers | 1 | 2+ |
| Rate Limiting | ❌ Desactivado | ✅ Activado |
---
## 🔄 Migrar de Dev a Producción
```bash
# 1. Detener entorno de desarrollo
docker-compose -f docker-compose.dev.yml down
# 2. Levantar entorno de producción
docker-compose up -d
# 3. Verificar
docker-compose ps
```
---
## 📝 Notas
- **No commitear** `docker-compose.dev.yml` en producción
- Usar `.env` para configuración sensible
- Los volúmenes de desarrollo son locales (no se comparten)
- MailHog solo atrapa emails locales, no los envía realmente
-462
View File
@@ -1,462 +0,0 @@
# 🐳 WhatsApp Bot Manager - Docker
## Guía Rápida de Instalación
Esta es la forma **más fácil y rápida** de desplegar el WhatsApp Bot Manager con todas sus funcionalidades.
---
## 📦 ¿Qué incluye?
La arquitectura Docker incluye:
-**PHP 8.2 + Nginx** (aplicación web)
-**MySQL 8.0** (base de datos)
-**Redis 7** (colas y caché)
-**3 Workers** automáticos (procesamiento asíncrono)
-**Supervisor** (gestor de procesos)
-**PHPMyAdmin** (opcional, para desarrollo)
-**Redis Commander** (opcional, para debugging)
-**Health checks** automáticos
-**Logs centralizados**
-**Auto-restart** en caso de fallos
---
## 🚀 Instalación en 3 Pasos
### 1. Clonar y configurar
```bash
cd /ruta/a/tu/proyecto
# Copiar variables de entorno
cp .env.docker .env
# Editar .env con tus credenciales de WhatsApp
nano .env
```
**Mínimo requerido en `.env`:**
```env
# WhatsApp API
WHATSAPP_TOKEN=tu_token_aqui
WHATSAPP_PHONE_NUMBER_ID=tu_phone_number_id
WEBHOOK_VERIFY_TOKEN=tu_token_verificacion
# Base de datos (puedes dejar estos valores)
DB_PASS=password_seguro
DB_ROOT_PASSWORD=root_password_seguro
```
### 2. Construir contenedores
```bash
docker-compose build
```
### 3. Iniciar servicios
```bash
docker-compose up -d
```
**¡Listo!** 🎉 Tu bot está corriendo en: `http://localhost:8080`
---
## 📋 Comandos Útiles
### Ver logs en tiempo real
```bash
# Todos los servicios
docker-compose logs -f
# Solo la aplicación
docker-compose logs -f app
# Solo workers
docker-compose logs -f worker
# Solo webhook
docker-compose logs -f app | grep webhook
```
### Ver estado de servicios
```bash
docker-compose ps
```
### Reiniciar servicios
```bash
# Reiniciar todo
docker-compose restart
# Reiniciar solo app
docker-compose restart app
# Reiniciar workers
docker-compose restart worker
```
### Ejecutar comandos dentro del contenedor
```bash
# Shell interactivo
docker-compose exec app sh
# Ejecutar script PHP
docker-compose exec app php test_architecture.php
# Ver cola de Redis
docker-compose exec redis redis-cli KEYS "whatsapp:queue:*"
# Ver estadísticas de workers
docker-compose exec app supervisorctl status
```
### Detener y eliminar todo
```bash
# Detener servicios
docker-compose down
# Detener y eliminar volúmenes (⚠️ borra datos)
docker-compose down -v
```
---
## 🔧 Configuración Avanzada
### Cambiar puerto de la aplicación
```bash
# En .env
APP_PORT=3000
# Reiniciar
docker-compose up -d
```
### Aumentar número de workers
```bash
# En .env
WORKER_REPLICAS=5
# En docker-compose.yml, en la sección app > environment
# Ajustar numprocs en supervisor/programs.conf
# Rebuild
docker-compose up -d --build
```
### Habilitar herramientas de desarrollo
```bash
# PHPMyAdmin en http://localhost:8081
# Redis Commander en http://localhost:8082
docker-compose --profile dev up -d
```
---
## 🏗️ Estructura de Contenedores
```
┌─────────────────────────────────────────┐
│ app (PHP + Nginx + Workers) │
│ - Puerto: 8080 → 80 │
│ - Supervisor: Nginx + PHP-FPM + 3 Workers│
│ - Health check: /health.php │
└─────────┬───────────────────────────────┘
├─────────┐
│ │
┌─────────▼──┐ ┌───▼────────┐
│ db │ │ redis │
│ MySQL 8.0 │ │ Redis 7 │
│ Puerto:3306│ │ Puerto:6379│
└────────────┘ └────────────┘
┌─────────────────────┐
│ worker (opcional) │
│ 2 workers extra │
└─────────────────────┘
┌──────────────────────────┐
│ phpmyadmin (opcional) │
│ Puerto: 8081 │
└──────────────────────────┘
┌──────────────────────────┐
│ redis-commander (opcional)│
│ Puerto: 8082 │
└──────────────────────────┘
```
---
## 📊 Monitoreo
### Health Check
```bash
curl http://localhost:8080/health.php
```
Respuesta:
```json
{
"status": "healthy",
"timestamp": "2026-01-27 10:30:00",
"checks": {
"php": {"status": "ok", "version": "8.2.15"},
"database": {"status": "ok", "host": "db"},
"redis": {"status": "ok", "host": "redis"},
"logs": {"status": "ok", "writable": true},
"uploads": {"status": "ok", "writable": true},
"queue": {
"status": "ok",
"stats": {
"messages": {"pending": 0, "delayed": 0, "failed": 0},
"media": {"pending": 0, "delayed": 0, "failed": 0}
}
}
}
}
```
### Estado de Workers
```bash
docker-compose exec app supervisorctl status
```
### Estadísticas de Redis
```bash
docker-compose exec redis redis-cli INFO stats
```
### Uso de recursos
```bash
docker stats
```
---
## 🔐 Actualizar Webhook en WhatsApp
Una vez desplegado, actualiza la URL del webhook en WhatsApp Business Dashboard:
```
https://tudominio.com/api/webhook_optimized.php
```
Para desarrollo local con ngrok:
```bash
ngrok http 8080
# Usar la URL generada:
# https://xxxxx.ngrok.io/api/webhook_optimized.php
```
---
## 🐛 Troubleshooting
### ❌ Puerto 8080 ya está en uso
```bash
# Cambiar puerto en .env
APP_PORT=3000
# O detener servicio que usa 8080
sudo lsof -ti:8080 | xargs kill -9
```
### ❌ Error de permisos en logs/uploads
```bash
docker-compose exec app chown -R www:www /var/www/html/logs
docker-compose exec app chown -R www:www /var/www/html/uploads
docker-compose exec app chmod -R 755 /var/www/html/logs
docker-compose exec app chmod -R 755 /var/www/html/uploads
```
### ❌ Workers no procesan mensajes
```bash
# Ver logs de workers
docker-compose logs worker
# Reiniciar workers
docker-compose exec app supervisorctl restart whatsapp-worker:*
# Ver estado
docker-compose exec app supervisorctl status
```
### ❌ Redis no conecta
```bash
# Verificar que Redis está corriendo
docker-compose ps redis
# Ver logs
docker-compose logs redis
# Reiniciar
docker-compose restart redis
```
### ❌ MySQL no inicia
```bash
# Ver logs
docker-compose logs db
# Eliminar volumen y recrear (⚠️ borra datos)
docker-compose down -v
docker-compose up -d
```
### ❌ "Connection refused" al webhook
```bash
# Verificar que Nginx está corriendo
docker-compose exec app ps aux | grep nginx
# Reiniciar Nginx
docker-compose exec app supervisorctl restart nginx
# Ver logs de Nginx
docker-compose exec app tail -f /var/log/nginx/error.log
```
---
## 🚢 Deployment en Producción
### 1. Servidor con Docker
```bash
# Clonar repositorio
git clone https://github.com/tu-repo/whatsapp-bot.git
cd whatsapp-bot
# Configurar .env
cp .env.docker .env
nano .env
# Construir y ejecutar
docker-compose up -d --build
# Ver logs
docker-compose logs -f
```
### 2. Con Docker Swarm (alta disponibilidad)
```bash
# Inicializar swarm
docker swarm init
# Deploy
docker stack deploy -c docker-compose.yml whatsapp
# Ver servicios
docker service ls
# Escalar workers
docker service scale whatsapp_worker=5
```
### 3. Con Kubernetes (opcional)
Archivos de configuración disponibles en `/k8s` (crear según necesidad).
---
## 📈 Performance
### Configuración optimizada para producción
La configuración Docker incluye:
-**OPcache** activado (PHP)
-**FastCGI cache** en Nginx
-**Redis** para sesiones PHP
-**Connection pooling** MySQL
-**Gzip** compresión
-**pm.dynamic** para PHP-FPM (auto-scaling)
### Benchmarks
| Métrica | Valor |
|---------|-------|
| Respuesta webhook | 50-100ms |
| Throughput | 80 msg/s |
| Memoria por worker | 30-50 MB |
| CPU por worker | 5-10% |
| Uptime | 99.9% |
---
## 🔄 Actualización
```bash
# Pull últimos cambios
git pull
# Rebuild contenedores
docker-compose build --no-cache
# Reiniciar con nuevas imágenes
docker-compose up -d
# Ver logs para verificar
docker-compose logs -f
```
---
## 🎯 Próximos Pasos
1. ✅ Configurar SSL/HTTPS (usar reverse proxy como Traefik o Caddy)
2. ✅ Configurar backups automáticos de MySQL
3. ✅ Implementar monitoreo con Prometheus + Grafana
4. ✅ Configurar alertas (email/Slack) para fallos críticos
5. ✅ Implementar CI/CD con GitHub Actions
---
## 📚 Documentación Adicional
- [Arquitectura Completa](ARQUITECTURA_MEJORADA.md)
- [Documentación Original](README.md)
- [WhatsApp API Docs](https://developers.facebook.com/docs/whatsapp)
---
## 🆘 Soporte
Si encuentras problemas:
1. Revisar logs: `docker-compose logs -f`
2. Verificar health: `curl http://localhost:8080/health.php`
3. Ejecutar tests: `docker-compose exec app php test_architecture.php`
4. Consultar troubleshooting arriba
---
**¡Todo listo para producción!** 🚀
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
# Dockerfile para contenedor de mantenimiento
FROM alpine:3.19
# Instalar herramientas necesarias incluyendo PHP
RUN apk add --no-cache \
bash \
php82 \
php82-pdo \
php82-pdo_mysql \
php82-mysqli \
php82-curl \
php82-json \
php82-mbstring \
php82-openssl \
php82-redis \
mariadb-client \
curl \
tzdata \
dcron \
logrotate \
gzip && \
ln -sf /usr/bin/php82 /usr/bin/php
# Configurar zona horaria
ENV TZ=America/Bogota
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Crear directorios de trabajo
RUN mkdir -p /backups /scripts /var/log/cron
# Copiar scripts de mantenimiento
COPY docker/maintenance/scripts/ /scripts/
RUN chmod +x /scripts/*.sh
# Configurar crontab
COPY docker/maintenance/crontab /etc/crontabs/root
# Script de inicio
COPY docker/maintenance/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
WORKDIR /scripts
ENTRYPOINT ["/entrypoint.sh"]
-146
View File
@@ -1,146 +0,0 @@
# 🚨 EMERGENCIA: "Table doesn't exist" - SOLUCIÓN INMEDIATA
## ⚠️ Tu Error Específico
```
❌ Table 'usite_whatsapp_bot.menus' doesn't exist
❌ Table 'usite_whatsapp_bot.users' doesn't exist
⚠️ 0 tablas, 0 elementos
```
**CAUSA:** Las tablas NO se están creando. Todos los instaladores anteriores fallan en tu hosting.
---
## 🚀 SOLUCIÓN DEFINITIVA
### 🎯 **OPCIÓN 1: Instalador Ultra Básico (GARANTIZADO)**
**👉 [install_ultra_basic.php](install_ultra_basic.php)**
**¿Por qué funcionará?**
- ✅ Crea SOLO 2 tablas esenciales: `users` y `system_config`
- ✅ Verifica CADA tabla después de crearla
- ✅ NO intenta insertar datos si las tablas fallan
- ✅ Proceso paso a paso con verificación completa
### 🛠️ **OPCIÓN 2: Crear Tablas Manualmente**
Si incluso el ultra básico falla, hazlo manual:
1. **Ve a phpMyAdmin en tu HestiaCP**
2. **Selecciona tu BD:** `usite_whatsapp_bot`
3. **Ejecuta este SQL:**
```sql
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
phone_number VARCHAR(20) UNIQUE NOT NULL,
name VARCHAR(100),
status ENUM('active', 'inactive') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS system_config (
id INT AUTO_INCREMENT PRIMARY KEY,
config_key VARCHAR(100) UNIQUE NOT NULL,
config_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO system_config (config_key, config_value) VALUES
('app_installed', '1'),
('install_date', NOW()),
('business_name', 'Mi Empresa');
```
4. **Luego actualiza config/config.php manualmente:**
```php
define('DB_HOST', 'localhost');
define('DB_NAME', 'usite_whatsapp_bot');
define('DB_USER', 'tu_usuario');
define('DB_PASS', 'tu_contraseña');
define('ADMIN_PASSWORD', password_hash('admin123', PASSWORD_DEFAULT));
```
5. **Crear archivo:** `.installation_completed` (contenido cualquiera)
---
## 📋 Diferencias de los Instaladores
| Instalador | Tablas que crea | Tu resultado anterior |
|------------|------------------|----------------------|
| `install.php` | Intenta crear BD | ❌ No tienes privilegios |
| `install_manual.php` | 7+ tablas complejas | ❌ Falla en parsing |
| `install_integrated.php` | 7+ tablas, schema interno | ❌ Aún falla creación |
| `install_simple.php` | 3 tablas básicas | ❌ Probablemente falla |
| **`install_ultra_basic.php`** | **SOLO 2 tablas** | ✅ **DEBERÍA FUNCIONAR** |
---
## 🔍 ¿Por qué fallan los otros?
### Problemas identificados en tu hosting:
1. **Permisos restrictivos:** Puede que no tengas CREATE TABLE completo
2. **Claves foráneas:** Tu MySQL puede rechazar FOREIGN KEY
3. **Tipos de datos:** Algunos ENUM o JSON no soportados
4. **Transacciones:** Problemas con múltiples operaciones
### El Ultra Básico evita esto:
- ✅ Solo 2 tablas simples, sin dependencias
- ✅ Sin claves foráneas complejas
- ✅ Sin tipos de datos exóticos
- ✅ Verifica cada paso antes de continuar
---
## 🎯 PASOS EXACTOS PARA TI
### 1. Ir al Instalador Ultra Básico
```
URL: tudominio.com/install_ultra_basic.php
```
### 2. Usar las mismas credenciales
- **Host:** localhost
- **BD:** usite_whatsapp_bot
- **Usuario:** [el que creaste en HestiaCP]
- **Contraseña:** [la que configuraste]
### 3. Si funciona verás:
```
✅ Conectado a 'usite_whatsapp_bot'
✅ Tabla 'users' creada y funcional
✅ Tabla 'system_config' creada y funcional
✅ Configuración inicial insertada
✅ config.php actualizado
✅ Verificación final: 0 usuarios, 4 configs
🎉 Instalación básica completada
```
### 4. Después podrás:
- Entrar al panel con: `admin` / `[contraseña generada]`
- Probar con: `test.php`
- Agregar funciones desde el panel
---
## 📞 Si AÚN No Funciona
### Información para soporte:
1. **Resultado exacto del ultra básico**
2. **Versión PHP de tu hosting**
3. **Versión MySQL de tu hosting**
4. **Permisos exactos del usuario de BD**
### Alternativa extrema:
Si ningún instalador funciona, el problema es de permisos de tu hosting. Contacta soporte de HestiaCP para revisar permisos de CREATE TABLE.
---
## 🎉 Una vez que funcione:
**Tendrás:** Sistema básico funcional
**Podrás:** Acceder al panel de admin
**Siguiente:** Configurar WhatsApp desde el panel
**Expandir:** Agregar más funciones gradualmente
**👉 [IR AL INSTALADOR ULTRA BÁSICO AHORA](install_ultra_basic.php)** 🚀
-57
View File
@@ -1,57 +0,0 @@
## ✅ Problema del Error 500 Corregido
### 🐛 Causa del Error:
Al pegar una imagen desde el portapapeles y enviarla, las peticiones `fetch` a los endpoints de API **NO estaban enviando las cookies de sesión**, causando que:
1. `upload_media.php` rechazara la petición → Error 401
2. Nginx devolviera error 500 por fallo en PHP
### 🔧 Solución Aplicada:
Agregué `credentials: 'same-origin'` a ambos fetch en `sendMediaMessage()`:
```javascript
// 1. Upload del archivo
const uploadResponse = await fetch('api/upload_media.php', {
method: 'POST',
credentials: 'same-origin', // ✅ Enviar cookies de sesión
body: formData
});
// 2. Envío del mensaje
const sendResponse = await fetch('api/send_media_message.php', {
method: 'POST',
credentials: 'same-origin', // ✅ Enviar cookies de sesión
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({...})
});
```
### 📋 Cómo Funciona Ahora:
1. **Copias una imagen** (Ctrl+C o Cmd+C)
2. **Haces click en el input de mensaje**
3. **Pegas** (Ctrl+V o Cmd+V)
4. ✅ Aparece el preview de la imagen
5. ✅ Haces click en "Enviar"
6. ✅ Se sube el archivo con autenticación correcta
7. ✅ Se envía por WhatsApp
8. ✅ Aparece en la conversación
### 🧪 Probar Ahora:
1. Recarga la página: http://localhost:8080/conversations.php
2. Abre una conversación
3. Copia una imagen desde cualquier lugar
4. Haz click en el input de texto
5. Presiona Ctrl+V (o Cmd+V en Mac)
6. Debe aparecer el preview sin errores
7. Haz click en enviar
8. Debe enviarse correctamente
### 📝 Nota Técnica:
Por defecto, `fetch()` **NO envía cookies** a menos que especifiques:
- `credentials: 'same-origin'` → Envía cookies al mismo dominio
- `credentials: 'include'` → Envía cookies incluso en CORS
El endpoint `upload_media.php` usa `requireAuthentication()` que verifica la sesión en cookies, por eso fallaba antes.
-167
View File
@@ -1,167 +0,0 @@
# Guía para Resolver Error de Plantillas WhatsApp
## Error: "Template name does not exist in the translation" (#132001)
### ¿Qué significa este error?
Este error indica que WhatsApp no encuentra la plantilla especificada, o hay un problema con su configuración.
### Causas Comunes:
1. La plantilla no existe en WhatsApp Business Manager
2. La plantilla no está aprobada por Meta
3. El idioma especificado no coincide con el de la plantilla
4. El nombre de la plantilla está mal escrito
## Pasos para Resolverlo
### 1. Verificar en WhatsApp Business Manager
1. Ve a [business.facebook.com](https://business.facebook.com)
2. Selecciona tu cuenta de negocio
3. Ve a **WhatsApp Manager****Plantillas de mensaje**
4. Verifica que existe la plantilla `jaspers_market_plain_text_v1`
5. Confirma que el estado es **"Aprobada"** (no "Pendiente de revisión")
### 2. Verificar el Idioma
- Si la plantilla está en inglés, usa `language: "en"`
- Si está en español, usa `language: "es"`
- El idioma debe coincidir EXACTAMENTE
### 3. Verificar el Nombre
- Usa el nombre EXACTO de la plantilla
- No uses espacios ni caracteres especiales si no están en el nombre original
- Verifica mayúsculas y minúsculas
### 4. Usar el Diagnóstico del Sistema
1. Ve a la pestaña **Configuración**
2. Haz clic en **"Ejecutar"** en el panel de Diagnóstico
3. Revisa el estado de tus plantillas
## Ejemplos de Envío Correcto
### Formato Simple:
```json
{
"phone": "+1234567890",
"type": "template",
"template_name": "jaspers_market_plain_text_v1",
"language": "en",
"parameters": ["Juan", "123456"]
}
```
### Formato WhatsApp Business API:
```json
{
"messaging_product": "whatsapp",
"to": "+1234567890",
"type": "template",
"template": {
"name": "jaspers_market_plain_text_v1",
"language": {
"code": "en"
},
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Juan"
},
{
"type": "text",
"text": "123456"
}
]
}
]
}
}
```
## Crear una Nueva Plantilla
### En WhatsApp Business Manager:
1. Ve a **Plantillas de mensaje****Crear plantilla**
2. Escoge la categoría apropiada (UTILITY, MARKETING, AUTHENTICATION)
3. Escribe el contenido usando variables como `{{1}}`, `{{2}}`, etc.
4. Envía para aprobación
5. Espera la aprobación de Meta (puede tomar 24-48 horas)
### Ejemplo de Plantilla Simple:
```
Nombre: mi_plantilla_bienvenida
Idioma: Español
Categoría: UTILITY
Contenido: Hola {{1}}, bienvenido a nuestro servicio. Tu código es: {{2}}
```
### Registrarla en el Sistema:
1. Ve a la pestaña **Plantillas**
2. Haz clic en **Nueva Plantilla**
3. Completa los datos:
- Nombre: `mi_plantilla_bienvenida`
- Plantilla WhatsApp: `mi_plantilla_bienvenida` (igual al nombre)
- Idioma: `es`
- Categoría: `UTILITY`
- Estado: `approved` (solo después de que Meta la apruebe)
## Herramientas de Debug
### 1. Verificar Plantillas Disponibles
```bash
curl -X GET "http://tu-dominio.com/api/check_templates.php"
```
### 2. Ejecutar Diagnóstico Completo
```bash
curl -X GET "http://tu-dominio.com/api/diagnose_whatsapp.php?debug=1"
```
### 3. Probar Envío con Debug
```bash
curl -X POST "http://tu-dominio.com/api/send_message.php?debug=1" \
-H "Content-Type: application/json" \
-d '{
"phone": "+1234567890",
"type": "template",
"template_name": "mi_plantilla_test",
"language": "es",
"parameters": ["Pedro", "ABC123"]
}'
```
## Estados de Plantillas
| Estado | Descripción | ¿Se puede usar? |
|--------|-------------|-----------------|
| `pending` | En revisión por Meta | ❌ No |
| `approved` | Aprobada por Meta | ✅ Sí |
| `rejected` | Rechazada por Meta | ❌ No |
| `disabled` | Deshabilitada | ❌ No |
## Resolución por Pasos
### ✅ Lista de Verificación:
- [ ] La plantilla existe en WhatsApp Business Manager
- [ ] La plantilla está aprobada (estado: "Aprobada")
- [ ] El nombre de la plantilla es exacto
- [ ] El idioma coincide con la configuración en Meta
- [ ] Los parámetros coinciden con las variables de la plantilla
- [ ] La configuración de WhatsApp Business API es correcta
### 🔧 Si Sigues Teniendo Problemas:
1. Revisa el Access Token (puede haber expirado)
2. Confirma los permisos de la aplicación de Facebook
3. Verifica que el Phone Number ID es correcto
4. Asegúrate de que el número de WhatsApp Business está verificado
## Contacto y Soporte
Si después de seguir estos pasos sigues teniendo problemas:
1. Usa el diagnóstico del sistema para obtener un reporte detallado
2. Verifica los logs en la pestaña "Logs"
3. Comprueba la documentación oficial de WhatsApp Business API
4. Considera crear una plantilla de prueba simple para verificar conectividad
---
*Guía actualizada: Enero 2026*
-158
View File
@@ -1,158 +0,0 @@
# 🛠️ Guía de Instalación en HestiaCP
**Desarrollado por U-Site.app** - [https://u-site.app](https://u-site.app)
## ⚠️ Problema Común en Hostings Compartidos
El instalador automático (`install.php`) **NO FUNCIONA** en hostings compartidos como HestiaCP, cPanel, etc., porque requiere privilegios de administrador para crear bases de datos y usuarios.
## ✅ Solución: Instalación Manual
Usa el archivo `install_manual.php` que he creado específicamente para hostings compartidos.
---
## 📋 Pasos en HestiaCP
### 1. Crear Base de Datos
1. Entra al panel de HestiaCP
2. Ve a **"Databases"** o **"Base de datos"**
3. Haz clic en **"Add Database"**
4. Configura:
- **Database Name:** `whatsapp_bot` (o el nombre que prefieras)
- **Database User:** Crea un usuario (ej: `whatsapp_user`)
- **Password:** Usa una contraseña segura
- **Privileges:** Selecciona **ALL** o todos los permisos
### 2. Anotar Credenciales
Guarda esta información que necesitarás:
```
Host: localhost
Puerto: 3306 (generalmente)
Base de datos: [el nombre que creaste]
Usuario: [el usuario que creaste]
Contraseña: [la contraseña que configuraste]
```
### 3. Ejecutar Instalación Manual
1. Abre tu navegador web
2. Ve a: `https://tudominio.com/ruta-del-bot/install_manual.php`
3. Completa el formulario con las credenciales del paso anterior
4. Haz clic en **"Configurar Sistema"**
---
## 🔧 Características del Instalador Manual
### ✅ Lo que SÍ hace:
- ✅ Conecta con tu BD existente
- ✅ Crea todas las tablas necesarias
- ✅ Inserta datos de configuración inicial
- ✅ Genera contraseña de admin aleatoria
- ✅ Configura el archivo `config.php`
- ✅ Detecta automáticamente la URL del servidor
- ✅ Protege contra reinstalaciones accidentales
- ✅ Crea archivo de credenciales
### ❌ Lo que NO necesita:
- ❌ Privilegios de root/administrador
- ❌ Crear la base de datos (ya la creaste)
- ❌ Crear el usuario (ya lo creaste)
- ❌ Configuración especial del servidor
---
## 📁 Estructura Típica en HestiaCP
```
/public_html/
├── tu-bot/
│ ├── install_manual.php ← Usa este archivo
│ ├── install.php ← NO uses este (no funciona)
│ ├── config/
│ ├── api/
│ └── ...resto de archivos
```
---
## 🚨 Errores Comunes y Soluciones
### Error: "se queda cargando"
**Problema:** El instalador automático intenta crear BD con privilegios que no tienes.
**Solución:** Usa `install_manual.php` en lugar de `install.php`
### Error: "Access denied for user"
**Problema:** Credenciales incorrectas o permisos insuficientes.
**Solución:**
- Verifica que el usuario tenga ALL PRIVILEGES en la BD
- Confirma host, usuario y contraseña
- Asegúrate de que la BD existe
### Error: "Connection refused"
**Problema:** Host o puerto incorrectos.
**Solución:**
- Usa `localhost` como host
- Confirma que el puerto sea 3306
- Verifica que MySQL esté corriendo
### Error: "Database doesn't exist"
**Problema:** Nombre de BD incorrecto o no existe.
**Solución:**
- Verifica el nombre exacto en el panel de HestiaCP
- Asegúrate de que la BD fue creada correctamente
---
## 🎯 Después de la Instalación
1. **Guarda las credenciales:** Se generará un archivo `CREDENCIALES_SISTEMA.txt`
2. **Prueba el sistema:** Ve a `test.php` para verificar
3. **Configura WhatsApp:** Ve a `server_setup.php`
4. **Accede al panel:** Ve a `index.php` con usuario `admin`
---
## 📞 Soporte Técnico
- **Email:** support@u-site.app
- **Web:** https://u-site.app/support
- **Documentación:** Este archivo y los comentarios en el código
---
## 🔐 Credenciales por Defecto
### Base de Datos
- **Host:** localhost
- **Usuario/Contraseña:** Los que TÚ configuraste en HestiaCP
### Panel Admin
- **Usuario:** admin
- **Contraseña:** Se genera automáticamente (se muestra durante instalación)
---
## ⚡ Comandos Útiles para Debugging
### Verificar conexión desde terminal:
```bash
mysql -h localhost -u tu_usuario -p tu_basedatos
```
### Ver tablas después de instalar:
```sql
USE tu_basedatos;
SHOW TABLES;
```
### Ver configuración de usuario:
```sql
SELECT User, Host FROM mysql.user WHERE User='tu_usuario';
SHOW GRANTS FOR 'tu_usuario'@'localhost';
```
---
**¡Listo! Con esta guía deberías poder instalar sin problemas en HestiaCP.** 🚀
-214
View File
@@ -1,214 +0,0 @@
# 🚀 Instalación en HestiaCP - WhatsApp Bot Manager
## 📦 Archivo para HestiaCP
- **Archivo:** `whatsapp_HESTIACP_READY_20260104_104546.zip`
- **Optimizado para:** HestiaCP + hosting compartido/VPS
- **Tamaño:** ~168KB
## 🔧 .htaccess Reparado para HestiaCP
### ✅ Mejoras Implementadas:
- **Compatibilidad HestiaCP:** Sintaxis moderna (Require all denied)
- **Seguridad avanzada:** Protección contra inyecciones SQL/XSS
- **Rendimiento:** Compresión GZIP y cache optimizado
- **APIs:** Headers específicos para WhatsApp webhook
- **CSP:** Content Security Policy configurado
### 🛡️ Protecciones de Seguridad:
- ✅ Archivos de configuración protegidos
- ✅ Archivos .env bloqueados
- ✅ Logs inaccesibles desde web
- ✅ Tests bloqueados en producción
- ✅ Headers de seguridad completos
## 📋 Instalación en HestiaCP
### 1. Acceso al Panel
```bash
1. Acceder a HestiaCP: https://tu-servidor:8083
2. Login con credenciales de admin
3. Ir a "Web" → Seleccionar dominio
```
### 2. Subir Archivos
```bash
1. File Manager → public_html/
2. Subir whatsapp_HESTIACP_READY_20260104_104546.zip
3. Extraer ZIP en public_html/
4. Borrar archivo ZIP después
```
### 3. Configurar Permisos en HestiaCP
```bash
1. En File Manager, seleccionar carpetas:
- logs/ → Permisos 777
- uploads/ → Permisos 777
- config/ → Permisos 755
2. O por SSH:
chmod 777 public_html/logs/
chmod 777 public_html/uploads/
chmod 644 public_html/.htaccess
```
### 4. Crear Base de Datos
```bash
1. En HestiaCP → "DB"
2. Add Database:
- Name: whatsapp_bot
- User: whatsapp_user
- Password: [generar segura]
3. Importar database/schema.sql
```
### 5. Configurar config.php
```php
// Editar public_html/config/config.php
define('DB_HOST', 'localhost');
define('DB_NAME', 'usuario_whatsapp_bot'); // prefijo_nombre
define('DB_USER', 'usuario_whatsapp_user'); // prefijo_usuario
define('DB_PASS', 'tu_password_db');
define('DB_PORT', 3306);
define('DB_CHARSET', 'utf8mb4');
```
## 🌐 Configuración Específica HestiaCP
### SSL/HTTPS (Recomendado)
```bash
1. En HestiaCP → "Web" → tu dominio
2. "Edit" → SSL → "Generate" (Let's Encrypt)
3. Activar "Force SSL redirect"
4. En .htaccess descomenta las líneas HTTPS
```
### PHP Version
```bash
1. En HestiaCP → "Web" → Edit domain
2. "Advanced options" → PHP Version: 8.1+
3. "Save"
```
### Subdominio (Opcional)
```bash
1. En HestiaCP → "Web""Add Web Domain"
2. Domain: bot.tudominio.com
3. Mismo proceso de instalación
```
## 🔑 Acceso al Sistema
### URLs de Acceso:
- **Dashboard:** https://tudominio.com/
- **Login:** https://tudominio.com/login.php
- **Verificador:** https://tudominio.com/verificar_servidor.php
- **Webhook:** https://tudominio.com/api/webhook.php
### Credenciales:
- **Usuario:** admin
- **Contraseña:** 2fa904eee46f
## 🧪 Verificación Post-Instalación
### 1. Test básico
```bash
1. Ir a: https://tudominio.com/verificar_servidor.php
2. Verificar que todo esté en ✅ verde
3. Corregir cualquier ❌ error mostrado
```
### 2. Test de login
```bash
1. Ir a: https://tudominio.com/login.php
2. Ingresar credenciales
3. Verificar acceso al dashboard
```
### 3. Test de webhook
```bash
1. Verificar: https://tudominio.com/api/webhook.php
2. Debe mostrar: "Webhook endpoint activo"
3. Configurar URL en WhatsApp Business API
```
## ⚙️ Configuración WhatsApp API
### En el Dashboard:
```bash
1. Login al sistema
2. Ir a "Configuraciones"
3. Completar:
- WhatsApp Access Token
- Phone Number ID
- Business Account ID
- Webhook Verify Token
```
### En WhatsApp Business API:
```bash
1. Facebook Developers → App → WhatsApp
2. Configuration → Webhook URL:
https://tudominio.com/api/webhook.php
3. Verify Token: (copiar del dashboard)
4. Suscribirse a: conversations, message_status
```
## 🚨 Solución de Problemas HestiaCP
### Error 500 - Internal Server Error
```bash
1. Revisar error log en HestiaCP → Statistics → Error Log
2. Verificar permisos de archivos/carpetas
3. Comprobar sintaxis de .htaccess
4. Revisar versión PHP (requiere 7.4+)
```
### Base de datos no conecta
```bash
1. Verificar credenciales en config/config.php
2. Comprobar prefijo de usuario/DB en HestiaCP
3. Verificar que DB esté en mismo servidor
4. Probar conexión desde File Manager
```
### Headers de seguridad
```bash
1. Si hay conflicto con CSP, editar .htaccess
2. Ajustar Content-Security-Policy según necesidades
3. Para debugging, comentar temporalmente CSP
```
### Webhook no recibe mensajes
```bash
1. Verificar URL pública accesible
2. Comprobar SSL activo
3. Revisar logs en HestiaCP
4. Test manual: curl -X POST https://tudominio.com/api/webhook.php
```
## 📞 Soporte Específico HestiaCP
**Documentación oficial:** https://docs.hestiacp.com/
**Panel de control:** Acceso vía puerto 8083
**Logs del sistema:** /var/log/hestia/
**PHP logs:** Panel → Statistics → Error Log
## 🎯 Optimizaciones HestiaCP
### Performance
```bash
1. Activar OPcache en PHP system_config
2. Configurar Redis/Memcached si disponible
3. Optimizar MySQL en HestiaCP system_config
4. Usar Cloudflare para CDN
```
### Backup
```bash
1. HestiaCP → Backups → Schedule
2. Backup automático diario/semanal
3. Incluir: Web, DB, Mail
4. Almacenar en storage externo
```
¡Sistema listo para funcionar en HestiaCP! 🚀
-270
View File
@@ -1,270 +0,0 @@
# 🚀 Configuración SSE para HestiaCP
## ✅ Pasos para Activar SSE en tu Servidor HestiaCP
### 1. Verificar que el código esté en el servidor
```bash
# Subir por FTP/SFTP o Git
cd /home/tu-usuario/web/tu-dominio.com/public_html/
ls -la api/sse_events.php # Debe existir
```
### 2. Configurar Apache para SSE (si usas Apache)
**Opción A: Editar .htaccess en la raíz**
```apache
# Agregar al final de .htaccess
<IfModule mod_headers.c>
# SSE Headers
<FilesMatch "sse_events\.php$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set X-Accel-Buffering "no"
SetEnv no-gzip 1
</FilesMatch>
</IfModule>
```
**Opción B: Crear .htaccess específico en /api/**
```bash
cd /home/tu-usuario/web/tu-dominio.com/public_html/api/
nano .htaccess
```
Contenido:
```apache
<IfModule mod_headers.c>
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set X-Accel-Buffering "no"
SetEnv no-gzip 1
</IfModule>
# Prevenir timeout en SSE
<IfModule mod_fcgid.c>
FcgidIOTimeout 300
</IfModule>
```
### 3. Ajustar PHP-FPM (Recomendado)
Editar configuración PHP-FPM en HestiaCP:
1. **Panel HestiaCP****Web****tu dominio****Edit**
2. **Backend Template**: PHP-FPM
3. **PHP Version**: 8.x
Luego editar el archivo de configuración (vía SSH):
```bash
# Ubicación típica en HestiaCP
sudo nano /etc/php/8.2/fpm/pool.d/tu-usuario.conf
# Agregar o modificar:
request_terminate_timeout = 300s
pm.max_requests = 500
```
Reiniciar PHP-FPM:
```bash
sudo systemctl restart php8.2-fpm
```
### 4. Verificar Nginx (si usas Nginx)
Si tu dominio usa Nginx como proxy, editar la configuración:
```bash
sudo nano /etc/nginx/conf.d/domains/tu-dominio.com.conf
```
Agregar dentro del bloque `location ~ \.php$`:
```nginx
# Configuración especial para SSE
location ~ ^/api/sse_events\.php$ {
fastcgi_buffering off;
proxy_buffering off;
fastcgi_read_timeout 300s;
fastcgi_send_timeout 300s;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
}
```
Reiniciar Nginx:
```bash
sudo systemctl restart nginx
```
### 5. Probar SSE desde el servidor
```bash
# Conectar por SSH a tu servidor
ssh usuario@tu-dominio.com
# Probar SSE
curl -N -m 5 'https://tu-dominio.com/api/sse_events.php?token=demo_token'
```
**Respuesta esperada:**
```
event: connected
data: {"timestamp":1706300000,"user_id":"token_12345678","mode":"authenticated","status":"ready"}
: heartbeat
```
Si ves esto, **SSE está funcionando**
### 6. Probar desde el navegador
1. Abrir tu sitio: `https://tu-dominio.com/conversations.php`
2. Abrir Consola del navegador (F12 → Console)
3. Buscar:
```
Conectando a SSE para eventos en tiempo real...
SSE URL: https://tu-dominio.com/api/sse_events.php?token=...
✅ SSE conectado (🔐 autenticado): {...}
```
### 7. Solución de Problemas
#### Error: "Failed to fetch" o timeout
**Causa**: Firewall o mod_security bloqueando
**Solución**:
```bash
# Desactivar mod_security para SSE (temporal)
sudo a2dismod security2
sudo systemctl restart apache2
# O agregar excepción en .htaccess:
<IfModule mod_security2.c>
SecRuleEngine Off
</IfModule>
```
#### Error: "EventSource's response has a MIME type"
**Causa**: Headers incorrectos
**Solución**: Verificar que sse_events.php tenga:
```php
header('Content-Type: text/event-stream');
```
#### Error: Conexión se cierra cada 30 segundos
**Causa**: Timeout de proxy
**Solución**: Aumentar timeouts en nginx.conf o apache2.conf:
```
# Apache
Timeout 300
KeepAliveTimeout 300
# Nginx
proxy_read_timeout 300s;
```
### 8. Configuración de Seguridad (Producción)
#### A. Token seguro en vez de demo_token
Editar `api/sse_events.php`:
```php
// Generar token en login
$_SESSION['sse_token'] = bin2hex(random_bytes(32));
// Validar token en SSE
$token = $_GET['token'] ?? null;
if ($token && isset($_SESSION['sse_token']) && $token === $_SESSION['sse_token']) {
$authenticated = true;
}
```
En `conversations.php`:
```javascript
// Usar token de sesión
const token = '<?php echo $_SESSION['sse_token'] ?? 'demo_token'; ?>';
const sseUrl = `${baseUrl}/api/sse_events.php?token=${token}&t=${Date.now()}`;
```
#### B. Restringir acceso por IP (opcional)
En `.htaccess`:
```apache
<FilesMatch "sse_events\.php$">
# Solo permitir desde localhost o IPs específicas
Require ip 127.0.0.1
Require ip tu.ip.publica
</FilesMatch>
```
### 9. Monitoreo
Ver conexiones SSE activas:
```bash
# Ver procesos PHP activos
ps aux | grep sse_events
# Ver logs de errores
tail -f /var/log/php8.2-fpm.log
tail -f /var/log/apache2/error.log
tail -f /var/log/nginx/error.log
```
### 10. Performance
Para muchos usuarios simultáneos:
```bash
# Aumentar límites PHP-FPM
sudo nano /etc/php/8.2/fpm/pool.d/tu-usuario.conf
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500
```
---
## 🎯 Checklist de Verificación
- [ ] Archivo `api/sse_events.php` subido al servidor
- [ ] `.htaccess` configurado con headers SSE
- [ ] PHP-FPM timeout aumentado a 300s
- [ ] Nginx/Apache configurado (si aplica)
- [ ] Test con curl funciona (muestra "event: connected")
- [ ] Test desde navegador funciona (console.log muestra "✅ SSE conectado")
- [ ] Token de seguridad configurado (no usar demo_token en producción)
- [ ] Firewall/mod_security permite conexiones largas
---
## 📞 Soporte HestiaCP
Si tienes problemas:
1. **Panel HestiaCP**: https://tu-dominio.com:8083
2. **Logs**: Panel → Log Files
3. **SSH**: Usuario y contraseña en el panel
4. **Documentación**: https://docs.hestiacp.com/
---
## ✅ Resultado Esperado
Una vez configurado correctamente:
```
ANTES (desarrollo local):
http://localhost:8000/api/sse_events.php
❌ Timeout (servidor PHP built-in no soporta SSE)
AHORA (producción HestiaCP):
https://tu-dominio.com/api/sse_events.php
✅ Conectado - eventos en tiempo real < 1 segundo
```
**¡Tu sistema de notificaciones en tiempo real estará funcionando!** 🚀
-260
View File
@@ -1,260 +0,0 @@
# 🔧 Guía de Solución de Errores SSE
## Error: "❌ Error en SSE" sin detalles
### Diagnóstico Rápido
1. **Revisar la consola del navegador**
- Buscar el nuevo log con `readyState: X (STATE_NAME)`
- Estados posibles:
- `0 (CONNECTING)`: El navegador está intentando conectar
- `1 (OPEN)`: Conexión establecida correctamente
- `2 (CLOSED)`: Conexión cerrada (error)
2. **Verificar la pestaña Network**
- Buscar la petición a `sse_events.php`
- Ver el status code:
- `200`: OK (pero puede haber error en el stream)
- `401`: No autenticado
- `500`: Error del servidor
- `502/504`: Error de proxy/timeout
### Solución por Estado
#### Estado CLOSED (readyState: 2)
**Causa común**: El servidor cierra la conexión inmediatamente
**Solución**:
```bash
# 1. Verificar logs del servidor
tail -f /var/log/apache2/error.log
# o
tail -f /var/log/nginx/error.log
# 2. Probar el endpoint directamente
curl -N http://localhost:8000/api/sse_events.php?token=demo_token
# 3. Ejecutar el diagnóstico completo
php test_sse_complete.php
```
**Posibles problemas**:
- PHP cierra la conexión por timeout → Verificar `set_time_limit(0)` en sse_events.php
- Buffer de salida activo → Verificar que no hay `ob_start()` antes
- Nginx/Apache cortando la conexión → Configurar proxy buffering off
#### Estado CONNECTING (readyState: 0)
**Causa común**: El navegador no puede conectar al servidor
**Solución**:
```bash
# 1. Verificar que el servidor está corriendo
lsof -i :8000
# 2. Verificar firewall
# macOS
sudo pfctl -s rules
# 3. Probar desde curl
curl -I http://localhost:8000/api/sse_events.php?token=demo_token
```
### Configuración del Servidor
#### Apache (.htaccess)
```apache
# Deshabilitar compresión para SSE
<IfModule mod_headers.c>
<FilesMatch "sse_events\.php$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set X-Accel-Buffering "no"
SetEnv no-gzip 1
</FilesMatch>
</IfModule>
```
#### Nginx (nginx.conf)
```nginx
location /api/sse_events.php {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
include fastcgi_params;
# Deshabilitar buffering para SSE
fastcgi_buffering off;
proxy_buffering off;
# Timeouts largos
fastcgi_read_timeout 300s;
fastcgi_send_timeout 300s;
}
```
#### PHP-FPM (www.conf)
```ini
; Aumentar timeout de ejecución
request_terminate_timeout = 300s
; Permitir más procesos para SSE
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
```
### Pruebas Manuales
#### Test 1: Verificar headers
```bash
curl -I http://localhost:8000/api/sse_events.php?token=demo_token
```
**Esperado**:
```
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache, no-store, must-revalidate
Connection: keep-alive
X-Accel-Buffering: no
```
#### Test 2: Recibir eventos
```bash
curl -N http://localhost:8000/api/sse_events.php?token=demo_token
```
**Esperado**:
```
event: connected
data: {"timestamp":1706300000,"user_id":"token_12345678","mode":"authenticated"}
: heartbeat
```
#### Test 3: Diagnóstico completo
```bash
php test_sse_complete.php
```
**Esperado**:
```
✅ Headers SSE correctos
[1] 📨 Evento: connected
📦 Data: {
"timestamp": 1706300000,
"user_id": "token_12345678",
"mode": "authenticated"
}
```
### Solución de Problemas Comunes
#### Error: "net::ERR_INCOMPLETE_CHUNKED_ENCODING"
**Causa**: El servidor termina el stream abruptamente
**Solución**:
1. Verificar que no hay errores PHP en el stream
2. Aumentar `max_execution_time` en php.ini
3. Verificar que no hay `exit()` o `die()` en el loop SSE
#### Error: "Failed to fetch"
**Causa**: CORS o credenciales
**Solución**:
```javascript
// En conversations.php, verificar que se usa:
this.eventSource = new EventSource(sseUrl, {
withCredentials: true
});
```
```php
// En sse_events.php, agregar headers CORS:
header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
header('Access-Control-Allow-Credentials: true');
```
#### Error: Connection timeout después de 30s
**Causa**: Servidor proxy cortando conexión
**Solución Apache**:
```apache
Timeout 300
KeepAliveTimeout 300
```
**Solución Nginx**:
```nginx
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
```
### Debugging Avanzado
#### Habilitar logs detallados PHP
```php
// Agregar al inicio de sse_events.php
error_reporting(E_ALL);
ini_set('display_errors', 0); // No mostrar en stream
ini_set('log_errors', 1);
ini_set('error_log', '/tmp/sse_debug.log');
```
#### Monitorear eventos en tiempo real
```bash
# Terminal 1: Ver logs
tail -f /tmp/sse_debug.log
# Terminal 2: Conectar SSE
php test_sse_complete.php
# Terminal 3: Enviar evento de prueba
curl -X POST http://localhost:8000/api/push_event.php \
-H "Content-Type: application/json" \
-d '{"event":"test","data":{"message":"Prueba"},"target":"all"}'
```
### Checklist de Verificación
- [ ] El servidor está corriendo en el puerto esperado
- [ ] Los headers SSE están correctos (Content-Type: text/event-stream)
- [ ] No hay buffering de salida activo
- [ ] El endpoint responde con HTTP 200
- [ ] Se recibe el evento 'connected' al conectar
- [ ] Los eventos tienen el formato correcto (event: XXX\ndata: {}\n\n)
- [ ] El archivo de eventos temporal es escribible (/uploads/)
- [ ] Los logs no muestran errores PHP
- [ ] El navegador no está bloqueando la conexión (CORS)
- [ ] La autenticación está funcionando (sesión o token)
### Contacto y Soporte
Si después de seguir esta guía el problema persiste:
1. Ejecutar `php test_sse_complete.php` y copiar la salida completa
2. Verificar `/tmp/sse_debug.log` y copiar los últimos 50 líneas
3. Revisar la consola del navegador y copiar todos los logs relacionados con SSE
4. Verificar Network tab y copiar headers/response de sse_events.php
## Mejoras Implementadas
### 1. Mejor Logging en Cliente (conversations.php)
- ✅ Log detallado del readyState con nombre del estado
- ✅ Limpieza de timeout antes de reconectar
- ✅ Try-catch al cerrar EventSource
### 2. Mejor Manejo en Servidor (sse_events.php)
- ✅ Límite de tiempo de conexión (5 minutos)
- ✅ Detección de cliente desconectado
- ✅ Logs más detallados con trace completo
- ✅ Headers optimizados para diferentes servidores
### 3. Herramientas de Diagnóstico
- ✅ test_sse_complete.php: Prueba completa de conexión
- ✅ Parseo de eventos en tiempo real
- ✅ Medición de tiempos de conexión
-91
View File
@@ -1,91 +0,0 @@
# 🚀 Instrucciones para Subir al Servidor
## 📁 Archivo Listo para Subir
- **Archivo:** `whatsapp_fixed_20260104_103742.zip`
- **Tamaño:** ~164KB
- **Estado:** ✅ Todas las rutas corregidas para hosting
## 🔧 Cambios Realizados
### ✅ Rutas Corregidas
- **Antes:** `require_once '../config/config.php';`
- **Después:** `require_once 'config/config.php';`
**Archivos corregidos:**
- ✅ api/webhook.php
- ✅ api/send_message.php
- ✅ api/send_broadcast.php
- ✅ Todas las 17 APIs del sistema
- ✅ Sistema de autenticación añadido en todas las APIs
## 📋 Proceso de Instalación
### 1. Subir Archivos
```bash
1. Extraer whatsapp_fixed_20260104_103742.zip
2. Subir todos los archivos a public_html/
3. Mantener la estructura de carpetas
```
### 2. Configurar Permisos
```bash
chmod 755 api/
chmod 755 config/
chmod 755 classes/
chmod 755 services/
chmod 777 logs/
chmod 777 uploads/
```
### 3. Configurar Base de Datos
```bash
1. Importar database/schema.sql
2. Editar config/config.php con datos de tu hosting:
- DB_HOST
- DB_NAME
- DB_USER
- DB_PASS
```
### 4. Configurar WhatsApp API
```bash
1. Obtener tokens de WhatsApp Business API
2. Actualizar en config/config.php:
- WHATSAPP_TOKEN
- WHATSAPP_PHONE_NUMBER_ID
- WHATSAPP_API_URL
```
## 🔑 Credenciales de Acceso
- **Usuario:** admin
- **Contraseña:** 2fa904eee46f
- **URL Login:** https://tudominio.com/login.php
## 🧪 Verificar Instalación
- **Dashboard:** https://tudominio.com/
- **Tests:** https://tudominio.com/run_tests.php
- **Webhook:** https://tudominio.com/api/webhook.php
## 🎯 Características Implementadas
- ✅ Sistema de autenticación completo
- ✅ Protección contra fuerza bruta
- ✅ API segura con verificación de sesión
- ✅ Sistema de plantillas WhatsApp
- ✅ Envío de mensajes con templates
- ✅ Dashboard con analytics
- ✅ Suite de tests completa
- ✅ Gestión de usuarios y conversaciones
## 🔒 Seguridad
- ✅ Todas las APIs requieren autenticación
- ✅ Protección contra ataques de fuerza bruta
- ✅ Sesiones seguras con timeouts
- ✅ Validación de entrada en formularios
- ✅ Headers de seguridad implementados
## 📞 Soporte
El sistema está completamente funcional y probado. Si tienes problemas:
1. Verificar permisos de archivos
2. Revisar configuración de base de datos
3. Comprobar tokens de WhatsApp API
4. Ejecutar tests desde run_tests.php
-337
View File
@@ -1,337 +0,0 @@
# ✅ Notificaciones por SSE - Implementación Completa
## 📋 Resumen de Cambios
### 🎯 Objetivo
Eliminar el polling constante de notificaciones (cada 7-15 segundos) y reemplazarlo con push en tiempo real mediante SSE.
---
## 🔧 Archivos Modificados
### 1. **conversations.php**
**Líneas 1058-1065**: Polling de notificaciones reducido
```javascript
// ANTES: setInterval(() => this.loadNotifications(), 7000);
// AHORA: setInterval(() => this.loadNotifications(), 60000);
```
- ✅ Cambio de 7s → 60s (solo como backup)
- ✅ Comentario: "SSE se encargará de las nuevas en tiempo real"
**Líneas 1112-1124**: Nuevo listener SSE para notificaciones
```javascript
this.eventSource.addEventListener('notification', (e) => {
const notification = JSON.parse(e.data);
this.showNotificationToast(notification);
});
```
- ✅ Escucha evento 'notification' de SSE
- ✅ Muestra el toast automáticamente
---
### 2. **api/sse_events.php**
**Líneas 125-142**: Envío de notificaciones en el loop SSE
```php
$notifications = $db->fetchAll(
"SELECT id, user_id, type, message, data, is_read, created_at
FROM notifications
WHERE is_read = 0
ORDER BY created_at DESC
LIMIT 10"
);
foreach ($notifications as $notification) {
sendSSEEvent('notification', $notification);
}
```
- ✅ Lee notificaciones no leídas cada 3 segundos
- ✅ Las envía por SSE a todos los clientes conectados
- ✅ Manejo de errores con try-catch
---
## 📦 Archivos Nuevos
### 3. **classes/NotificationHelper.php** ⭐
Helper para crear y enviar notificaciones automáticamente por SSE.
**Método principal: `create()`**
```php
NotificationHelper::create(
$userId, // ID del usuario
'document', // Tipo: message, document, status, urgent, etc.
'Usuario subió 3 documentos', // Mensaje
['count' => 3] // Datos adicionales (opcional)
);
```
**Método auxiliar: `pushSSE()`**
```php
NotificationHelper::pushSSE($notification);
```
**Características:**
- ✅ Inserta en BD (tabla `notifications`)
- ✅ Envía automáticamente por SSE vía `push_event.php`
- ✅ Broadcast a todos los operadores conectados
- ✅ Fire-and-forget (no bloquea ejecución)
- ✅ Manejo de errores con logs
---
### 4. **examples/notification_helper_usage.php**
Ejemplos de uso del helper:
- Notificación cuando usuario sube documento
- Notificación de mensaje importante
- Notificación de cambio de estado
- Notificación personalizada
---
## 📊 Comparación Antes vs Ahora
| Aspecto | ANTES | AHORA | Mejora |
|---------|-------|-------|--------|
| **Notificaciones** | Cada 7s | Cada 60s (backup) | **88% menos** |
| **Conversaciones** | Cada 30s | ❌ ELIMINADO (solo SSE) | **100% menos** |
| **Latencia** | 3-7 segundos | < 1 segundo | **~85% más rápido** |
| **Carga servidor** | ~10 req/min | ~1 req/min | **90% reducción** |
| **Método** | HTTP GET repetido | SSE push | Push en tiempo real |
### Detalles por Endpoint
| Endpoint | Frecuencia ANTES | Frecuencia AHORA | Reducción |
|----------|------------------|------------------|-----------|
| `get_notifications.php` | Cada 7s (~8.5/min) | Cada 60s (1/min) | **88%** |
| `get_conversations.php` | Cada 30s (2/min) | ❌ ELIMINADO | **100%** |
| `get_messages.php` | Cada 10s (6/min) | Cada 10s (solo activa) | 0% (ya optimizado) |
**Total del sistema: 90% menos peticiones HTTP** 🚀
---
## 🚀 Cómo Usar
### Crear notificación desde cualquier archivo PHP:
```php
// 1. Incluir el helper
require_once __DIR__ . '/classes/NotificationHelper.php';
// 2. Crear y enviar notificación (automáticamente por SSE)
NotificationHelper::create(
$userId,
'document',
'Usuario subió documento importante',
['file_name' => 'factura.pdf', 'file_size' => 2048000]
);
```
### El cliente NO necesita hacer nada:
- ✅ El evento SSE llega automáticamente
- ✅ Se muestra el toast de notificación
- ✅ Se reproduce sonido (si está habilitado)
- ✅ El usuario puede hacer clic para ver detalles
---
## 🔍 Flujo Completo
```
1. Evento ocurre (usuario sube archivo, nuevo mensaje, etc.)
2. Tu código llama: NotificationHelper::create(...)
3. Helper inserta en BD (tabla notifications)
4. Helper envía POST a push_event.php
5. push_event.php escribe evento en archivo temporal
6. sse_events.php lee eventos pendientes
7. sse_events.php envía evento 'notification' al cliente
8. Cliente recibe evento y muestra toast
9. Usuario ve notificación en < 1 segundo
```
---
## 🎨 Tipos de Notificaciones Soportados
| Tipo | Descripción | Uso |
|------|-------------|-----|
| `message` | Mensaje nuevo o importante | Mensajes con palabras clave |
| `document` | Usuario subió documento | Upload de archivos |
| `status` | Cambio de estado | Estado del ticket/caso |
| `urgent` | Notificación urgente | Mensajes prioritarios |
| `info` | Información general | Avisos del sistema |
| `warning` | Advertencia | Límites, errores menores |
| `success` | Acción exitosa | Confirmaciones |
| `custom` | Personalizado | Cualquier otro tipo |
---
## 📝 Ejemplos Prácticos
### Ejemplo 1: Notificar cuando usuario sube documentos
```php
// En api/upload_media.php
if ($uploadSuccess && $fileType === 'document') {
require_once __DIR__ . '/../classes/NotificationHelper.php';
NotificationHelper::create(
$userId,
'document',
"Usuario {$userName} subió: {$fileName}",
[
'file_name' => $fileName,
'file_type' => $fileType,
'file_size' => $fileSize
]
);
}
```
### Ejemplo 2: Notificar mensaje con palabra clave
```php
// En api/webhook.php
if (preg_match('/\b(urgente|importante|ayuda)\b/i', $messageText)) {
require_once __DIR__ . '/../classes/NotificationHelper.php';
NotificationHelper::create(
$userId,
'urgent',
"Mensaje urgente de {$userName}",
[
'message_preview' => substr($messageText, 0, 100),
'phone' => $userPhone
]
);
}
```
### Ejemplo 3: Notificar cambio de estado
```php
// En cualquier script de gestión
if ($statusChanged) {
require_once __DIR__ . '/classes/NotificationHelper.php';
NotificationHelper::create(
$userId,
'status',
"Estado cambiado: {$oldStatus}{$newStatus}",
[
'old_status' => $oldStatus,
'new_status' => $newStatus,
'changed_by' => $_SESSION['admin_name'] ?? 'Sistema'
]
);
}
```
---
## ✅ Ventajas de Esta Implementación
1. **Sin Cambios en Cliente**: El frontend sigue igual, solo agregamos un listener
2. **Automático**: Solo llamas a `create()` y todo sucede automáticamente
3. **Sin Bloqueos**: Fire-and-forget, no afecta el rendimiento
4. **Escalable**: Funciona con 1 o 1000 operadores conectados
5. **Fallback**: Si SSE falla, el polling de 60s lo cubre
6. **Flexible**: Acepta cualquier tipo y datos personalizados
7. **Debugging**: Logs detallados en error_log
---
## 🧪 Cómo Probar
### 1. Verificar que SSE está conectado:
```bash
# Abrir consola del navegador en conversations.php
# Deberías ver: "✅ SSE conectado (🔐 autenticado)"
```
### 2. Crear notificación de prueba:
```bash
php -r "
require_once 'classes/NotificationHelper.php';
NotificationHelper::create(1, 'test', 'Prueba de notificación SSE', ['test' => true]);
echo 'Notificación enviada\n';
"
```
### 3. Verificar en el navegador:
- Deberías ver el toast aparecer en < 1 segundo
- En la consola: "🔔 Nueva notificación (SSE): ..."
---
## 📈 Impacto en Performance
### Servidor
- **Antes**: ~8-9 peticiones GET /api/get_notifications.php por minuto por cliente
- **Ahora**: ~1 petición por minuto (solo backup)
- **Reducción**: 88% menos carga HTTP
### Cliente
- **Antes**: Latencia de 3-7 segundos para ver notificación
- **Ahora**: Latencia < 1 segundo
- **Mejora**: 6x más rápido
### Base de Datos
- **Antes**: Query cada 7s → ~8.5 queries/min
- **Ahora**: Query cada 60s (backup) + SSE cada 3s (compartido con otros eventos)
- **Reducción**: ~85% menos queries independientes
---
## 🔐 Seguridad
- ✅ Solo localhost puede llamar a `push_event.php` (protegido por IP)
- ✅ Autenticación por sesión o token en SSE
- ✅ Notificaciones solo visibles para operadores autenticados
- ✅ Datos sensibles en campo `data` (no en mensaje)
---
## 🐛 Troubleshooting
### Notificaciones no aparecen:
1. Verificar que SSE está conectado (consola del navegador)
2. Revisar logs: `tail -f /var/log/apache2/error.log`
3. Verificar que `push_event.php` es accesible
4. Ejecutar: `php test_sse_complete.php`
### Notificaciones duplicadas:
- Normal si hay múltiples operadores conectados
- Cada operador recibe su propia notificación
### Polling aún muy frecuente:
- Verificar `setupNotificationPolling()` en conversations.php
- Debe ser 60000ms (60 segundos), no 15000ms
---
## 📚 Referencias
- [SSE_REALTIME_DOCS.md](SSE_REALTIME_DOCS.md) - Documentación completa del sistema SSE
- [GUIA_SOLUCION_SSE.md](GUIA_SOLUCION_SSE.md) - Guía de solución de problemas
- [examples/notification_helper_usage.php](examples/notification_helper_usage.php) - Ejemplos de uso
---
## 🎉 Resumen Final
Con esta implementación:
- ✅ Eliminamos 88% de las peticiones de notificaciones
- ✅ Notificaciones llegan 6x más rápido (< 1s vs 3-7s)
- ✅ Sistema más escalable y eficiente
- ✅ API simple y fácil de usar: `NotificationHelper::create()`
- ✅ Sin cambios en el frontend (solo agregamos listener)
- ✅ Fallback automático si SSE falla
**Total de reducción en peticiones HTTP: ~85% en todo el sistema** 🚀
-381
View File
@@ -1,381 +0,0 @@
# 🎯 Optimización Completa - Eliminación de Polling
## 📊 Resumen de Cambios
### ❌ ANTES: Sistema de Polling Constante
```
┌─────────────────────────────────────────────────────────┐
│ Cliente (Navegador) │
├─────────────────────────────────────────────────────────┤
│ │
│ ⏰ Cada 7 segundos: │
│ GET /api/get_notifications.php │
│ → 8.5 peticiones/minuto │
│ │
│ ⏰ Cada 30 segundos: │
│ GET /api/get_conversations.php │
│ → 2 peticiones/minuto │
│ │
│ ⏰ Cada 10 segundos (si hay conversación abierta): │
│ GET /api/get_user_messages.php (delta) │
│ → 6 peticiones/minuto │
│ │
│ 📊 TOTAL: ~16.5 peticiones/minuto por cliente │
│ │
└─────────────────────────────────────────────────────────┘
```
**Problemas:**
- 🔴 Alta latencia (3-7 segundos para ver cambios)
- 🔴 Carga innecesaria en servidor
- 🔴 Peticiones redundantes (aunque no haya cambios)
- 🔴 No escala bien con muchos usuarios
---
### ✅ AHORA: Sistema SSE Puro (Sin Polling)
```
┌─────────────────────────────────────────────────────────┐
│ Cliente (Navegador) │
├─────────────────────────────────────────────────────────┤
│ │
│ 🔌 SSE Conectado (1 conexión persistente): │
│ ← Evento 'new_message' (< 1s) │
│ → Recarga mensajes de conversación activa │
│ ← Evento 'new_conversation' (< 1s) │
│ → Agrega conversación a lista │
│ ← Evento 'notification' (< 1s) │
│ → Muestra toast de notificación │
│ ← Evento 'heartbeat' (cada 30s) │
│ → Mantiene conexión viva │
│ │
│ 🔄 Backup (solo si SSE falla): │
│ GET /api/get_notifications.php (cada 60s) │
│ → 1 petición/minuto │
│ │
│ 📊 TOTAL: ~1 petición/minuto por cliente │
│ (95% reducción) 🎉 │
│ │
└─────────────────────────────────────────────────────────┘
```
**Ventajas:**
- 🟢 Latencia < 1 segundo (10x más rápido)
- 🟢 95% menos carga en servidor
- 🟢 Solo transmite cuando hay cambios reales
- 🟢 Escala perfectamente con muchos usuarios
- 🟢 Sin polling = Sin queries innecesarias a BD
---
## 📁 Archivos Modificados
### 1. `conversations.php`
#### Cambio 1: Notificaciones (Línea 1058-1065)
```javascript
// ANTES
setupNotificationPolling() {
this.loadNotifications();
setInterval(() => this.loadNotifications(), 7000); // ❌ Cada 7s
}
// AHORA
setupNotificationPolling() {
// Carga inicial de notificaciones pendientes
// SSE se encargará de las nuevas en tiempo real
this.loadNotifications();
// Backup: verificar cada 60 segundos (solo por si SSE falla)
setInterval(() => this.loadNotifications(), 60000); // ✅ Cada 60s
}
```
#### Cambio 2: Conversaciones (Línea 2132-2138)
```javascript
// ANTES
setupAutoRefresh() {
// Actualizar conversaciones cada 30 segundos
setInterval(() => {
this.loadConversations(); // ❌ Cada 30s
}, 30000);
// ... más código
}
// AHORA
setupAutoRefresh() {
// NOTA: La recarga automática de conversaciones fue ELIMINADA
// SSE se encarga de actualizar en tiempo real con eventos:
// - 'new_message': actualiza la conversación existente
// - 'new_conversation': agrega nueva conversación a la lista
// Esto elimina el polling constante y reduce la carga en ~90%
// ✅ Sin setInterval de conversaciones
// ... resto del código (solo polling de mensajes)
}
```
#### Cambio 3: Listeners SSE (Línea 1094-1125)
```javascript
// Evento: nuevo mensaje entrante
this.eventSource.addEventListener('new_message', (e) => {
const data = JSON.parse(e.data);
// Actualizar la lista de conversaciones (sin recargar)
this.updateConversationInList(data); // ✅ Actualización inteligente
// Si es la conversación activa, recargar mensajes
if (this.currentUserId === data.user_id) {
this.loadMessages(this.currentUserId, false, true);
}
this.playNotificationSound();
});
// Evento: nueva conversación detectada
this.eventSource.addEventListener('new_conversation', (e) => {
const data = JSON.parse(e.data);
// Agregar a la lista (sin recargar todo)
this.addConversationToList(data); // ✅ Solo agrega una
this.playNotificationSound();
});
// Evento: notificación del sistema
this.eventSource.addEventListener('notification', (e) => {
const notification = JSON.parse(e.data);
this.showNotificationToast(notification); // ✅ Push instantáneo
});
```
---
### 2. `api/sse_events.php`
#### Cambio: Envío de notificaciones (Línea 125-142)
```php
// Verificar notificaciones no leídas cada 3 segundos
try {
$notifications = $db->fetchAll(
"SELECT id, user_id, type, message, data, is_read, created_at
FROM notifications
WHERE is_read = 0
ORDER BY created_at DESC
LIMIT 10"
);
if ($notifications && count($notifications) > 0) {
foreach ($notifications as $notification) {
sendSSEEvent('notification', $notification); // ✅ Push automático
}
}
} catch (Exception $e) {
error_log('SSE: Error leyendo notificaciones: ' . $e->getMessage());
}
```
---
### 3. `classes/NotificationHelper.php` (NUEVO)
```php
class NotificationHelper {
// Crear y enviar notificación automáticamente
public static function create($userId, $type, $message, $data = []) {
// 1. Insertar en BD
$notificationId = $db->insert('notifications', [...]);
// 2. Enviar por SSE automáticamente
self::pushSSE($notification);
return $notificationId;
}
// Enviar notificación por SSE
public static function pushSSE($notification) {
// Fire-and-forget a push_event.php
$ch = curl_init('http://localhost:8000/api/push_event.php');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'event' => 'notification',
'data' => $notification,
'target' => 'all'
]));
curl_exec($ch);
curl_close($ch);
}
}
```
**Uso simple:**
```php
// En cualquier archivo PHP
NotificationHelper::create(
$userId,
'document',
'Usuario subió 3 documentos',
['count' => 3]
);
// ¡Eso es todo! Aparece en el navegador en < 1 segundo
```
---
## 📈 Impacto Medible
### Por Endpoint
user_messages.php` | 6 | 0 | **100%** ⬇️ |
| **TOTAL** | **16.5** | **~1** | **~95%** ⬇️ |
### Por Usuario
- **1 usuario conectado**: De 16.5 req/min → ~1 req/min
- **10 usuarios**: De 165 req/min → ~10 req/min
- **100 usuarios**: De 1650 req/min → ~100 req/min
**Ahorro de recursos**: 15
- **1 usuario conectado**: De 16.5 req/min → 1-2 req/min
- **10 usuarios**: De 165 req/min → 10-20 req/min
- **100 usuarios**: De 1650 req/min → 100-200 req/min
**Ahorro de recursos**: 1450 peticiones HTTP menos por minuto con 100 usuarios 🚀
### Latencia
| Evento | Antes | Ahora | Mejora |
|--------|-------|-------|--------|
| **Nueva notificación** | 3-7s | < 1s | **6x más rápido** |
| **Nueva conversación** | 15-30s | < 1s | **30x más rápido** |
| **Nuevo mensaje** | 5-10s | < 1s | **10x más rápido** |
---
## 🎯 Flujo Completo de un Evento
### Ejemplo: Usuario sube un documento
```
┌─────────────────────────────────────────────────────────┐
│ 1. Usuario sube archivo │
│ → POST /api/upload_media.php │
└─────────────────┬───────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 2. Script PHP llama a NotificationHelper │
│ NotificationHelper::create( │
│ $userId, 'document', │
│ 'Usuario subió documento.pdf', │
│ ['file_name' => 'documento.pdf'] │
│ ) │
└─────────────────┬───────────────────────────────────────┘
├─► Inserta en tabla notifications
└─► POST interno a push_event.php
┌─────────────────────────────────────────────────────────┐
│ 3. push_event.php escribe evento en archivo temporal │
│ /uploads/events_all.json │
└─────────────────┬───────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 4. sse_events.php lee el archivo (cada 2-3s) │
│ → sendSSEEvent('notification', [...]) │
└─────────────────┬───────────────────────────────────────┘
│ (Conexión SSE persistente)
┌─────────────────────────────────────────────────────────┐
│ 5. Navegador recibe evento en < 1 segundo │
│ eventSource.addEventListener('notification', ...) │
│ → showNotificationToast(notification) │
└─────────────────┬───────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 6. Usuario ve el toast de notificación │
│ 🔔 "Usuario subió documento.pdf" │
└─────────────────────────────────────────────────────────┘
⏱️ Tiempo total: < 1 segundo
📉 Peticiones HTTP: 0 (solo la subida inicial)
```
---
## ✅ Checklist de Optimización
- [x] Notificaciones: 7s → 60s (backup)
- [x] Conversaciones: 30s → **ELIMINADO**
- [x] Mensajes delta: 10s → **ELIMINADO**
- [x] SSE listener para `notification`
- [x] SSE listener para `new_message` (recarga mensajes automáticamente)
- [x] SSE listener para `new_conversation`
- [x] SSE envía notificaciones automáticamente
- [x] NotificationHelper creado
- [x] Ejemplos de uso documentados
- [x] Métodos `updateConversationInList()` y `addConversationToList()` funcionando
- [x] Documentación actualizada
**Todo el polling HTTP ha sido eliminado. Sistema 100% push con SSE.**
---
## 🚀 Resultado Final
### Antes vs Ahora - Visualización
```
ANTES (Polling):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GET notifications ⏰⏰⏰⏰⏰⏰⏰⏰ (cada 7s)
GET conversations ⏰ ⏰ ⏰ ⏰ (cada 30s)
GET messages ⏰⏰⏰⏰⏰⏰ (cada 10s)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total: ~16 peticiones/minuto 🔴
AHORA (SSE + Backup):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SSE push 🔌 ────────────────── (persistente)
new_message ▶ (cuando ocurre, < 1s)
new_conversation ▶ (cuando ocurre, < 1s)
notification ▶ (cuando ocurre, < 1s)
heartbeat ♥ (cada 30s)
GET notifications ⏰ (cada 60s, backup)
GET messages ⏰⏰⏰⏰⏰⏰ (cada 10s, activa)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total: ~1-2 peticiones/minuto 🟢
📊 Reducción: 90%
⚡ Velocidad: 6-30x más rápido
```
---
## 🎉 Conclusión
Con esta optimización completa:
✅ **95% menos peticiones HTTP** (16.5/min → ~1/min)
✅ **10x más rápido** en tiempo de respuesta (< 1s vs 3-30s)
✅ **Escalabilidad mejorada** exponencialmente
✅ **Mejor experiencia de usuario** (tiempo real verdadero)
✅ **Menor carga en servidor** (CPU, RAM, BD)
✅ **Menor consumo de batería** en dispositivos móviles
✅ **API simple** para crear notificaciones: `NotificationHelper::create()`
**Sin polling** = Solo eventos cuando hay cambios reales
### Sistema Verdaderamente En Tiempo Real
**Antes**: Polling cada X segundos (cliente pregunta: "¿hay algo nuevo?")
**Ahora**: SSE push instantáneo (servidor dice: "¡ESTO es nuevo!")
**El sistema ahora es verdaderamente en tiempo real con 0 polling innecesario.** 🚀
-177
View File
@@ -1,177 +0,0 @@
# WhatsApp Bot Manager 🚀
<p align="center">
<img src="https://img.shields.io/badge/PHP-8.2+-777BB4?style=for-the-badge&logo=php&logoColor=white" alt="PHP 8.2+">
<img src="https://img.shields.io/badge/MariaDB-10.11-003545?style=for-the-badge&logo=mariadb&logoColor=white" alt="MariaDB">
<img src="https://img.shields.io/badge/Docker-Ready-2496ED?style=for-the-badge&logo=docker&logoColor=white" alt="Docker Ready">
<img src="https://img.shields.io/badge/WhatsApp-API-25D366?style=for-the-badge&logo=whatsapp&logoColor=white" alt="WhatsApp API">
</p>
<p align="center">
<strong>Panel administrativo para gestionar bots y envíos de WhatsApp Business</strong>
</p>
<p align="center">
Desarrollado con 💚 por <a href="https://u-site.app"><strong>U-Site.app</strong></a>
</p>
---
## 🏢 Sobre U-Site.app
**U-Site.app** es una casa de desarrollo de software especializada en soluciones empresariales, automatización y aplicaciones web modernas. Creamos herramientas que simplifican procesos y potencian negocios.
🌐 **Website:** [u-site.app](https://u-site.app)
---
## ✨ Características
- 📱 **Gestión de conversaciones** - Interfaz tipo WhatsApp Web
- 📨 **Plantillas de mensajes** - Envíos masivos con templates aprobados
- 🤖 **Bot automatizado** - Respuestas automáticas configurables
- 🔐 **Autenticación segura** - Protección contra fuerza bruta
- 📎 **Multimedia** - Soporte para imágenes, documentos, audio y video
- 🔔 **Notificaciones SSE** - Tiempo real sin polling
- 🐳 **Docker Ready** - Despliegue en contenedores
- 🇨🇴 **Zona horaria Colombia** - Configurado para América/Bogotá
---
## 🛠️ Requisitos
| Requisito | Versión |
|-----------|---------|
| PHP | 8.0+ |
| MariaDB/MySQL | 10.11+ / 8.0+ |
| Redis | 7.0+ |
| Docker | 24.0+ (opcional) |
**Extensiones PHP requeridas:** PDO, MySQL, mbstring, curl, openssl, json, gd
---
## 🚀 Instalación
### Opción 1: Docker (Recomendado)
```bash
# Clonar repositorio
git clone https://github.com/tu-usuario/whatsapp.git
cd whatsapp
# Desarrollo local
docker-compose -f docker-compose.dev.yml up -d
# Producción
docker-compose -f docker-compose.prod.yml up -d
```
**URLs de acceso (desarrollo):**
- 🌐 App: http://localhost:8080
- 🗄️ phpMyAdmin: http://localhost:8084
### Opción 2: Instalación Manual
1. Clonar y configurar:
```bash
git clone https://github.com/tu-usuario/whatsapp.git
cd whatsapp
cp .env.example .env
```
2. Editar `.env` con tus credenciales
3. Ejecutar instalador: `http://tu-servidor/install.php`
---
## 📁 Estructura del Proyecto
```
whatsapp/
├── api/ # Endpoints API REST
├── assets/ # CSS, JS, imágenes
├── classes/ # Clases PHP (Database, Services)
├── config/ # Configuración
├── database/ # Scripts SQL y backups
├── docker/ # Configuración Docker (nginx, php, supervisor)
├── services/ # Servicios (WhatsApp, Bot)
├── uploads/ # Archivos subidos
├── Dockerfile # Imagen principal
├── Dockerfile.worker # Imagen para workers
├── docker-compose.dev.yml # Desarrollo
├── docker-compose.prod.yml # Producción
└── deploy-vps.sh # Script de despliegue
```
---
## ⚙️ Configuración
### Variables de Entorno (.env)
```env
# Base de datos
DB_HOST=mysql
DB_PORT=3306
DB_NAME=usite_whatsapp_bot
DB_USER=tu_usuario
DB_PASS=tu_password
# WhatsApp Business API
WHATSAPP_TOKEN=tu_token_aqui
WHATSAPP_PHONE_NUMBER_ID=tu_phone_id
WEBHOOK_VERIFY_TOKEN=tu_webhook_token
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
```
### Webhook de WhatsApp
Configura el webhook en Meta Developer Console:
- **URL:** `https://tu-dominio.com/api/webhook.php`
- **Verify Token:** El mismo que `WEBHOOK_VERIFY_TOKEN`
---
## 🐳 Contenedores Docker
| Contenedor | Descripción | Puerto |
|------------|-------------|--------|
| `whatsapp-app` | PHP + Nginx | 80, 443 |
| `whatsapp-mysql` | MariaDB 10.11 | 3306 |
| `whatsapp-redis` | Redis 7 | 6379 |
| `whatsapp-worker` | Procesos background | - |
---
## 📖 Documentación Adicional
- [Guía de Instalación](INSTALACION.md)
- [Guía de Despliegue](DEPLOYMENT_GUIDE.md)
- [Configuración SSE](README_SSE.md)
- [Solución de Problemas](SOLUCION_HOSTING.md)
---
## 🔒 Seguridad
- Los directorios `dev/`, `tests/`, `scripts/` contienen herramientas de desarrollo
- **NO exponer** estos directorios en producción
- Proteger con `.htaccess` o configuración de nginx
- Rotar credenciales regularmente
---
## 📄 Licencia
Propietario - © 2026 [U-Site.app](https://u-site.app)
---
<p align="center">
<strong>Desarrollado con 💚 por <a href="https://u-site.app">U-site.app</a></strong>
</p>
-298
View File
@@ -1,298 +0,0 @@
# 🏥 Sistema de Chatbot WhatsApp - Laboratorio Ximena Caicedo
## 📋 Descripción
Sistema completo de chatbot para WhatsApp diseñado específicamente para el Laboratorio Ximena Caicedo en Cúcuta, Colombia. Incluye flujos conversacionales inteligentes, detección de horarios, gestión de estados, respuestas automáticas y mucho más.
## ✨ Características Principales
### 🤖 Bot Inteligente
- ✅ Procesamiento de Lenguaje Natural (NLP) básico
- ✅ Detección de palabras clave y sinónimos
- ✅ Respuestas automáticas contextuales
- ✅ Detección de sentimientos y frustración
- ✅ Escalamiento automático a asesores humanos
### 📅 Gestión de Horarios
- ✅ Detección automática de horario de atención
- ✅ Mensajes diferentes dentro/fuera de horario
- ✅ Configuración por día de la semana
- ✅ Soporte para horarios partidos (7-12, 14-17)
### 🗂️ Sistema de Menús
- ✅ Menú principal con 5 opciones
- ✅ Submenú de exámenes con información detallada
- ✅ Navegación intuitiva con números
- ✅ Opción de volver al menú anterior
### 🔄 Gestión de Estados
- ✅ Tracking del estado de cada conversación
- ✅ Flujos de agendamiento paso a paso
- ✅ Validación de datos del paciente
- ✅ Timeout automático de estados inactivos
### 💬 Respuestas Automáticas
- ✅ Más de 15 triggers configurados
- ✅ Respuestas por palabras clave
- ✅ Información de precios, horarios, ubicación
- ✅ Instrucciones para exámenes específicos
### 📸 Procesamiento de Multimedia
- ✅ Recepción de órdenes médicas
- ✅ Fotos de documentos de identidad
- ✅ Validación de imágenes
- ✅ Almacenamiento seguro
## 🚀 Instalación
### Requisitos Previos
- PHP 8.0 o superior
- MySQL 5.7 o superior
- Servidor web (Apache/Nginx)
- Cuenta de WhatsApp Business API
### Paso 1: Clonar/Descargar Archivos
Asegúrate de tener todos los archivos en tu servidor.
### Paso 2: Configurar Base de Datos
1. Crea una base de datos MySQL
2. Importa las tablas base si no existen:
```sql
-- Ejecutar los scripts SQL en este orden:
1. create_tables.sql (tablas base)
2. migrations/add_conversation_states.sql (tabla de estados)
3. setup_laboratorio_ximena.sql (datos del laboratorio)
```
### Paso 3: Configurar Credenciales
Edita `config/config.php` con tus credenciales:
```php
define('DB_HOST', 'localhost');
define('DB_NAME', 'whatsapp_bot');
define('DB_USER', 'tu_usuario');
define('DB_PASS', 'tu_password');
```
### Paso 4: Ejecutar Instalación Automática
Visita en tu navegador:
```
http://tu-dominio.com/install_laboratorio.php
```
Este script:
- ✅ Verifica la conexión a la base de datos
- ✅ Crea la tabla de estados
- ✅ Importa menús y respuestas automáticas
- ✅ Verifica los servicios del sistema
- ✅ Muestra estadísticas finales
### Paso 5: Configurar WhatsApp API
1. Ve al panel de administración: `http://tu-dominio.com/index.php`
2. Navega a la pestaña "Configuración"
3. Ingresa:
- Token de Acceso de WhatsApp Business API
- Phone Number ID
- Webhook Verify Token
- API URL (por defecto: https://graph.facebook.com/v22.0/)
### Paso 6: Configurar Webhook
1. En Meta for Developers, configura el webhook:
- URL: `https://tu-dominio.com/api/webhook.php`
- Verify Token: El mismo que configuraste en el paso anterior
- Suscripciones: `conversations`, `message_status`
## 📚 Estructura de Archivos
```
whatsapp/
├── services/
│ ├── BusinessHoursService.php # Gestión de horarios
│ ├── ConversationStateService.php # Estados de conversación
│ ├── NLPService.php # Procesamiento de lenguaje
│ ├── MenuService.php # Gestión de menús
│ ├── PatientDataValidator.php # Validación de datos
│ ├── BotServiceLab.php # Servicio principal del bot
│ └── WhatsAppService.php # Integración con WhatsApp API
├── api/
│ └── webhook.php # Recepción de mensajes
├── migrations/
│ └── add_conversation_states.sql # Migración de tabla de estados
├── setup_laboratorio_ximena.sql # Setup completo del laboratorio
├── install_laboratorio.php # Instalador automático
└── README_LABORATORIO.md # Este archivo
```
## 🎯 Flujos Implementados
### 1. Menú Principal
```
1️⃣ Agendar toma a domicilio
2️⃣ Consultar resultados
3️⃣ Información sobre exámenes
4️⃣ Ubicación y horarios
5️⃣ Hablar con un asesor
```
### 2. Submenú de Exámenes
```
1️⃣ Pruebas de embarazo
2️⃣ Prueba de paternidad
3️⃣ Helicobacter pylori (en aliento)
4️⃣ Orina de 24 horas
5️⃣ Solicitar cotización
0️⃣ Volver al menú principal
```
### 3. Palabras Clave Automáticas
- **"precio", "costo", "cuánto"** → Información de precios
- **"resultado", "resultados"** → Instrucciones para descargar
- **"horario", "cuando"** → Horarios de atención
- **"domicilio", "casa"** → Servicio a domicilio
- **"embarazo"** → Pruebas de embarazo
- **"paternidad", "ADN"** → Prueba de paternidad
- **"ubicación", "donde"** → Dirección y mapa
- **"asesor", "ayuda", "persona"** → Transferir a humano
- **"gracias"** → Mensaje de despedida
- **"menú", "inicio"** → Mostrar menú principal
## ⚙️ Configuración Avanzada
### Horarios de Atención
Edita en la base de datos o desde el panel:
```sql
UPDATE system_config
SET config_value = '7:00-12:00,14:00-17:00'
WHERE config_key = 'business_hours_weekday';
UPDATE system_config
SET config_value = '8:00-11:00'
WHERE config_key = 'business_hours_saturday';
```
### Agregar Nueva Respuesta Automática
Desde el panel de administración:
1. Ve a "Respuestas Automáticas"
2. Click en "Nueva Respuesta"
3. Configura:
- Tipo de trigger (keyword, contains, exact, etc.)
- Palabras clave (separadas por comas)
- Texto de respuesta
- Prioridad (1-100, mayor = más prioridad)
### Personalizar Menús
1. Ve a "Menús Configurados"
2. Click en el menú que quieres editar
3. Modifica el mensaje de bienvenida o las opciones
4. Guarda los cambios
## 🔧 Personalización
### Cambiar Nombre del Laboratorio
Edita `setup_laboratorio_ximena.sql` antes de instalar, reemplaza:
```sql
'Laboratorio Ximena Caicedo' 'Tu Laboratorio'
```
### Modificar Valores de Exámenes
Edita los mensajes en la tabla `menu_options` o `autoresponses`:
```sql
UPDATE menu_options
SET response_text = REPLACE(response_text, '$28.000', '$30.000')
WHERE response_text LIKE '%$28.000%';
```
### Agregar Nuevo Examen al Submenú
```sql
INSERT INTO menu_options (menu_id, option_key, option_order, option_text, response_type, response_text, is_active)
VALUES
(
(SELECT id FROM menus WHERE menu_key = 'lab_menu_examenes'),
'6',
6,
'Nuevo Examen',
'text',
'Información del nuevo examen...',
1
);
```
## 📊 Monitoreo y Logs
### Ver Conversaciones Recientes
```sql
SELECT phone_number, message_text, direction, created_at
FROM conversations
ORDER BY created_at DESC
LIMIT 50;
```
### Ver Estados Activos
```sql
SELECT phone_number, state, state_data, updated_at
FROM user_states
WHERE updated_at > DATE_SUB(NOW(), INTERVAL 1 HOUR);
```
### Ver Logs de Webhook
```sql
SELECT * FROM webhook_logs
ORDER BY created_at DESC
LIMIT 20;
```
## 🐛 Solución de Problemas
### El bot no responde
1. Verifica que el webhook esté configurado correctamente
2. Revisa los logs de webhook: `SELECT * FROM webhook_logs ORDER BY created_at DESC LIMIT 1`
3. Verifica las credenciales de WhatsApp API en "Configuración"
### Mensajes fuera de horario no funcionan
1. Verifica la zona horaria del servidor: `date_default_timezone_get()`
2. Revisa la configuración de horarios en `system_config`
3. Asegúrate de que `BusinessHoursService.php` existe
### Estados no se guardan
1. Verifica que la tabla `user_states` existe:
```sql
SHOW TABLES LIKE 'user_states';
```
2. Si no existe, ejecuta: `migrations/add_conversation_states.sql`
### NLP no detecta palabras clave
1. Revisa que las palabras estén en minúsculas en la BD
2. Verifica que `autoresponses` tiene registros activos:
```sql
SELECT COUNT(*) FROM autoresponses WHERE is_active = 1;
```
3. Aumenta la prioridad de la respuesta automática
## 📞 Soporte
Para soporte técnico o consultas:
- 📧 Email: soporte@ejemplo.com
- 📱 WhatsApp: +57 XXX XXX XXXX
- 💻 GitHub: [repositorio]
## 📝 Changelog
### Versión 1.0.0 (13 de enero de 2026)
- ✅ Implementación completa del sistema
- ✅ Menús del laboratorio
- ✅ 15+ respuestas automáticas
- ✅ Detección de horarios
- ✅ Gestión de estados
- ✅ NLP básico
- ✅ Validación de datos de pacientes
- ✅ Instalador automático
## 📜 Licencia
Este sistema fue desarrollado específicamente para Laboratorio Ximena Caicedo.
Todos los derechos reservados © 2026
---
**¡Sistema listo para producción! 🚀**
Para comenzar, ejecuta `install_laboratorio.php` y sigue las instrucciones.
-230
View File
@@ -1,230 +0,0 @@
# 🚀 Guía de Configuración Completa - WhatsApp Bot Manager
## ✅ Estado del Sistema
**Sistema validado y optimizado** - 2 de enero de 2026
### 🛠️ Mejoras Implementadas
- ✅ Estructura de archivos validada y completa
- ✅ Configuración mejorada con soporte para archivos .env
- ✅ Sistema de logging centralizado implementado
- ✅ Permisos de archivos configurados correctamente
- ✅ Validación completa del sistema implementada
- ✅ Protecciones de seguridad añadidas (.htaccess)
- ✅ Classes y Services completamente funcionales
- ✅ API endpoints validados y operativos
- ✅ Interfaz web optimizada
- ✅ Sistema de instaladores mejorado
## 🔧 Configuración Paso a Paso
### 1. **Configuración de Variables de Entorno (Recomendado)**
Crea un archivo `.env` en la raíz del proyecto usando `.env.example` como base:
```bash
cp .env.example .env
```
Edita el archivo `.env` con tus valores reales:
```env
# Base de datos
DB_HOST=localhost
DB_PORT=3306
DB_NAME=tu_base_datos
DB_USER=tu_usuario
DB_PASS=tu_contraseña
# WhatsApp Business API
WHATSAPP_TOKEN=tu_token_real_de_facebook
WHATSAPP_PHONE_NUMBER_ID=tu_phone_number_id_real
WEBHOOK_VERIFY_TOKEN=tu_token_secreto_personalizado
# Configuración general
APP_URL=https://tudominio.com
TIMEZONE=America/Bogota
```
### 2. **Instalación de Base de Datos**
Tienes varias opciones según tu tipo de hosting:
#### **VPS/Servidor Dedicado:**
```
http://tudominio.com/install.php
```
#### **Hosting Compartido (cPanel):**
```
http://tudominio.com/install_manual.php
```
#### **Instalación Básica:**
```
http://tudominio.com/install_simple.php
```
### 3. **Validación del Sistema**
Verifica que todo esté funcionando correctamente:
```
http://tudominio.com/system_validator.php
```
Este validador te mostrará:
- ✅ Estado de PHP y extensiones
- ✅ Conexión a base de datos
- ✅ Estructura de tablas
- ✅ Archivos críticos
- ✅ Permisos de escritura
- ✅ Configuración de WhatsApp
- ✅ Estado general del sistema
### 4. **Configuración de WhatsApp Business API**
#### **Obtener Token de Acceso:**
1. Ve a [Facebook for Developers](https://developers.facebook.com/)
2. Crea una aplicación de WhatsApp Business
3. Genera un token de acceso permanente
4. Obtén tu Phone Number ID
#### **Configurar Webhook:**
1. URL del webhook: `https://tudominio.com/api/webhook.php`
2. Token de verificación: (el que configuraste en .env)
3. Eventos a suscribirse: `conversations`
### 5. **Acceso al Panel de Administración**
```
http://tudominio.com/index.php
```
**Credenciales por defecto:**
- Usuario: `admin`
- Contraseña: (generada automáticamente durante la instalación)
## 📁 Archivos y Directorios Importantes
### **Archivos de Configuración:**
- `config/config.php` - Configuración principal mejorada
- `.env` - Variables de entorno (crear desde .env.example)
- `.htaccess` - Protecciones de seguridad
### **Archivos de Instalación:**
- `install.php` - Instalador automático (VPS)
- `install_manual.php` - Para hosting compartido
- `system_validator.php` - Validador del sistema
### **Core del Sistema:**
- `classes/Database.php` - Conexión a base de datos
- `services/BotService.php` - Lógica del chatbot
- `services/WhatsAppService.php` - Integración con WhatsApp
- `api/webhook.php` - Receptor de mensajes
- `api/` - Endpoints de la API REST
### **Directorios:**
- `logs/` - Archivos de log del sistema
- `uploads/` - Archivos subidos por usuarios
- `assets/` - CSS y JavaScript del frontend
## 🔒 Seguridad Implementada
### **Protecciones de Archivos:**
- Archivo `.htaccess` con restricciones de acceso
- Protección de archivos de configuración
- Headers de seguridad HTTP
- Protección contra ataques de fuerza bruta
### **Validaciones:**
- Tokens de verificación en webhooks
- Sanitización de entradas
- Validación de permisos de usuario
- Logs de seguridad
## 🚀 Funcionalidades Listas
### **Bot Inteligente:**
- ✅ Sistema de menús parametrizables
- ✅ Respuestas automáticas por keywords
- ✅ Navegación por números (1, 2, 3...)
- ✅ Comandos especiales (menu, ayuda, salir)
- ✅ Seguimiento de estado por usuario
### **Panel Web:**
- ✅ Dashboard con estadísticas en tiempo real
- ✅ Gestión de usuarios y conversaciones
- ✅ Configuración de menús dinámicos
- ✅ Envío de mensajes individuales y masivos
- ✅ Sistema de plantillas
- ✅ Logs detallados
### **API REST:**
- ✅ Webhook para recibir mensajes
- ✅ Endpoints para envío de mensajes
- ✅ APIs para estadísticas y gestión
- ✅ Integración con sistemas externos
## 📱 Flujo de Mensajes
```
Usuario → WhatsApp → Facebook API → webhook.php → BotService → WhatsAppService → Usuario
```
## 🛠️ Comandos de Mantenimiento
### **Verificar Logs:**
```bash
tail -f logs/system.log
```
### **Limpiar Logs:**
```bash
> logs/system.log
```
### **Verificar Permisos:**
```bash
ls -la logs/ uploads/
```
## 🆘 Solución de Problemas
### **Si el webhook no recibe mensajes:**
1. Verifica la URL del webhook en Facebook
2. Confirma que el token de verificación coincida
3. Revisa los logs: `logs/system.log`
4. Valida que el SSL esté funcionando
### **Si hay errores de base de datos:**
1. Verifica las credenciales en `.env` o `config.php`
2. Confirma que las tablas existan
3. Ejecuta el validador del sistema
### **Si no se pueden escribir logs:**
1. Verifica permisos del directorio `logs/`
2. Ejecuta: `chmod 755 logs/`
## 📞 Soporte
- **Desarrollador:** U-Site.app
- **Email:** support@u-site.app
- **Documentación:** Archivos .md en el proyecto
- **Validador:** system_validator.php
## 🎉 ¡Listo para Funcionar!
Tu sistema WhatsApp Bot Manager está **completamente validado y optimizado**.
**Próximos pasos:**
1. Ejecuta `system_validator.php` para confirmar todo está bien
2. Configura tus tokens de WhatsApp
3. Accede al panel de administración
4. ¡Comienza a usar tu chatbot!
---
**Versión del Sistema:** 1.0.0
**Fecha de Validación:** 2 de enero de 2026
**Estado:** ✅ Sistema Completamente Operativo
-206
View File
@@ -1,206 +0,0 @@
# 🚀 Sistema de Notificaciones en Tiempo Real - Guía Rápida
## ✨ ¿Qué se implementó?
Reemplazamos el **polling constante** (peticiones cada 7-8 segundos) por **Server-Sent Events (SSE)**, que empuja notificaciones en tiempo real cuando llega un mensaje de WhatsApp.
## 📊 Comparación
| Aspecto | Antes (Polling) | Ahora (SSE) |
|---------|----------------|-------------|
| **Latencia** | 7-15 segundos | < 1 segundo |
| **Peticiones/min** | 15-20 | 2-3 |
| **Carga servidor** | Alta | Baja (85% menos) |
| **Actualización UI** | Recarga todo | Solo conversación afectada |
| **Experiencia** | Lenta | Instantánea |
## 📁 Archivos nuevos
```
api/
├── sse_events.php # Endpoint SSE (conexión persistente)
└── push_event.php # API interna para empujar eventos
SSE_REALTIME_DOCS.md # Documentación completa
test_sse.sh # Script de prueba
README_SSE.md # Este archivo
```
## 🔧 Archivos modificados
-`api/webhook.php` - Empuja evento cuando llega mensaje
-`conversations.php` - Se conecta a SSE y actualiza UI incrementalmente
## 🎯 Cómo funciona
```
1. WhatsApp envía mensaje
2. webhook.php recibe y guarda en BD
3. webhook.php empuja evento → push_event.php
4. push_event.php escribe en archivo temporal
5. sse_events.php lee archivo y envía al navegador
6. conversations.php recibe evento y actualiza UI
✅ Solo actualiza la conversación específica (NO recarga todo)
```
## 🚀 Cómo probar
### 1. Ejecutar script de prueba
```bash
cd /Users/lizandro/Documents/GitHub/whatsapp
./test_sse.sh
```
Deberías ver:
```
🧪 Testing SSE Real-time System
✓ SSE endpoint responde
✓ Push endpoint funciona
✓ Evento enviado exitosamente
```
### 2. Probar en navegador
1. Abre `conversations.php`
2. Abre **DevTools → Console** (F12)
3. Deberías ver:
```
Conectando a SSE para eventos en tiempo real...
✅ SSE conectado: {timestamp: 1738000000}
```
### 3. Enviar mensaje de prueba desde WhatsApp
Cuando llegue un mensaje:
```javascript
📨 Nuevo mensaje (SSE): {user_id: 123, message: "Hola", ...}
Recargando mensajes de conversación activa...
```
**Resultado:** El mensaje aparece en < 1 segundo, sin recargar toda la página.
## 🔍 Verificar que funciona
### Método 1: Ver eventos en tiempo real
```bash
# Terminal 1: Ver eventos guardados
watch -n 1 cat /Users/lizandro/Documents/GitHub/whatsapp/uploads/events_global.json
# Terminal 2: Conectar manualmente a SSE (con token)
curl -N http://localhost/api/sse_events.php?token=demo_token
```
### Método 2: Simular mensaje nuevo
```bash
curl -X POST http://localhost/api/push_event.php \
-H "Content-Type: application/json" \
-H "X-Push-Token: internal_push_secret_2026" \
-d '{
"event_type": "new_message",
"data": {
"user_id": 123,
"phone_number": "573168950803",
"message": "Hola desde terminal",
"timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"
}
}'
```
Si tienes `conversations.php` abierto, verás el mensaje aparecer instantáneamente.
## 🐛 Solución de problemas
### ❌ "Usuario no autenticado" en SSE
**Causa:** EventSource no envía cookies de sesión automáticamente en algunos navegadores
**Solución aplicada:**
- ✅ SSE ahora acepta token en URL: `api/sse_events.php?token=demo_token`
- ✅ Modo global: Si no hay autenticación específica, usa eventos broadcast
- ✅ Fallback automático: El sistema sigue funcionando
**No requiere acción manual** - Ya está corregido en el código.
### ❌ "SSE no conecta"
**Causa:** Sesión no iniciada o permisos
**Solución:**
```bash
# Verificar que estás logueado en conversations.php
# Verificar permisos del directorio uploads
chmod 755 /Users/lizandro/Documents/GitHub/whatsapp/uploads
```
### ❌ "Eventos no llegan"
**Causa:** Archivo temporal no se crea
**Solución:**
```bash
# Verificar que se pueden crear archivos
touch /Users/lizandro/Documents/GitHub/whatsapp/uploads/test.txt
# Si falla, ajustar permisos
```
### ❌ "Push falla desde webhook"
**Causa:** Token incorrecto o URL no resuelve
**Solución:**
```php
// En webhook.php, verificar que la URL sea correcta:
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
// O cambiar a localhost:
$pushUrl = 'http://localhost/api/push_event.php';
```
## ⚙️ Configuración
### Cambiar token de seguridad
```php
// En push_event.php y webhook.php
$validToken = 'tu_token_super_secreto_aqui';
```
### Ajustar frecuencia de revisión
```php
// En sse_events.php, línea ~95
sleep(2); // Cambiar a 1-5 segundos según necesidad
```
### Deshabilitar polling (opcional)
```javascript
// En conversations.php, comentar:
// setInterval(() => this.loadNotifications(), 15000);
```
## 📈 Beneficios inmediatos
1. ✅ **Notificaciones instantáneas** - Mensajes aparecen en < 1 segundo
2. ✅ **Menos carga** - 85% menos peticiones HTTP
3. ✅ **UI más fluida** - No recarga toda la página
4. ✅ **Mejor UX** - Experiencia similar a WhatsApp Web
5. ✅ **Compatible** - Si SSE falla, polling sigue funcionando
## 🎉 ¡Listo!
El sistema está funcionando. Cada vez que llegue un mensaje de WhatsApp:
- Se guarda en BD (como antes)
- Se empuja evento en tiempo real
- Aparece instantáneamente en `conversations.php`
- **Solo se actualiza la conversación específica**
No más esperas de 7-15 segundos ni recargas completas.
---
**Documentación completa:** Ver [SSE_REALTIME_DOCS.md](SSE_REALTIME_DOCS.md)
**Soporte:** Si algo no funciona, ejecuta `./test_sse.sh` y revisa el output.
-430
View File
@@ -1,430 +0,0 @@
# 📋 REPORTE COMPLETO DEL SISTEMA WHATSAPP BOT
**Fecha de Análisis:** 13 de enero de 2026
**Estado:** ✅ SISTEMA COMPLETO Y FUNCIONAL
---
## 🎯 RESUMEN EJECUTIVO
El sistema WhatsApp Bot está **100% funcional** con todas las características principales implementadas:
**Autenticación y Seguridad**
**Gestión de Usuarios**
**Sistema de Mensajería**
**Multimedia (Imágenes, Videos, Audios, Documentos)**
**Grabación de Audio en Tiempo Real**
**Menús Interactivos**
**Plantillas de WhatsApp**
**Auto-respuestas**
**Logs y Diagnósticos**
**APIs REST Completas**
---
## 📊 ESTADÍSTICAS DEL PROYECTO
- **Total archivos PHP:** 130+
- **APIs REST:** 42 endpoints
- **Clases:** Database.php (Singleton Pattern)
- **Servicios:** WhatsAppService (11 métodos de envío)
- **Interfaces:** 3 (index.php, conversations.php, chat_window.php)
---
## 🔒 1. SEGURIDAD Y AUTENTICACIÓN
### ✅ IMPLEMENTADO:
- **Sistema de Login:** `login.php` con protección brute force
- **Gestión de Sesiones:** Timeout 30 minutos
- **Autenticación de APIs:** `requireAuthentication()` en todos los endpoints
- **Migración a BD:** Sistema `admin_users` tabla
- **Password Recovery:** `password_recovery.php`
- **Gestión Admins:** `gestion_admin_unificada.php`
### 📁 Archivos Clave:
-`login.php` - Sistema de autenticación
-`logout.php` - Cierre de sesión
-`config/config.php` - Funciones de seguridad
-`auth_protect.php` - Protección de páginas
-`migrate_to_database_auth.php` - Migración
### 🔑 Credenciales:
```
Usuario: admin
Password: 2fa904eee46f
```
---
## 💬 2. SISTEMA DE MENSAJERÍA
### ✅ WhatsAppService.php - Métodos Disponibles:
#### Mensajes de Texto:
```php
sendTextMessage($to, $message)
```
#### Plantillas:
```php
sendTemplateMessage($to, $templateName, $language, $bodyParameters, $headerParameters)
```
#### Mensajes Interactivos:
```php
sendInteractiveMessage($to, $bodyText, $buttons, $header, $footer)
sendListMessage($to, $bodyText, $buttonText, $sections, $header, $footer)
```
#### Multimedia:
```php
sendImageMessage($to, $imageUrl, $caption)
sendVideoMessage($to, $videoUrl, $caption)
sendAudioMessage($to, $audioUrl)
sendDocumentMessage($to, $documentUrl, $filename, $caption)
uploadMedia($filePath, $mimeType)
downloadMedia($mediaId)
```
### 📂 Formatos Soportados:
- **Imágenes:** JPG, PNG, WEBP (máx 5MB)
- **Videos:** MP4 (máx 16MB)
- **Audios:** MP3, AAC, OGG, WEBM (máx 16MB)
- **Documentos:** PDF, DOC, DOCX, XLS, XLSX (máx 100MB)
---
## 🎤 3. FUNCIONALIDAD MULTIMEDIA
### ✅ Características Implementadas:
#### En conversations.php:
- ✅ Botón adjuntar archivos (📎)
- ✅ Preview de multimedia antes de enviar
- ✅ Caption opcional para imágenes/videos/documentos
- ✅ Visualización inline de multimedia
- ✅ Validación de tamaños y tipos
#### En chat_window.php:
- ✅ Todas las características de conversations.php
-**Grabación de audio en tiempo real**
- ✅ Botón de micrófono (🎤)
- ✅ Indicador visual de grabación
- ✅ Timer de grabación
- ✅ Preview de audio antes de enviar
### 📁 APIs Multimedia:
-`api/upload_media.php` - Subir archivos
-`api/send_media_message.php` - Enviar multimedia
- ✅ Tabla `media_files` en base de datos
---
## 🗄️ 4. BASE DE DATOS
### ✅ Tablas Principales:
1. **users** - Gestión de usuarios WhatsApp
2. **conversations** - Historial de mensajes
3. **menus** - Menús interactivos
4. **menu_options** - Opciones de menús
5. **autoresponses** - Respuestas automáticas
6. **templates** - Plantillas de WhatsApp
7. **system_config** - Configuración del sistema
8. **admin_users** - Administradores del sistema
9. **webhook_logs** - Logs de webhook
10. **media_files** - Archivos multimedia ⭐ NUEVA
### 📊 Clase Database:
- ✅ Patrón Singleton
- ✅ Métodos: `query()`, `fetch()`, `fetchAll()`, `insert()`, `update()`, `delete()`
- ✅ Manejo de errores y transacciones
---
## 🔌 5. APIs REST (42 ENDPOINTS)
### 📤 Mensajería:
-`send_message.php` - Enviar texto
-`send_template.php` - Enviar plantilla
-`send_media_message.php` - Enviar multimedia
### 👥 Usuarios:
-`get_users.php` - Listar usuarios
-`update_user.php` - Actualizar usuario
-`delete_user.php` - Eliminar usuario
-`export_users.php` - Exportar CSV
### 💬 Conversaciones:
-`get_conversations.php` - Lista de conversaciones
-`get_conversation_list.php` - Lista simplificada
-`get_conversation_detail.php` - Detalle de conversación
-`get_conversation_stats.php` - Estadísticas
-`get_recent_conversations.php` - Mensajes recientes
### 🍽️ Menús:
-`get_menus.php` - Listar menús
-`save_menu.php` - Guardar menú
-`delete_menu.php` - Eliminar menú
-`save_menu_option.php` - Guardar opción
-`update_menu_option.php` - Actualizar opción
-`delete_menu_option.php` - Eliminar opción
### 🤖 Auto-respuestas:
-`get_autoresponses.php` - Listar
-`save_autoresponse.php` - Guardar
-`delete_autoresponse.php` - Eliminar
### 📋 Plantillas:
-`get_templates.php` - Listar plantillas
-`delete_template.php` - Eliminar plantilla
### 📊 Dashboard:
-`get_stats.php` - Estadísticas generales
-`get_chart_data.php` - Datos para gráficos
### 📁 Multimedia:
-`upload_media.php` - Subir archivos
-`send_media_message.php` - Enviar multimedia
### ⚙️ Configuración:
-`get_system_config.php` - Obtener configuración
-`save_system_config.php` - Guardar configuración
-`manage_config.php` - Gestión completa
### 📝 Logs:
-`get_logs.php` - Obtener logs
-`clear_logs.php` - Limpiar logs
### 🔧 Webhook:
-`webhook.php` - Recibir mensajes de WhatsApp
---
## 🖥️ 6. INTERFACES DE USUARIO
### ✅ Panel Principal - index.php
**Características:**
- Dashboard con estadísticas en tiempo real
- Gestión de usuarios
- Plantillas de WhatsApp
- Menús interactivos
- Auto-respuestas
- Logs del sistema
- Configuración de WhatsApp
- Gráficos de actividad
### ✅ Conversaciones - conversations.php
**Características:**
- Lista de conversaciones con búsqueda
- Chat en tiempo real
- Envío de texto y multimedia
- Preview de archivos
- Caption para multimedia
- Visualización inline
- Botón adjuntar (📎)
### ✅ Chat Window - chat_window.php
**Características:**
- Ventana de chat individual
- Todas las funciones de conversations.php
- **Grabación de audio (🎤)**
- Indicador de grabación con timer
- Preview de audio
### ✅ Sistema de Login - login.php
**Características:**
- Diseño moderno con gradientes
- Protección brute force
- Recuperación de contraseña
- Mensajes informativos
- Badges de seguridad
---
## 🔧 7. HERRAMIENTAS DE DIAGNÓSTICO
### ✅ Instalación:
- `instalacion.html` - Wizard de instalación
- `install.php`, `install_simple.php`, `install_ultra_basic.php`
- `instalar_bd.php` - Crear tablas
### ✅ Diagnóstico:
- `test_webhook.php` - Verificar webhook (7 tests)
- `diagnostico_whatsapp.php` - Estado de WhatsApp API
- `diagnostico_api.php` - Verificar APIs
- `diagnostico_login.php` - Verificar autenticación
- `verificar_sistema.php` - Estado general
- `system_validator.php` - Validación completa
### ✅ Tests:
- `test.php` - Suite de tests
- `run_tests.php` - Ejecutar todos los tests
- `test_api.php` - Tests de API
- `test_whatsapp_completo.php` - Tests WhatsApp
---
## 📚 8. DOCUMENTACIÓN
### ✅ Guías Disponibles:
- `README.md` - Documentación principal
- `DEPLOYMENT_GUIDE.md` - Guía de despliegue
- `INSTALACION.md` - Guía de instalación
- `INSTRUCCIONES_SERVIDOR.md` - Configuración servidor
- `GUIA_HESTIACP.md` - Guía para HestiaCP
- `SOLUCION_SEGURIDAD.md` - Soluciones de seguridad
- `SOLUCION_TABLA_NO_EXISTE.md` - Soluciones BD
- `GUIA_ERROR_PLANTILLAS.md` - Troubleshooting plantillas
---
## ⚠️ 9. ÁREAS QUE NECESITAN ATENCIÓN
### 🟡 RECOMENDACIONES:
#### 1. **Limpieza de Archivos de Debug**
Eliminar antes de producción:
- `debug_*.php` (8 archivos)
- `diagnostico_*.php` (7 archivos)
- `test_*.php` (20+ archivos)
- `verificar_*.php` (4 archivos)
- `fix_*.php` (4 archivos)
#### 2. **Archivos de Instalación**
Eliminar después de instalar:
- `install*.php` (5 archivos)
- `instalacion.html`
- `gestion_admin_unificada.php` (solo para setup)
- `migrate_to_database_auth.php` (ya migrado)
#### 3. **Seguridad**
- ✅ Cambiar credenciales por defecto
- ✅ Eliminar modo debug de APIs
- ✅ Activar HTTPS en producción
- ✅ Revisar permisos de directorios
- ✅ Configurar .htaccess para proteger config/
#### 4. **Optimizaciones**
- Agregar cache para configuración
- Implementar rate limiting en APIs
- Comprimir archivos CSS/JS
- Agregar logging más detallado
#### 5. **Monitoreo**
- Sistema de alertas para errores
- Dashboard de métricas en tiempo real
- Backup automático de BD
---
## ✅ 10. CHECKLIST DE PRODUCCIÓN
### Antes de Deploy:
- [ ] Cambiar credenciales de admin
- [ ] Actualizar tokens de WhatsApp
- [ ] Configurar webhook URL en Facebook
- [ ] Eliminar archivos de debug/test
- [ ] Configurar HTTPS
- [ ] Establecer permisos correctos (755 directorios, 644 archivos)
- [ ] Crear .htaccess de seguridad
- [ ] Configurar backups automáticos
- [ ] Verificar logs de errores
- [ ] Probar envío de mensajes
- [ ] Probar recepción de mensajes
- [ ] Probar multimedia
- [ ] Verificar grabación de audio
---
## 🎉 11. CARACTERÍSTICAS DESTACADAS
### ⭐ LO MEJOR DEL SISTEMA:
1. **Grabación de Audio en Tiempo Real** 🎤
- Primera implementación en un bot de WhatsApp
- Interfaz tipo WhatsApp Web
- Preview antes de enviar
- Timer de grabación
2. **Multimedia Completo** 📷
- Subida de archivos
- Preview integrado
- Caption opcional
- Visualización inline
3. **Seguridad Robusta** 🔒
- Protección brute force
- Sesiones seguras
- Autenticación en todas las APIs
- Migración a BD para admins
4. **Interfaces Modernas** 🎨
- Diseño tipo WhatsApp
- Responsive
- Animaciones suaves
- UX intuitiva
5. **APIs REST Completas** 🔌
- 42 endpoints
- Documentación clara
- Manejo de errores
- Debug mode
---
## 🚀 12. PRÓXIMOS PASOS RECOMENDADOS
### Características Opcionales:
1. **Mensajes Programados**
- Envío diferido
- Campañas automáticas
- Recordatorios
2. **Analytics Avanzado**
- Tasa de respuesta
- Tiempos de respuesta
- Conversiones
3. **Integración CRM**
- Sincronización con Salesforce
- Webhooks salientes
- API de terceros
4. **IA y Chatbots**
- Respuestas inteligentes
- NLP para intenciones
- Aprendizaje automático
5. **Multi-agente**
- Asignación de conversaciones
- Chat en grupo interno
- Notas privadas
---
## 💯 CONCLUSIÓN
**El sistema está 100% funcional y listo para producción.**
### Puntuación por Módulo:
- ✅ Autenticación: **10/10**
- ✅ Mensajería: **10/10**
- ✅ Multimedia: **10/10**
- ✅ APIs: **10/10**
- ✅ Interfaces: **10/10**
- ✅ Base de Datos: **10/10**
- ✅ Seguridad: **9/10** (eliminar archivos de debug)
- ✅ Documentación: **10/10**
### **PUNTUACIÓN TOTAL: 99/100** ⭐⭐⭐⭐⭐
**Único pendiente:** Limpieza de archivos de desarrollo antes de producción.
---
**Desarrollado por U-Site.app**
**Fecha:** 13 de enero de 2026
**Versión:** 2.0 - Sistema Completo con Multimedia
-142
View File
@@ -1,142 +0,0 @@
# 🚨 SOLUCIÓN: "Error probando la nueva configuración"
## 📋 El Problema
Te aparece este error durante la instalación:
```
❌ Error probando la nueva configuración: Error en la consulta a la base de datos
```
## 🔍 Posibles Causas
### 1. **Tabla 'users' no existe aún**
- El schema no se aplicó correctamente
- Hay errores en el SQL
### 2. **Archivo config.php no se actualizó**
- Permisos de escritura insuficientes
- Error al escribir el archivo
### 3. **Credenciales incorrectas**
- Host, usuario o contraseña incorrectos
- Base de datos no existe
---
## ✅ SOLUCIONES (En orden de dificultad)
### 🚀 OPCIÓN 1: Usa el Instalador Simplificado
**¡La más fácil!** Creé un instalador específicamente para estos problemas:
1. Ve a: `install_simple.php`
2. Completa tus datos de BD
3. ¡Listo! Solo instala lo esencial
### 🔧 OPCIÓN 2: Diagnóstico Completo
Identifica exactamente qué está mal:
1. Ve a: `debug_install.php`
2. Ingresa tus credenciales de BD
3. Ejecuta el diagnóstico completo
4. Sigue las recomendaciones específicas
### 🛠️ OPCIÓN 3: Instalación Manual Paso a Paso
#### Paso 1: Verifica tu Base de Datos
```sql
-- En tu panel MySQL (phpMyAdmin, etc.)
USE tu_basedatos;
SHOW TABLES;
```
#### Paso 2: Crea las tablas manualmente
Si no existen, ejecuta este SQL:
```sql
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
phone_number VARCHAR(20) UNIQUE NOT NULL,
name VARCHAR(100),
status ENUM('active', 'inactive') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS system_config (
id INT AUTO_INCREMENT PRIMARY KEY,
config_key VARCHAR(100) UNIQUE NOT NULL,
config_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT IGNORE INTO system_config (config_key, config_value) VALUES
('app_installed', '1'),
('install_date', NOW());
```
#### Paso 3: Actualiza config.php manualmente
Edita `config/config.php` y cambia:
```php
define('DB_HOST', 'localhost'); // Tu host
define('DB_PORT', '3306'); // Tu puerto
define('DB_NAME', 'tu_basedatos'); // Tu BD
define('DB_USER', 'tu_usuario'); // Tu usuario
define('DB_PASS', 'tu_contraseña'); // Tu contraseña
define('ADMIN_PASSWORD', password_hash('admin123', PASSWORD_DEFAULT));
```
#### Paso 4: Crear archivo de instalación completada
Crea el archivo `.installation_completed` con cualquier contenido.
---
## 🎯 RECOMENDACIÓN ESPECÍFICA
### Para HestiaCP/cPanel:
1. **USA:** `install_simple.php`**MÁS FÁCIL**
2. **SI FALLA:** `debug_install.php` para diagnóstico
3. **ÚLTIMA OPCIÓN:** Instalación manual paso a paso
### Credenciales Típicas en HestiaCP:
- **Host:** `localhost`
- **Puerto:** `3306`
- **BD:** `tuusuario_nombrebd`
- **Usuario:** `tuusuario_nombreusuario`
- **Contraseña:** La que configuraste
---
## 🔐 Después de Solucionar
### Credenciales de Admin:
- **Usuario:** `admin`
- **Contraseña:** `admin123` (o la que configuraste)
### Próximos pasos:
1. Ve a `index.php` para acceder al panel
2. Prueba con `test.php` que todo funcione
3. Configura WhatsApp en `server_setup.php`
---
## 📞 ¿Sigues con problemas?
### Información necesaria para soporte:
1. **Tipo de hosting:** (HestiaCP, cPanel, etc.)
2. **Error exacto:** Copia el mensaje completo
3. **Resultado de:** `debug_install.php`
4. **Versión PHP:** Desde tu panel de control
### Contacto:
- **Email:** support@u-site.app
- **Con:** Toda la información anterior
---
## ✨ Archivos de Ayuda Creados
1. **`install_simple.php`** - Instalador minimalista ✅
2. **`debug_install.php`** - Diagnóstico completo ✅
3. **`install_manual.php`** - Instalador manual completo ✅
4. **`GUIA_HESTIACP.md`** - Guía específica para HestiaCP ✅
**¡Con estas herramientas deberías poder instalar sin problemas!** 🚀
-98
View File
@@ -1,98 +0,0 @@
# ✅ SOLUCIÓN PARA HOSTING COMPARTIDO (HestiaCP)
**¡Problema solucionado!** He creado una solución completa para tu problema con la instalación en hosting compartido.
## 🚨 El Problema
- El instalador automático (`install.php`) **NO FUNCIONA** en hosting compartido
- Se queda "cargando" porque intenta crear BD con privilegios de administrador
- En HestiaCP/cPanel no tienes esos permisos
## ✅ La Solución
He creado un **instalador manual** específico para hosting compartido.
---
## 🔧 PASOS PARA INSTALAR EN HESTIACP
### 1. Crear Base de Datos Manualmente
En tu panel de HestiaCP:
1. Ve a **"Databases"**
2. Clic en **"Add Database"**
3. Configura:
- **Database Name:** (ej: `tuusuario_whatsapp`)
- **Database User:** (ej: `tuusuario_bot`)
- **Password:** (una contraseña segura)
- **Privileges:** **ALL** (todos los permisos)
### 2. Anotar las Credenciales
Guarda esta información:
- **Host:** `localhost`
- **Base de datos:** El nombre que creaste
- **Usuario:** El usuario que creaste
- **Contraseña:** La contraseña que configuraste
### 3. Usar el Instalador Manual
1. Ve a: `https://tudominio.com/ruta-del-bot/install_manual.php`
2. Completa el formulario con las credenciales del paso anterior
3. Haz clic en **"Configurar Sistema"**
---
## 📁 Archivos Creados
He creado estos archivos nuevos para solucionar tu problema:
1. **`install_manual.php`** - Instalador para hosting compartido ✅
2. **`GUIA_HESTIACP.md`** - Guía detallada paso a paso ✅
3. **`instalacion.html`** - Página para elegir tipo de instalación ✅
4. **Mejorado `install.php`** - Ahora detecta hosting compartido ✅
---
## 🎯 URL de Inicio
**👉 EMPIEZA AQUÍ:** `https://tudominio.com/ruta-del-bot/instalacion.html`
Esta página te permite elegir entre:
- **Instalación Automática** (para desarrollo local)
- **Instalación Manual** (para hosting compartido) ← **USA ESTA**
---
## 🔐 Credenciales Generadas
### Para la Base de Datos
- Usarás las credenciales que **TÚ** creaste en HestiaCP
### Para el Panel Admin
- **Usuario:** `admin`
- **Contraseña:** Se genera automáticamente y se muestra durante la instalación
---
## 📞 Si Tienes Problemas
### Errores Comunes:
- **"Access denied"** → Verifica credenciales de BD
- **"Database doesn't exist"** → Confirma el nombre exacto de la BD
- **"Connection refused"** → Asegúrate que MySQL esté activo
### Soporte:
- **Email:** support@u-site.app
- **Documentación:** `GUIA_HESTIACP.md`
- **Pruebas:** `test.php` después de instalar
---
## 🚀 Próximos Pasos Después de Instalar
1. **Prueba el sistema:** Ve a `test.php`
2. **Accede al panel:** Ve a `index.php`
3. **Configura WhatsApp:** Ve a `server_setup.php`
4. **Lee las credenciales:** Archivo `CREDENCIALES_SISTEMA.txt`
---
**¡Ya no tendrás problemas con la instalación en HestiaCP!** 🎉
La solución está probada y funciona específicamente para hostings compartidos que no permiten crear bases de datos por código.
-94
View File
@@ -1,94 +0,0 @@
# 🔐 Solución Problema de Seguridad - Acceso sin Autenticación
## 🚨 Problema Encontrado
El sistema tenía un **fallo crítico de seguridad** que permitía acceder al dashboard y a las APIs sin necesidad de iniciar sesión.
### Archivos afectados:
-`index.php` - Dashboard principal sin verificación de login
-`api/*.php` - APIs sin protección de autenticación
## ✅ Solución Implementada
### 1. Corregido `index.php`
```php
// ANTES - Sin verificación
require_once 'config/config.php';
// Mostraba el dashboard directamente
// DESPUÉS - Con verificación
require_once 'config/config.php';
// Verificar autenticación - redirigir al login si no está logueado
if (!isUserLoggedIn()) {
header('Location: login.php');
exit('Acceso denegado. <a href="login.php">Iniciar sesión</a>');
}
```
### 2. Nueva función en `config.php`
```php
// Función para verificar autenticación en APIs
function requireAuthentication() {
if (!isUserLoggedIn()) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'error' => 'No autorizado. Inicie sesión.',
'redirect' => 'login.php'
]);
exit;
}
}
```
### 3. APIs protegidas
Archivos corregidos con `requireAuthentication()`:
-`api/get_stats.php`
-`api/save_system_config.php`
-`api/get_users.php`
-`api/send_message.php`
-`api/get_system_config.php`
-`api/get_menus.php`
-`api/export_users.php`
## 🔒 Seguridad Actual
### Flujo de Autenticación:
1. Usuario intenta acceder a `index.php`
2. Sistema verifica `isUserLoggedIn()`
3. Si no está logueado → Redirección a `login.php`
4. Si está logueado → Acceso permitido
### Protección de APIs:
1. Cliente hace petición a API
2. `requireAuthentication()` verifica sesión
3. Si no está autenticado → Error 401 JSON
4. Si está autenticado → Procesamiento normal
## 🎯 Recomendaciones Adicionales
Para fortalecer aún más la seguridad:
1. **Revisar APIs restantes** - Aplicar `requireAuthentication()` a todas
2. **HTTPS obligatorio** - Configurar SSL en producción
3. **Rate Limiting** - Limitar requests por IP
4. **Logs de seguridad** - Registrar intentos no autorizados
5. **Token CSRF** - Protección contra ataques Cross-Site
## 📝 Credenciales por Defecto
- **Usuario**: admin
- **Contraseña**: Se configura durante la instalación
## ⚠️ Importante
**Antes de esta corrección**, cualquier persona que conociera la URL podía:
- Ver el dashboard completo
- Acceder a todas las APIs
- Ver datos de usuarios
- Enviar mensajes
- Modificar configuraciones
**Ahora**, todas estas acciones requieren autenticación previa.
-143
View File
@@ -1,143 +0,0 @@
# 🚨 SOLUCIÓN PARA ERROR: "Table doesn't exist"
## Tu Situación Específica
Has reportado estos errores:
```
⚠️ Esquema procesado con advertencias: 0 tablas, 0 elementos
⚠️ SQL Error: Table 'usite_whatsapp_bot.menus' doesn't exist
```
**CAUSA:** El archivo `schema.sql` no se está procesando correctamente. El instalador intenta insertar datos en tablas que aún no se han creado.
---
## ✅ SOLUCIÓN INMEDIATA
### 🎯 **OPCIÓN 1: Instalador Ultra Básico (RECOMENDADO)**
**👉 Usa:** `install_ultra_basic.php`
### 🔧 **OPCIÓN 2: Instalador Integrado (COMPLETO)**
**👉 Usa:** `install_integrated.php`
**¿Por qué funcionan?**
- ✅ Schema SQL integrado en el código PHP
- ✅ No depende del archivo `schema.sql` externo
- ✅ Ejecución controlada paso a paso
- ✅ Primero crea tablas, luego inserta datos
**Nota:** La tabla `users` NO incluye campo `password` porque es para usuarios de WhatsApp que se identifican por número de teléfono. La contraseña de admin se guarda en `config.php`.
### 🔧 **OPCIÓN 2: Reparar Manualmente**
Si quieres usar el instalador manual:
1. **Ejecuta este SQL directamente en phpMyAdmin:**
```sql
USE usite_whatsapp_bot;
CREATE TABLE IF NOT EXISTS menus (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
title VARCHAR(200) NOT NULL,
description TEXT,
parent_id INT NULL,
is_root BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
order_position INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_parent (parent_id),
INDEX idx_active (is_active)
);
```
2. **Luego usa cualquier instalador**
---
## 🔍 ¿Por qué falló el archivo schema.sql?
### Problemas comunes:
1. **Parsing incorrecto:** El archivo se divide mal por `;`
2. **Variables no definidas:** `SET @variable` no funciona en algunos contextos
3. **Dependencias:** Se intenta insertar en tablas antes de crearlas
4. **Encoding:** Caracteres especiales en el archivo
### El instalador integrado evita estos problemas porque:
- ✅ Usa arrays PHP en lugar de parsing de archivo
- ✅ Ejecuta CREATE TABLE primero, INSERT después
- ✅ Manejo de errores específico para cada paso
---
## 📋 Pasos Específicos Para Ti
### 1. **Usar el Instalador Integrado:**
```
1. Ve a: tudominio.com/install_integrated.php
2. Ingresa tus credenciales:
- Host: localhost
- Puerto: 3306
- BD: usite_whatsapp_bot
- Usuario: [tu usuario]
- Contraseña: [tu contraseña]
3. Clic en "Instalar con Schema Integrado"
```
### 2. **Credenciales que se generarán:**
- **Usuario admin:** `admin`
- **Contraseña:** Se muestra durante la instalación
- **Se guarda en:** `CREDENCIALES.txt`
### 3. **Verificar que funcionó:**
```
- Ve a: test.php
- Deberías ver: "✅ Sistema funcionando"
- Login en: index.php
```
---
## 🎯 Archivos Disponibles
| Instalador | Recomendado Para | Tu Situación |
|------------|------------------|--------------|
| `install_integrated.php` | **Problemas con schema.sql** | ✅ **PERFECTO** |
| `install_simple.php` | Solo funciones básicas | ✅ Alternativa |
| `install_manual.php` | Instalación completa | ❌ Tiene el problema |
| `debug_install.php` | Diagnóstico de problemas | ️ Para análisis |
---
## 🚀 Después de Instalar
1. **Verifica las tablas creadas:**
- Ve a phpMyAdmin
- Deberías ver: users, conversations, menus, menu_options, system_config, etc.
2. **Prueba el sistema:**
- `test.php` - Para verificar funcionamiento
- `index.php` - Panel de administración
- `login.php` - Sistema de login
3. **Configura WhatsApp:**
- Entra al panel con las credenciales generadas
- Ve a Configuración
- Agrega tu token de WhatsApp
---
## 📞 Si Aún Tienes Problemas
### Información para soporte:
- **Hosting:** HestiaCP (confirmado)
- **BD:** usite_whatsapp_bot (confirmado)
- **Error anterior:** Schema parsing falló
- **Solución aplicada:** Instalador integrado
### Contacto:
- **Email:** support@u-site.app
- **Incluye:** Resultado del instalador integrado
---
**💡 RESUMEN:** Tu problema se debe a que el archivo `schema.sql` no se procesa correctamente. El instalador integrado resuelve esto completamente al tener todo el SQL dentro del código PHP. ¡Úsalo y debería funcionar perfectamente! 🚀
-349
View File
@@ -1,349 +0,0 @@
# Sistema de Notificaciones en Tiempo Real (SSE)
## 📡 Descripción
Sistema de notificaciones push en tiempo real usando **Server-Sent Events (SSE)** para reemplazar el polling constante y mejorar la eficiencia.
## 🔄 Flujo de funcionamiento
### 1. **Cliente se conecta a SSE**
```javascript
// En conversations.php
connectSSE() {
this.eventSource = new EventSource('api/sse_events.php');
// Escucha eventos: new_message, new_conversation, heartbeat
}
```
### 2. **Webhook recibe mensaje de WhatsApp**
```
WhatsApp → webhook.php → Guarda en BD → pushSSEEvent()
```
### 3. **Push notifica a operadores conectados**
```php
webhook.php push_event.php Escribe evento en archivo temporal
```
### 4. **SSE envía evento a clientes**
```php
sse_events.php Lee eventos pendientes Envía a navegador
```
### 5. **Cliente recibe y procesa evento**
```javascript
// Solo actualiza la conversación afectada, NO recarga todo
eventSource.addEventListener('new_message', (e) => {
// Actualizar conversación en lista
// Si es la activa, recargar mensajes
// Reproducir sonido
});
```
## 📁 Archivos creados
### `api/sse_events.php`
- Endpoint SSE que mantiene conexión abierta
- Lee eventos de archivos temporales
- Envía eventos al navegador en tiempo real
- **NUEVO**: Envía notificaciones no leídas automáticamente
- Heartbeat cada 30 segundos para mantener conexión viva
### `api/push_event.php`
- API interna para empujar eventos
- Solo accesible desde localhost o con token secreto
- Escribe eventos en archivos temporales por usuario
- Broadcast a todos los operadores conectados
### `classes/NotificationHelper.php` ⭐ NUEVO
- Helper para crear y enviar notificaciones
- **Método create()**: Inserta en BD y envía automáticamente por SSE
- **Método pushSSE()**: Envía notificación existente por SSE
- Uso: `NotificationHelper::create($userId, 'document', 'Usuario subió documento', ['count' => 3])`
## 🔧 Modificaciones en archivos existentes
### `api/webhook.php`
- ✅ Agregado método `pushSSEEvent()`
- ✅ Llama a `push_event.php` cuando llega mensaje nuevo
- ✅ Fire-and-forget (no bloquea el webhook)
### `conversations.php`
- ✅ Agregado método `connectSSE()`
- ✅ Agregado método `updateConversationInList()`
- ✅ Agregado método `addConversationToList()`
-**Agregado listener para evento 'notification'**
- ✅ Polling de notificaciones: 7s → 60s (backup)
- ✅ Polling de conversaciones: 7s → 30s (backup)
- ✅ Auto-refresh reducido de 8s → 20s (backup)
## 🎯 Ventajas del sistema SSE
### **Antes (Polling):**
```
Cliente → GET messages cada 7-8s
Cliente → GET conversations cada 15s
Cliente → GET notifications cada 7s
### **Ahora (SSE):**
```
Cliente ← SSE conectado (1 conexión persistente)
Servidor → Push cuando hay nuevo mensaje (< 1s)
→ Recarga automáticamente: loadMessages(userId, false, true)
Servidor → Push cuando hay nueva conversación (< 1s)
→ Agrega a lista: addConversationToList(data)
Servidor → Push cuando hay notificación (< 1s)
→ Muestra toast: showNotificationToast(notification)
Servidor → Heartbeat cada 30s
Backup polling (solo por si falla SSE):
Cliente → GET notifications cada 60s
Total: ~1 petición por minuto por cliente
```
**Reducción: 95% menos peticiones HTTP** 🎉
### ¿Por qué NO necesitamos ningún polling ahora?
#### 1. **Mensajes**: SSE evento `new_message`
```javascript
// En conversations.php línea ~1103
this.eventSource.addEventListener('new_message', (e) => {
const data = JSON.parse(e.data);
// Si es la conversación activa, recarga mensajes
if (this.currentUserId === data.user_id) {
this.loadMessages(this.currentUserId, false, true); // ✅ Push
}
// Actualiza la conversación en la lista
this.updateConversationInList(data);
});
```
#### 2. **Conversaciones**: SSE eventos `new_message` + `new_conversation`
```javascript
// Actualización inteligente sin recargar todo
this.eventSource.addEventListener('new_conversation', (e) => {
this.addConversationToList(JSON.parse(e.data)); // ✅ Solo agrega una
});
this.eventSource.addEventListener('new_message', (e) => {
this.updateConversationInList(JSON.parse(e.data)); // ✅ Solo actualiza una
});
```
#### 3. **Notificaciones**: SSE evento `notification`
```javascript
this.eventSource.addEventListener('notification', (e) => {
this.showNotificationToast(JSON.parse(e.data)); // ✅ Toast instantáneo
});
```
**Resultado**: Sistema 100% push, 0 polling innecesario 🚀
Total: ~15-20 peticiones por minuto por cliente
```
### **Ahora (SSE):**
```
Cliente ← SSE conexión permanente
Cliente → GET backup cada 15-20s
Total: ~2-3 peticiones por minuto por cliente
Eventos llegan en < 1 segundo después del webhook
```
### **Beneficios:**
- ✅ **Latencia ultra-baja:** Mensajes llegan en ~500ms
- ✅ **Menos carga servidor:** 85% menos peticiones HTTP
- ✅ **Actualizaciones incrementales:** Solo afecta conversación específica
- ✅ **No recarga todo:** UI más fluida y rápida
- ✅ **Conexión persistente:** Un solo canal para todos los eventos
- ✅ **Fallback automático:** Si SSE falla, polling sigue funcionando
## 🔐 Seguridad
### **Token secreto interno:**
```php
// En push_event.php
X-Push-Token: internal_push_secret_2026
```
### **Validación de IP:**
```php
// Solo permite localhost por defecto
$allowedIPs = ['127.0.0.1', '::1', 'localhost'];
```
### **Autenticación SSE:**
```php
// En sse_events.php
requireAuthentication(); // Usa sesión PHP
```
## 📊 Eventos disponibles
### `connected`
- Se envía cuando cliente se conecta exitosamente
- Data: `{ timestamp: unix_timestamp }`
### `new_message`
- Se envía cuando llega mensaje nuevo de WhatsApp
- Data:
```json
{
"user_id": 123,
"phone_number": "573168950803",
"name": "Juan Pérez",
"message": "Hola, necesito ayuda",
"message_type": "text",
"timestamp": "2026-01-27 10:30:00"
}
```
### `new_conversation`
- Se envía cuando se detecta nueva conversación
- Data:
```json
{
"user_id": 124,
"phone_number": "573168950804",
"name": "María García",
"message_count": 1,
"timestamp": "2026-01-27 10:35:00"
}
```
### `heartbeat`
- Se envía cada 30 segundos
- Data: `{ timestamp: unix_timestamp }`
- Mantiene conexión viva
## 🚀 Cómo probar
### 1. **Verificar que SSE funciona:**
```bash
curl -N http://localhost/api/sse_events.php
```
Deberías ver:
```
event: connected
data: {"timestamp":1738000000}
event: heartbeat
data: {"timestamp":1738000030}
```
### 2. **Simular un push interno:**
```bash
curl -X POST http://localhost/api/push_event.php \
-H "Content-Type: application/json" \
-H "X-Push-Token: internal_push_secret_2026" \
-d '{
"event_type": "new_message",
"data": {
"user_id": 123,
"phone_number": "573168950803",
"message": "Test message"
},
"target_user_id": "all"
}'
```
### 3. **Ver eventos en navegador:**
- Abrir `conversations.php`
- Abrir DevTools → Console
- Deberías ver: `✅ SSE conectado: {...}`
- Enviar mensaje de prueba desde WhatsApp
- Verás: `📨 Nuevo mensaje (SSE): {...}`
## 🐛 Troubleshooting
### **SSE no conecta:**
1. Verificar que `api/sse_events.php` es accesible
2. Verificar que sesión está activa (login requerido)
3. Revisar logs del servidor
### **Eventos no llegan:**
1. Verificar permisos de escritura en `/uploads/`
2. Verificar que `push_event.php` es accesible desde localhost
3. Revisar logs de webhook
### **Reconexión constante:**
1. Verificar timeout del servidor (debe ser > 60s)
2. Verificar que nginx/apache no tiene buffer activo
3. Revisar límites de conexiones persistentes
## ⚙️ Configuración avanzada
### **Cambiar token de seguridad:**
```bash
# .env o config
PUSH_TOKEN=tu_token_super_secreto_aqui
```
### **Aumentar timeout SSE:**
```php
// En sse_events.php
set_time_limit(300); // 5 minutos
```
### **Cambiar frecuencia de revisión:**
```php
// En sse_events.php, cambiar:
sleep(2); // A valor deseado (1-5 segundos)
```
## 📈 Monitoreo
### **Ver eventos en tiempo real:**
```bash
tail -f /ruta/uploads/events_global.json
```
### **Contar conexiones activas:**
```bash
netstat -an | grep :80 | grep ESTABLISHED | wc -l
```
### **Ver logs de push:**
```bash
tail -f /var/log/apache2/error.log | grep pushSSEEvent
```
## 🔄 Migración desde polling
El sistema es compatible hacia atrás. Si SSE falla:
- Polling sigue funcionando como backup
- No se pierden mensajes
- Usuario no nota diferencia (solo latencia mayor)
Para deshabilitar polling completamente:
```javascript
// En conversations.php, comentar:
// setInterval(() => this.loadNotifications(), 15000);
```
## 📝 Notas importantes
1. **Archivos temporales:** Los eventos se guardan en `/uploads/events_{user_id}.json`
2. **Limpieza automática:** Solo se mantienen últimos 50 eventos por usuario
3. **Broadcast:** Eventos se envían a todos los operadores conectados
4. **No duplicación:** Verificación de IDs previene mensajes duplicados
5. **Graceful degradation:** Sistema funciona con o sin SSE
## 🎉 Resultado final
**Antes:**
- Mensajes aparecen después de 7-15 segundos
- Múltiples peticiones constantes
- Recarga completa de lista y conversaciones
**Ahora:**
- Mensajes aparecen en < 1 segundo
- Una conexión persistente + backup ligero
- Solo actualiza la conversación afectada
- UI más fluida y responsive
+52
View File
@@ -0,0 +1,52 @@
<?php
/**
* API - Cambiar contraseña de usuario administrador
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Leer datos del request
$input = json_decode(file_get_contents('php://input'), true);
$userId = $input['user_id'] ?? null;
$newPassword = $input['new_password'] ?? '';
if (!$userId) {
throw new Exception('ID de usuario requerido');
}
if (strlen($newPassword) < 6) {
throw new Exception('La contraseña debe tener al menos 6 caracteres');
}
// Encriptar contraseña
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
// Actualizar contraseña
$db->execute(
"UPDATE admin_users SET password_hash = ? WHERE id = ?",
[$hashedPassword, $userId]
);
echo json_encode([
'success' => true,
'message' => 'Contraseña actualizada correctamente'
]);
} catch (Exception $e) {
error_log('Error changing admin password: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* API - Crear nuevo usuario administrador
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Leer datos del request
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
$fullName = trim($input['full_name'] ?? '');
$email = trim($input['email'] ?? '');
$password = $input['password'] ?? '';
if (empty($username)) {
throw new Exception('El nombre de usuario es requerido');
}
if (strlen($password) < 6) {
throw new Exception('La contraseña debe tener al menos 6 caracteres');
}
// Verificar si el username ya existe
$existing = $db->fetch(
"SELECT id FROM admin_users WHERE username = ?",
[$username]
);
if ($existing) {
throw new Exception('El nombre de usuario ya está en uso');
}
// Encriptar contraseña
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// Insertar usuario
$db->execute(
"INSERT INTO admin_users (username, password_hash, full_name, email, is_active, created_at)
VALUES (?, ?, ?, ?, 1, NOW())",
[$username, $hashedPassword, $fullName, $email]
);
echo json_encode([
'success' => true,
'message' => 'Usuario creado correctamente'
]);
} catch (Exception $e) {
error_log('Error creating admin user: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+193
View File
@@ -0,0 +1,193 @@
<?php
/**
* API - Eliminar mensaje
* Permite eliminar un mensaje de la conversación
* Fecha: 28 de enero de 2026
*/
// Habilitar display de errores para debug
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once '../config/config.php';
// Log para debug
error_log("delete_message.php: Iniciando, session_id=" . session_id() . ", admin_logged_in=" . (isset($_SESSION['admin_logged_in']) ? $_SESSION['admin_logged_in'] : 'no definido'));
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Manejar preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'DELETE') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Método no permitido. Use POST o DELETE.']);
exit;
}
try {
$input = json_decode(file_get_contents('php://input'), true);
// También soportar query params para DELETE
if (!$input && $_SERVER['REQUEST_METHOD'] === 'DELETE') {
$input = [
'message_id' => $_GET['message_id'] ?? $_GET['id'] ?? null,
'delete_for_everyone' => $_GET['delete_for_everyone'] ?? false
];
}
if (!$input) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'JSON inválido']);
exit;
}
$messageId = $input['message_id'] ?? $input['id'] ?? null;
$deleteForEveryone = $input['delete_for_everyone'] ?? false;
$softDelete = $input['soft_delete'] ?? true; // Por defecto, soft delete
if (!$messageId) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'message_id es requerido']);
exit;
}
$db = Database::getInstance();
// Buscar el mensaje
$sql = "SELECT id, user_id, direction, message_type, content, message_id as wa_message_id,
local_file, local_thumb, media_url
FROM conversations
WHERE id = :mid1 OR message_id = :mid2
LIMIT 1";
$message = $db->fetch($sql, ['mid1' => $messageId, 'mid2' => $messageId]);
if (!$message) {
http_response_code(404);
echo json_encode(['success' => false, 'error' => 'Mensaje no encontrado']);
exit;
}
// Solo permitir eliminar mensajes salientes (outgoing) a menos que sea admin
$isAdmin = isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'];
if ($message['direction'] !== 'outgoing' && !$isAdmin) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Solo puedes eliminar tus propios mensajes']);
exit;
}
$deletedMessageId = $message['id'];
$userId = $message['user_id'];
$waMessageId = $message['wa_message_id'];
if ($softDelete) {
// Soft delete: marcar como eliminado pero mantener registro
// Verificar si existen las columnas deleted_at y original_content
$hasDeletedAt = false;
$hasOriginalContent = false;
try {
$columns = $db->fetchAll("SHOW COLUMNS FROM conversations WHERE Field IN ('deleted_at', 'original_content')");
foreach ($columns as $col) {
if ($col['Field'] === 'deleted_at') $hasDeletedAt = true;
if ($col['Field'] === 'original_content') $hasOriginalContent = true;
}
} catch (Exception $e) {
// Ignorar
}
// Construir la query según las columnas disponibles
// IMPORTANTE: Guardar original_content ANTES de modificar content
if ($hasDeletedAt && $hasOriginalContent) {
// Primero guardamos el contenido original
$db->execute("UPDATE conversations SET original_content = content WHERE id = :id AND original_content IS NULL", ['id' => $deletedMessageId]);
// Luego marcamos como eliminado
$updateSql = "UPDATE conversations SET content = '[Mensaje eliminado]', deleted_at = NOW() WHERE id = :id";
} elseif ($hasDeletedAt) {
$updateSql = "UPDATE conversations SET content = '[Mensaje eliminado]', deleted_at = NOW() WHERE id = :id";
} else {
$updateSql = "UPDATE conversations SET content = '[Mensaje eliminado]' WHERE id = :id";
}
$db->execute($updateSql, ['id' => $deletedMessageId]);
error_log("Message soft-deleted: ID={$deletedMessageId}, User={$userId}");
} else {
// Hard delete: eliminar completamente
// Primero, eliminar archivos multimedia asociados si existen
if (!empty($message['local_file'])) {
$filePath = dirname(__DIR__) . '/' . $message['local_file'];
if (file_exists($filePath)) {
@unlink($filePath);
error_log("Deleted media file: {$filePath}");
}
}
if (!empty($message['local_thumb'])) {
$thumbPath = dirname(__DIR__) . '/' . $message['local_thumb'];
if (file_exists($thumbPath)) {
@unlink($thumbPath);
error_log("Deleted thumbnail: {$thumbPath}");
}
}
// Eliminar el mensaje de la base de datos
$deleteSql = "DELETE FROM conversations WHERE id = :id";
$db->execute($deleteSql, ['id' => $deletedMessageId]);
error_log("Message hard-deleted: ID={$deletedMessageId}, User={$userId}");
}
// Intentar notificar por SSE si está disponible
try {
$ssePayload = [
'type' => 'message_deleted',
'user_id' => $userId,
'message_id' => $deletedMessageId,
'wa_message_id' => $waMessageId,
'soft_delete' => $softDelete,
'deleted_at' => date('Y-m-d H:i:s')
];
// Llamar al endpoint push_event interno
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
$ch = curl_init($pushUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'event_type' => 'message_deleted',
'data' => $ssePayload,
'target_user_id' => 'all'
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
curl_exec($ch);
curl_close($ch);
} catch (Exception $sseErr) {
error_log("delete_message: SSE notification failed: " . $sseErr->getMessage());
}
echo json_encode([
'success' => true,
'message' => $softDelete ? 'Mensaje eliminado' : 'Mensaje eliminado permanentemente',
'data' => [
'id' => $deletedMessageId,
'message_id' => $waMessageId,
'soft_delete' => $softDelete,
'deleted_at' => date('Y-m-d H:i:s')
]
]);
} catch (Exception $e) {
error_log('delete_message.php error: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Error interno del servidor', 'debug' => $e->getMessage()]);
}
+55
View File
@@ -0,0 +1,55 @@
<?php
/**
* API - Eliminar mensaje programado
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: DELETE, POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
// Intentar obtener de query params
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
} else {
$id = isset($input['id']) ? intval($input['id']) : null;
}
if (!$id) {
throw new Exception('ID de mensaje programado requerido');
}
$db = Database::getInstance();
// Verificar que existe
$existing = $db->fetch("SELECT * FROM scheduled_messages WHERE id = ?", [$id]);
if (!$existing) {
throw new Exception('Mensaje programado no encontrado');
}
// Eliminar
$db->execute("DELETE FROM scheduled_messages WHERE id = ?", [$id]);
writeLog('INFO', "Mensaje programado eliminado: ID={$id}");
echo json_encode([
'success' => true,
'message' => 'Mensaje programado eliminado exitosamente'
]);
} catch (Exception $e) {
error_log("Error in delete_scheduled_message.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+160
View File
@@ -0,0 +1,160 @@
<?php
/**
* API - Editar mensaje
* Permite editar el contenido de un mensaje enviado (solo mensajes propios/outgoing)
* Fecha: 28 de enero de 2026
*/
// Habilitar display de errores para debug
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once '../config/config.php';
// Log para debug
error_log("edit_message.php: Iniciando, session_id=" . session_id() . ", admin_logged_in=" . (isset($_SESSION['admin_logged_in']) ? $_SESSION['admin_logged_in'] : 'no definido'));
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Manejar preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Método no permitido. Use POST.']);
exit;
}
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'JSON inválido']);
exit;
}
$messageId = $input['message_id'] ?? $input['id'] ?? null;
$newContent = $input['content'] ?? $input['new_content'] ?? null;
if (!$messageId) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'message_id es requerido']);
exit;
}
if ($newContent === null || trim($newContent) === '') {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'El contenido no puede estar vacío']);
exit;
}
$db = Database::getInstance();
// Buscar el mensaje
$sql = "SELECT id, user_id, direction, message_type, content, message_id as wa_message_id
FROM conversations
WHERE id = :mid1 OR message_id = :mid2
LIMIT 1";
$message = $db->fetch($sql, ['mid1' => $messageId, 'mid2' => $messageId]);
if (!$message) {
http_response_code(404);
echo json_encode(['success' => false, 'error' => 'Mensaje no encontrado']);
exit;
}
// Solo permitir editar mensajes salientes (outgoing)
if ($message['direction'] !== 'outgoing') {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Solo puedes editar tus propios mensajes']);
exit;
}
// Solo permitir editar mensajes de texto
if ($message['message_type'] !== 'text' && $message['message_type'] !== null) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Solo se pueden editar mensajes de texto']);
exit;
}
// Guardar contenido original para historial (opcional)
$originalContent = $message['content'];
// Verificar si existe la columna edited_at
$hasEditedAt = false;
try {
$columns = $db->fetchAll("SHOW COLUMNS FROM conversations WHERE Field = 'edited_at'");
$hasEditedAt = !empty($columns);
} catch (Exception $e) {
// Ignorar
}
// Actualizar el mensaje en la base de datos
if ($hasEditedAt) {
$updateSql = "UPDATE conversations SET content = :content, edited_at = NOW() WHERE id = :id";
} else {
$updateSql = "UPDATE conversations SET content = :content WHERE id = :id";
}
$db->execute($updateSql, [
'content' => trim($newContent),
'id' => $message['id']
]);
// Registrar la edición en el log
error_log("Message edited: ID={$message['id']}, User={$message['user_id']}, Original='{$originalContent}', New='{$newContent}'");
// Intentar notificar por SSE si está disponible
try {
$ssePayload = [
'type' => 'message_edited',
'user_id' => $message['user_id'],
'message_id' => $message['id'],
'wa_message_id' => $message['wa_message_id'],
'new_content' => trim($newContent),
'edited_at' => date('Y-m-d H:i:s')
];
// Llamar al endpoint push_event interno
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
$ch = curl_init($pushUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'event_type' => 'message_edited',
'data' => $ssePayload,
'target_user_id' => 'all'
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
curl_exec($ch);
curl_close($ch);
} catch (Exception $sseErr) {
error_log("edit_message: SSE notification failed: " . $sseErr->getMessage());
}
echo json_encode([
'success' => true,
'message' => 'Mensaje editado correctamente',
'data' => [
'id' => $message['id'],
'message_id' => $message['wa_message_id'],
'new_content' => trim($newContent),
'edited_at' => date('Y-m-d H:i:s')
]
]);
} catch (Exception $e) {
error_log('edit_message.php error: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Error interno del servidor', 'debug' => $e->getMessage()]);
}
+86 -7
View File
@@ -26,6 +26,9 @@ try {
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$offset = ($page - 1) * $limit;
// Parámetro de búsqueda
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
// Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar
$filter = isset($_GET['filter']) ? strtolower(trim($_GET['filter'])) : 'all';
@@ -47,7 +50,19 @@ try {
lm.local_file AS last_local_file,
lm.local_thumb AS last_local_thumb,
lm.created_at AS last_time,
IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) AS unread_count
IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) AS unread_count";
// Si hay búsqueda, agregar campo para ordenar por relevancia
if (!empty($search)) {
$sql .= ",
(CASE
WHEN u.name LIKE ? THEN 3
WHEN u.phone_number LIKE ? THEN 2
ELSE 1
END) AS relevance";
}
$sql .= "
FROM users u
LEFT JOIN conversations c ON c.user_id = u.id
LEFT JOIN (
@@ -55,7 +70,23 @@ try {
JOIN (
SELECT user_id, MAX(created_at) AS last_time FROM conversations GROUP BY user_id
) t2 ON t1.user_id = t2.user_id AND t1.created_at = t2.last_time
) lm ON lm.user_id = u.id
) lm ON lm.user_id = u.id";
// Agregar condición de búsqueda si existe
if (!empty($search)) {
$sql .= "
WHERE (
u.name LIKE ?
OR u.phone_number LIKE ?
OR EXISTS (
SELECT 1 FROM conversations c2
WHERE c2.user_id = u.id
AND c2.content LIKE ?
)
)";
}
$sql .= "
GROUP BY u.id";
// Aplicar filtro 'unread' si se solicita
@@ -63,15 +94,63 @@ try {
$sql .= " HAVING IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) > 0";
}
$sql .= "\n ORDER BY lm.created_at DESC\n LIMIT %d OFFSET %d";
// Ordenar por relevancia si hay búsqueda, sino por fecha
if (!empty($search)) {
$sql .= "\n ORDER BY relevance DESC, lm.created_at DESC";
} else {
$sql .= "\n ORDER BY lm.created_at DESC";
}
$conversations = $db->fetchAll(sprintf($sql, $limit, $offset));
$sql .= "\n LIMIT %d OFFSET %d";
// Preparar parámetros para la consulta
$params = [];
if (!empty($search)) {
$searchParam = '%' . $search . '%';
// Para el campo de relevancia
$params[] = $searchParam; // name LIKE
$params[] = $searchParam; // phone_number LIKE
// Para el WHERE
$params[] = $searchParam; // name LIKE
$params[] = $searchParam; // phone_number LIKE
$params[] = $searchParam; // content LIKE
}
$finalSql = sprintf($sql, $limit, $offset);
if (!empty($params)) {
$conversations = $db->fetchAll($finalSql, $params);
} else {
$conversations = $db->fetchAll($finalSql);
}
// Conteo total de usuarios con al menos una conversación (útil para paginar)
if ($filter === 'unread') {
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations c WHERE c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)");
if (!empty($search)) {
// Conteo con búsqueda
$searchParam = '%' . $search . '%';
$countSql = "SELECT COUNT(DISTINCT u.id) AS count
FROM users u
LEFT JOIN conversations c ON c.user_id = u.id
WHERE (
u.name LIKE ?
OR u.phone_number LIKE ?
OR EXISTS (
SELECT 1 FROM conversations c2
WHERE c2.user_id = u.id
AND c2.content LIKE ?
)
)";
if ($filter === 'unread') {
$countSql .= " AND c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)";
}
$totalRow = $db->fetch($countSql, [$searchParam, $searchParam, $searchParam]);
} else {
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations");
// Conteo normal sin búsqueda
if ($filter === 'unread') {
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations c WHERE c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)");
} else {
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations");
}
}
$total = isset($totalRow['count']) ? intval($totalRow['count']) : 0;
+113
View File
@@ -0,0 +1,113 @@
<?php
/**
* API - Obtener mensajes programados
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$db = Database::getInstance();
// Parámetros de filtro
$userId = isset($_GET['user_id']) ? intval($_GET['user_id']) : null;
$status = $_GET['status'] ?? null;
$dateFrom = $_GET['date_from'] ?? null;
$dateTo = $_GET['date_to'] ?? null;
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : 100;
$offset = isset($_GET['offset']) ? intval($_GET['offset']) : 0;
// Construir query
$sql = "SELECT * FROM scheduled_messages_view WHERE 1=1";
$params = [];
if ($userId) {
$sql .= " AND user_id = ?";
$params[] = $userId;
}
if ($status) {
$sql .= " AND status = ?";
$params[] = $status;
}
if ($dateFrom) {
$sql .= " AND scheduled_date >= ?";
$params[] = $dateFrom;
}
if ($dateTo) {
$sql .= " AND scheduled_date <= ?";
$params[] = $dateTo;
}
// Ordenar por fecha más cercana primero
$sql .= " ORDER BY scheduled_date ASC, scheduled_time ASC";
// Paginación
$sql .= " LIMIT ? OFFSET ?";
$params[] = $limit;
$params[] = $offset;
$messages = $db->fetchAll($sql, $params);
error_log('📋 get_scheduled_messages.php - Encontrados: ' . count($messages) . ' mensajes');
error_log('🔍 SQL ejecutado: ' . $sql);
error_log('📊 Parámetros: ' . json_encode($params));
// Decodificar parámetros JSON
foreach ($messages as &$message) {
if ($message['template_parameters']) {
$message['template_parameters'] = json_decode($message['template_parameters'], true);
}
}
// Contar total
$countSql = "SELECT COUNT(*) as total FROM scheduled_messages WHERE 1=1";
$countParams = [];
if ($userId) {
$countSql .= " AND user_id = ?";
$countParams[] = $userId;
}
if ($status) {
$countSql .= " AND status = ?";
$countParams[] = $status;
}
if ($dateFrom) {
$countSql .= " AND scheduled_date >= ?";
$countParams[] = $dateFrom;
}
if ($dateTo) {
$countSql .= " AND scheduled_date <= ?";
$countParams[] = $dateTo;
}
$countResult = $db->fetch($countSql, $countParams);
$total = $countResult['total'] ?? 0;
echo json_encode([
'success' => true,
'data' => $messages,
'total' => $total,
'limit' => $limit,
'offset' => $offset
]);
} catch (Exception $e) {
error_log("Error in get_scheduled_messages.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Error obteniendo mensajes programados'
]);
}
+111
View File
@@ -0,0 +1,111 @@
<?php
/**
* API - Obtener detalles completos de una plantilla con variables
* Devuelve los parámetros que se deben llenar
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$templateId = isset($_GET['id']) ? intval($_GET['id']) : null;
$templateName = isset($_GET['name']) ? trim($_GET['name']) : null;
$language = isset($_GET['language']) ? trim($_GET['language']) : 'es';
if (!$templateId && !$templateName) {
throw new Exception('Se requiere ID o nombre de plantilla');
}
$db = Database::getInstance();
if ($templateId) {
$template = $db->fetch(
"SELECT * FROM message_templates WHERE id = ?",
[$templateId]
);
} else {
$template = $db->fetch(
"SELECT * FROM message_templates WHERE template_name = ? AND language_code = ?",
[$templateName, $language]
);
}
if (!$template) {
throw new Exception('Plantilla no encontrada');
}
// Extraer variables del body_text o de example_parameters
$bodyText = $template['body_text'] ?? '';
$variables = [];
// Primero intentar usar las variables guardadas en example_parameters
if (!empty($template['example_parameters'])) {
$examples = json_decode($template['example_parameters'], true);
if (isset($examples['variables']) && is_array($examples['variables'])) {
// Usar las variables ya procesadas durante la sincronización
foreach ($examples['variables'] as $var) {
$varName = $var['name'] ?? $var['index'];
$variables[] = [
'index' => $var['index'],
'placeholder' => $var['placeholder'],
'label' => is_numeric($varName) ? 'Variable ' . $varName : ucfirst(str_replace('_', ' ', $varName)),
'required' => true,
'example' => $var['example'] ?? null
];
}
}
}
// Si no hay variables guardadas, extraerlas del body_text (fallback)
if (empty($variables) && preg_match_all('/\{\{([^\}]+)\}\}/', $bodyText, $matches)) {
$uniqueVars = array_unique($matches[1]);
$index = 1;
foreach ($uniqueVars as $varName) {
$varIndex = is_numeric($varName) ? intval($varName) : $index++;
$variables[] = [
'index' => $varIndex,
'placeholder' => '{{' . $varName . '}}',
'label' => is_numeric($varName) ? 'Variable ' . $varName : ucfirst(str_replace('_', ' ', $varName)),
'required' => true,
'example' => null
];
}
}
// Preparar respuesta
$response = [
'success' => true,
'template' => [
'id' => $template['id'],
'name' => $template['name'],
'template_name' => $template['template_name'],
'language_code' => $template['language_code'],
'category' => $template['category'],
'status' => $template['status'],
'body_text' => $bodyText,
'header_text' => $template['header_text'] ?? null,
'header_type' => $template['header_type'] ?? null,
'footer_text' => $template['footer_text'] ?? null,
'has_variables' => count($variables) > 0,
'variables_count' => count($variables),
'variables' => $variables
]
];
echo json_encode($response, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
error_log("Error in get_template_details.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+16 -2
View File
@@ -27,8 +27,22 @@ try {
$approvedOnly = isset($_GET['approved_only']) && $_GET['approved_only'] == '1';
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : null;
// Ajuste: some DBs may not have body_text column; omit it to avoid SQL errors
$sql = "SELECT id, name, template_name, language_code, category, status, created_at FROM message_templates";
// Incluir todos los campos necesarios para la UI
$sql = "SELECT
id,
name,
template_name,
language_code,
category,
status,
body_text,
header_text,
header_type,
footer_text,
components,
example_parameters,
created_at
FROM message_templates";
$params = [];
if ($approvedOnly) {
+41 -5
View File
@@ -24,8 +24,30 @@ header('Access-Control-Allow-Headers: Content-Type');
try {
$db = Database::getInstance();
$users = $db->fetchAll(
"SELECT
// Parámetros de paginación
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$limit = isset($_GET['limit']) ? max(1, min(100, intval($_GET['limit']))) : 20;
$offset = ($page - 1) * $limit;
// Búsqueda opcional
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$whereClause = '';
$params = [];
if (!empty($search)) {
$whereClause = "WHERE u.phone_number LIKE ? OR u.name LIKE ? OR u.email LIKE ?";
$searchParam = "%{$search}%";
$params = [$searchParam, $searchParam, $searchParam];
}
// Contar total de registros
$countQuery = "SELECT COUNT(*) as total FROM users u {$whereClause}";
$totalResult = $db->fetch($countQuery, $params);
$total = $totalResult['total'];
$totalPages = ceil($total / $limit);
// Obtener usuarios paginados
$query = "SELECT
u.id,
u.phone_number,
u.name,
@@ -38,12 +60,26 @@ try {
m.name as current_menu
FROM users u
LEFT JOIN menus m ON u.current_menu_id = m.id
ORDER BY u.created_at DESC"
);
{$whereClause}
ORDER BY u.created_at DESC
LIMIT ? OFFSET ?";
$params[] = $limit;
$params[] = $offset;
$users = $db->fetchAll($query, $params);
echo json_encode([
'success' => true,
'data' => $users
'data' => $users,
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'totalPages' => $totalPages,
'hasNext' => $page < $totalPages,
'hasPrev' => $page > 1
]
]);
} catch (Exception $e) {
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* API - Listar usuarios administradores del sistema
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Obtener todos los usuarios administradores
$users = $db->fetchAll("
SELECT
id,
username,
full_name,
email,
is_active,
last_login,
created_at
FROM admin_users
ORDER BY created_at DESC
");
echo json_encode([
'success' => true,
'data' => $users
]);
} catch (Exception $e) {
error_log('Error listing admin users: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Error al obtener usuarios: ' . $e->getMessage()
]);
}
+130
View File
@@ -0,0 +1,130 @@
<?php
/**
* API - Generar vista previa de plantilla con parámetros
* Reemplaza las variables y muestra cómo se verá el mensaje final
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Datos inválidos');
}
$templateId = $input['template_id'] ?? null;
$parameters = $input['parameters'] ?? [];
if (!$templateId) {
throw new Exception('Se requiere ID de plantilla');
}
$db = Database::getInstance();
$template = $db->fetch(
"SELECT * FROM message_templates WHERE id = ?",
[$templateId]
);
if (!$template) {
throw new Exception('Plantilla no encontrada');
}
// Obtener textos de la plantilla
$bodyText = $template['body_text'] ?? '';
$headerText = $template['header_text'] ?? null;
$footerText = $template['footer_text'] ?? null;
// Obtener variables guardadas para saber sus placeholders
$exampleParams = json_decode($template['example_parameters'] ?? '{}', true);
$savedVariables = $exampleParams['variables'] ?? [];
// Reemplazar variables en el body
$bodyPreview = $bodyText;
if (is_array($parameters)) {
// Si tenemos variables guardadas, usar sus placeholders
if (!empty($savedVariables)) {
foreach ($savedVariables as $var) {
$index = $var['index'] - 1; // Convertir a índice de array (0-based)
if (isset($parameters[$index])) {
$placeholder = $var['placeholder']; // Ejemplo: {{nombre_tema}} o {{1}}
$bodyPreview = str_replace($placeholder, $parameters[$index], $bodyPreview);
}
}
} else {
// Fallback: reemplazar variables numéricas estándar
foreach ($parameters as $index => $value) {
$placeholder = '{{' . ($index + 1) . '}}';
$bodyPreview = str_replace($placeholder, $value, $bodyPreview);
}
}
}
// Reemplazar variables en el header si existen
$headerPreview = $headerText;
if ($headerText && is_array($parameters)) {
if (!empty($savedVariables)) {
foreach ($savedVariables as $var) {
$index = $var['index'] - 1;
if (isset($parameters[$index])) {
$placeholder = $var['placeholder'];
$headerPreview = str_replace($placeholder, $parameters[$index], $headerPreview);
}
}
} else {
foreach ($parameters as $index => $value) {
$placeholder = '{{' . ($index + 1) . '}}';
$headerPreview = str_replace($placeholder, $value, $headerPreview);
}
}
}
// Verificar si quedan variables sin reemplazar (tanto numéricas como con nombres)
$hasUnreplacedVars = preg_match('/\{\{[^\}]+\}\}/', $bodyPreview);
// Generar vista previa HTML
$htmlPreview = '';
if ($headerPreview) {
$htmlPreview .= '<div class="template-header" style="font-weight: bold; margin-bottom: 8px; color: #075e54;">';
$htmlPreview .= htmlspecialchars($headerPreview);
$htmlPreview .= '</div>';
}
$htmlPreview .= '<div class="template-body" style="margin-bottom: 8px;">';
$htmlPreview .= nl2br(htmlspecialchars($bodyPreview));
$htmlPreview .= '</div>';
if ($footerText) {
$htmlPreview .= '<div class="template-footer" style="font-size: 0.85em; color: #667781; margin-top: 8px;">';
$htmlPreview .= htmlspecialchars($footerText);
$htmlPreview .= '</div>';
}
echo json_encode([
'success' => true,
'preview' => [
'body' => $bodyPreview,
'header' => $headerPreview,
'footer' => $footerText,
'html' => $htmlPreview,
'has_unreplaced_variables' => $hasUnreplacedVars,
'is_complete' => !$hasUnreplacedVars
]
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
error_log("Error in preview_template.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+150
View File
@@ -0,0 +1,150 @@
<?php
/**
* API - Crear mensaje programado / recordatorio
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$input = json_decode(file_get_contents('php://input'), true);
error_log('📥 schedule_message.php - Input recibido: ' . json_encode($input));
if (!$input) {
throw new Exception('Datos inválidos');
}
// Validar campos requeridos
$userId = isset($input['user_id']) ? intval($input['user_id']) : null;
$messageType = $input['message_type'] ?? 'template';
$scheduledDate = $input['scheduled_date'] ?? null;
$scheduledTime = $input['scheduled_time'] ?? '09:00';
if (!$userId) {
throw new Exception('ID de usuario requerido');
}
if (!$scheduledDate) {
throw new Exception('Fecha programada requerida');
}
// Validar que la fecha no sea en el pasado
$scheduledDateTime = new DateTime($scheduledDate . ' ' . $scheduledTime);
$now = new DateTime();
if ($scheduledDateTime < $now) {
throw new Exception('La fecha programada no puede ser en el pasado');
}
$db = Database::getInstance();
// Verificar que el usuario existe
$user = $db->fetch("SELECT * FROM users WHERE id = ?", [$userId]);
if (!$user) {
throw new Exception('Usuario no encontrado');
}
// Preparar datos según el tipo de mensaje
$templateId = null;
$templateName = null;
$templateLanguage = 'es';
$templateParameters = null;
$messageContent = null;
if ($messageType === 'template') {
$templateId = isset($input['template_id']) ? intval($input['template_id']) : null;
$templateName = $input['template_name'] ?? null;
$templateLanguage = $input['template_language'] ?? 'es';
$templateParameters = $input['template_parameters'] ?? null;
error_log('🔍 template_parameters recibido: ' . json_encode($templateParameters));
error_log('🔍 Es array? ' . (is_array($templateParameters) ? 'Sí' : 'No'));
if (!$templateId && !$templateName) {
throw new Exception('Se requiere ID o nombre de plantilla');
}
// Si se proporcionaron parámetros, convertir a JSON
if ($templateParameters && is_array($templateParameters)) {
$templateParameters = json_encode($templateParameters, JSON_UNESCAPED_UNICODE);
error_log('✅ Convertido a JSON: ' . $templateParameters);
} else {
error_log('⚠️ No se convirtió - es null o no es array');
}
} else if ($messageType === 'text') {
$messageContent = $input['message_content'] ?? null;
if (empty($messageContent)) {
throw new Exception('Contenido del mensaje requerido');
}
} else {
throw new Exception('Tipo de mensaje no soportado');
}
// Obtener ID del admin actual
$createdBy = $_SESSION['admin_user']['id'] ?? $_SESSION['user_id'] ?? null;
// Insertar mensaje programado
$db->execute(
"INSERT INTO scheduled_messages (
user_id,
template_id,
template_name,
template_language,
template_parameters,
message_type,
message_content,
scheduled_date,
scheduled_time,
created_by,
status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')",
[
$userId,
$templateId,
$templateName,
$templateLanguage,
$templateParameters,
$messageType,
$messageContent,
$scheduledDate,
$scheduledTime,
$createdBy
]
);
$scheduledId = $db->lastInsertId();
error_log('✅ Mensaje programado guardado con ID: ' . $scheduledId);
// Log
writeLog('INFO', "Mensaje programado creado: ID={$scheduledId}, Usuario={$userId}, Fecha={$scheduledDate} {$scheduledTime}");
echo json_encode([
'success' => true,
'message' => 'Recordatorio programado exitosamente',
'data' => [
'id' => $scheduledId,
'scheduled_date' => $scheduledDate,
'scheduled_time' => $scheduledTime,
'user_name' => $user['name'],
'phone_number' => $user['phone_number']
]
]);
} catch (Exception $e) {
error_log("Error in schedule_message.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+189 -27
View File
@@ -23,55 +23,214 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['message'])) {
if (!$input) {
http_response_code(400);
echo json_encode(['error' => 'Mensaje requerido']);
echo json_encode(['error' => 'Datos requeridos']);
exit;
}
$filter = $input['filter'] ?? 'all';
$message = $input['message'];
$selectionType = $input['selection_type'] ?? 'filter';
$messageType = $input['message_type'] ?? 'text';
$db = Database::getInstance();
$whatsappService = new WhatsAppService();
// Construir query según filtro
$whereClause = "WHERE u.status = 'active'";
// Obtener usuarios según tipo de selección
$users = [];
switch ($filter) {
case 'active':
$whereClause .= " AND EXISTS (
SELECT 1 FROM conversations c
WHERE c.user_id = u.id
AND c.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
)";
break;
if ($selectionType === 'specific') {
// Usuarios específicos seleccionados
if (!isset($input['user_ids']) || !is_array($input['user_ids']) || empty($input['user_ids'])) {
http_response_code(400);
echo json_encode(['error' => 'Debes seleccionar al menos un usuario']);
exit;
}
case 'recent':
$whereClause .= " AND u.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)";
break;
$userIds = array_map('intval', $input['user_ids']);
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
$query = "SELECT id, phone_number, name FROM users WHERE id IN ($placeholders) ORDER BY id ASC";
$users = $db->fetchAll($query, $userIds);
} else {
// Por filtro
$filter = $input['filter'] ?? 'all';
$whereClause = "1=1";
switch ($filter) {
case 'active':
$whereClause .= " AND EXISTS (
SELECT 1 FROM conversations c
WHERE c.user_id = u.id
AND c.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
)";
break;
case 'recent':
$whereClause .= " AND u.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)";
break;
case 'all':
// Incluir todos los usuarios
break;
}
$query = "SELECT id, phone_number, name FROM users u WHERE " . $whereClause . " ORDER BY id ASC";
$users = $db->fetchAll($query);
}
// Obtener usuarios según filtro
$users = $db->fetchAll(
"SELECT phone_number FROM users u " . $whereClause
);
if (empty($users)) {
http_response_code(404);
echo json_encode([
'error' => 'No se encontraron usuarios para enviar',
'selection_type' => $selectionType
]);
exit;
}
$sentCount = 0;
$errorCount = 0;
$errors = [];
foreach ($users as $user) {
try {
$response = $whatsappService->sendTextMessage($user['phone_number'], $message);
if ($response) {
$sentCount++;
$response = null;
$messageContent = '';
// Enviar según tipo de mensaje
if ($messageType === 'template') {
// Enviar plantilla
if (!isset($input['template_id'])) {
throw new Exception('ID de plantilla requerido');
}
$templateId = intval($input['template_id']);
$template = $db->fetch("SELECT * FROM message_templates WHERE id = ?", [$templateId]);
if (!$template) {
throw new Exception('Plantilla no encontrada');
}
$status = strtoupper($template['status']);
if ($status !== 'APPROVED') {
throw new Exception('La plantilla no está aprobada. Status: ' . $template['status']);
}
// Preparar variables
$templateVariables = $input['template_variables'] ?? [];
error_log('📝 Variables recibidas: ' . json_encode($templateVariables));
error_log('🔍 Tipo: ' . gettype($templateVariables));
// Detectar si son variables numéricas o con nombres
$isNumericVars = false;
$isNamedVars = false;
if (is_array($templateVariables) && !empty($templateVariables)) {
$firstKey = array_key_first($templateVariables);
if (is_int($firstKey) && isset($templateVariables[0])) {
// Array indexado numéricamente [0 => "val1", 1 => "val2"]
$isNumericVars = true;
error_log('✅ Variables tipo: Array numérico indexado');
} elseif (is_string($firstKey) && !is_numeric($firstKey)) {
// Array asociativo con nombres ["nombre_tema" => "val1", "fecha" => "val2"]
$isNamedVars = true;
error_log('✅ Variables tipo: Objeto con nombres (asociativo)');
} elseif (is_numeric($firstKey)) {
// Array con claves numéricas [1 => "val1", 2 => "val2"]
$isNumericVars = true;
error_log('✅ Variables tipo: Array con claves numéricas');
}
}
// Procesar según el tipo
$processedVariables = [];
if ($isNamedVars) {
// Variables con nombres: pasar como objeto asociativo
$processedVariables = $templateVariables;
error_log('📦 Enviando como objeto con nombres: ' . json_encode($processedVariables));
} elseif ($isNumericVars) {
// Variables numéricas: ordenar y convertir a array indexado
ksort($templateVariables, SORT_NUMERIC);
foreach ($templateVariables as $value) {
$processedVariables[] = $value;
}
error_log('📊 Enviando como array ordenado: ' . json_encode($processedVariables));
} else {
error_log('⚠️ Sin variables o formato desconocido');
}
error_log('📤 Variables procesadas: ' . json_encode($processedVariables));
// Verificar variables si la plantilla usa formato numérico
$expectedVars = preg_match_all('/\{\{(\d+)\}\}/', $template['body_text'] ?? '', $matches);
if ($expectedVars > 0 && $isNumericVars && count($processedVariables) !== $expectedVars) {
throw new Exception("La plantilla requiere {$expectedVars} variable(s) numéricas, pero se enviaron " . count($processedVariables));
}
$response = $whatsappService->sendTemplateMessage(
$user['phone_number'],
$template['template_name'] ?? $template['name'],
$template['language_code'] ?? 'es',
$processedVariables
);
// Construir contenido para BD (reemplazar variables en body_text)
$messageContent = $template['body_text'] ?? $template['name'];
if ($isNamedVars) {
// Reemplazar por nombres
foreach ($templateVariables as $key => $value) {
$messageContent = str_replace('{{' . $key . '}}', $value, $messageContent);
}
} else {
// Reemplazar por índice numérico
foreach ($processedVariables as $index => $value) {
$messageContent = str_replace('{{' . ($index + 1) . '}}', $value, $messageContent);
}
}
} else {
// Enviar texto simple
$message = trim($input['message'] ?? '');
if (empty($message)) {
throw new Exception('Mensaje vacío');
}
$response = $whatsappService->sendTextMessage($user['phone_number'], $message);
$messageContent = $message;
}
// Pequeña pausa para no saturar la API
usleep(100000); // 0.1 segundo
if ($response && isset($response['success']) && $response['success']) {
$sentCount++;
// Guardar mensaje en BD
$db->insert('conversations', [
'user_id' => $user['id'],
'message_id' => $response['message_id'] ?? null,
'content' => $messageContent,
'direction' => 'outgoing',
'message_type' => $messageType === 'template' ? 'template' : 'text',
'status' => 'sent',
'created_at' => date('Y-m-d H:i:s')
]);
} else {
$errorCount++;
$errors[] = [
'phone' => $user['phone_number'],
'error' => 'Respuesta inválida de WhatsApp'
];
}
// Pausa para no saturar la API (250ms entre mensajes)
usleep(250000);
} catch (Exception $e) {
$errorCount++;
$errors[] = [
'phone' => $user['phone_number'],
'name' => $user['name'] ?? 'Sin nombre',
'error' => $e->getMessage()
];
error_log("Error sending broadcast to {$user['phone_number']}: " . $e->getMessage());
}
}
@@ -80,7 +239,10 @@ try {
'success' => true,
'sent_count' => $sentCount,
'error_count' => $errorCount,
'total_users' => count($users)
'total_users' => count($users),
'selection_type' => $selectionType,
'message_type' => $messageType,
'errors' => $errorCount > 0 ? $errors : null
]);
} catch (Exception $e) {
+24
View File
@@ -239,3 +239,27 @@
[2026-01-29 11:39:41] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
[2026-01-29 11:39:41] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
[2026-01-29 11:39:41] Saving to DB - content: record_1769704778262.webm, media_url: 906365935662402, local_file: uploads/media_697b8d4b88a6c2.66006027.ogg, local_thumb: null
[2026-02-02 21:43:43] Raw input: {"recipient":"573168950803","media_url":"https://bot.u-s.app/uploads/media_698160df42ea78.64430762.ogg","media_type":"audio","caption":null,"filename":"record_1770086620567.webm","is_voice":true}
[2026-02-02 21:43:43] Input decoded: {"recipient":"573168950803","media_url":"https:\/\/bot.u-s.app\/uploads\/media_698160df42ea78.64430762.ogg","media_type":"audio","caption":null,"filename":"record_1770086620567.webm","is_voice":true}
[2026-02-02 21:43:43] is_voice: true
[2026-02-02 21:43:44] Upload result: {"id":"1591364868775705"}
[2026-02-02 21:43:44] Media ID obtained: 1591364868775705
[2026-02-02 21:43:45] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMDhGRTNDMUQ5NEFGQTRBQ0FDAA=="}]}
[2026-02-02 21:43:45] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMDhGRTNDMUQ5NEFGQTRBQ0FDAA=="}]}
[2026-02-02 21:43:45] Saving to DB - content: record_1770086620567.webm, media_url: 1591364868775705, local_file: uploads/media_698160df42ea78.64430762.ogg, local_thumb: null
[2026-02-03 08:52:11] Raw input: {"recipient":"573168950803","media_url":"https://bot.u-s.app/uploads/media_6981fd8bb34da0.05106104.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
[2026-02-03 08:52:11] Input decoded: {"recipient":"573168950803","media_url":"https:\/\/bot.u-s.app\/uploads\/media_6981fd8bb34da0.05106104.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
[2026-02-03 08:52:11] is_voice: false
[2026-02-03 08:52:12] Upload result: {"id":"817602961329813"}
[2026-02-03 08:52:12] Media ID obtained: 817602961329813
[2026-02-03 08:52:13] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMUVGMkUzOTNCNjBENTE4OEI1AA=="}]}
[2026-02-03 08:52:13] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMUVGMkUzOTNCNjBENTE4OEI1AA=="}]}
[2026-02-03 08:52:13] Saving to DB - content: 20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx, media_url: 817602961329813, local_file: uploads/media_6981fd8bb34da0.05106104.xlsx, local_thumb: null
[2026-02-03 16:20:01] Raw input: {"recipient":"573022548060","media_url":"https://bot.u-s.app/uploads/media_69826681178109.48762271.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
[2026-02-03 16:20:01] Input decoded: {"recipient":"573022548060","media_url":"https:\/\/bot.u-s.app\/uploads\/media_69826681178109.48762271.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
[2026-02-03 16:20:01] is_voice: false
[2026-02-03 16:20:02] Upload result: {"id":"876532491851564"}
[2026-02-03 16:20:02] Media ID obtained: 876532491851564
[2026-02-03 16:20:02] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573022548060","wa_id":"573022548060"}],"messages":[{"id":"wamid.HBgMNTczMDIyNTQ4MDYwFQIAERgSMjAzNDlFQTVENzA2OEQ5QzkwAA=="}]}
[2026-02-03 16:20:02] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573022548060","wa_id":"573022548060"}],"messages":[{"id":"wamid.HBgMNTczMDIyNTQ4MDYwFQIAERgSMjAzNDlFQTVENzA2OEQ5QzkwAA=="}]}
[2026-02-03 16:20:02] Saving to DB - content: 20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx, media_url: 876532491851564, local_file: uploads/media_69826681178109.48762271.xlsx, local_thumb: null
+10 -3
View File
@@ -163,10 +163,17 @@ try {
echo json_encode(['error' => 'Mensaje requerido']);
exit;
}
// Procesar saltos de línea escapados (\\n o \n) -> convertir a saltos reales
$message = $input['message'];
$message = str_replace('\\n', "\n", $message); // \n escapado literal
$message = preg_replace('/(?<!\\\\)\\\\n/', "\n", $message); // \n en string
// DEBUG: Mostrar datos de envío de mensaje de texto
error_log("=== DEBUG WHATSAPP TEXT MESSAGE ===");
error_log("Recipient: " . $recipient);
error_log("Message: " . $input['message']);
error_log("Message original: " . $input['message']);
error_log("Message processed: " . $message);
error_log("Method: sendTextMessage()/sendTextReply()");
error_log("====================================");
@@ -175,9 +182,9 @@ try {
if (!empty($input['reply_to'])) {
$replyTo = $input['reply_to'];
$response = $whatsappService->sendTextReply($recipient, $replyTo, $input['message'], $operatorId ? ['operator_id' => $operatorId] : null);
$response = $whatsappService->sendTextReply($recipient, $replyTo, $message, $operatorId ? ['operator_id' => $operatorId] : null);
} else {
$response = $whatsappService->sendTextMessage($recipient, $input['message'], $operatorId ? ['operator_id' => $operatorId] : null);
$response = $whatsappService->sendTextMessage($recipient, $message, $operatorId ? ['operator_id' => $operatorId] : null);
}
break;
+266
View File
@@ -0,0 +1,266 @@
<?php
/**
* API - Sincronizar plantillas desde Facebook/WhatsApp Business API
* Obtiene las plantillas desde la API de WhatsApp y las guarda en la base de datos
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$db = Database::getInstance();
// Obtener configuración de WhatsApp
$token = getConfigFromDB('whatsapp_token', '');
$wabaId = getConfigFromDB('whatsapp_business_account_id', '');
if (empty($token)) {
throw new Exception('Token de WhatsApp no configurado');
}
if (empty($wabaId)) {
throw new Exception('Business Account ID no configurado. Configure el WABA ID en la configuración del sistema.');
}
// Hacer petición a la API de WhatsApp para obtener plantillas
$url = "https://graph.facebook.com/v21.0/{$wabaId}/message_templates";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Content-Type: application/json"
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new Exception("Error de conexión: {$error}");
}
if ($httpCode !== 200) {
$errorData = json_decode($response, true);
$errorMsg = $errorData['error']['message'] ?? 'Error desconocido';
throw new Exception("Error de API ({$httpCode}): {$errorMsg}");
}
$data = json_decode($response, true);
// LOG: Ver respuesta completa de WhatsApp
error_log('📥 Respuesta de WhatsApp API: ' . substr($response, 0, 2000));
error_log('📊 Total de plantillas recibidas: ' . count($data['data'] ?? []));
if (!isset($data['data']) || !is_array($data['data'])) {
throw new Exception('Respuesta inválida de la API de WhatsApp');
}
$templates = $data['data'];
// LOG: Ver primera plantilla como ejemplo
if (!empty($templates)) {
error_log('📋 Ejemplo de plantilla recibida: ' . json_encode($templates[0], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
$syncedCount = 0;
$updatedCount = 0;
$skippedCount = 0;
$errors = [];
foreach ($templates as $template) {
try {
$templateName = $template['name'] ?? '';
$language = $template['language'] ?? 'es';
$status = $template['status'] ?? 'pending';
$category = $template['category'] ?? 'UTILITY';
$components = $template['components'] ?? [];
if (empty($templateName)) {
$skippedCount++;
continue;
}
// Extraer información de los componentes
$bodyText = null;
$headerText = null;
$headerType = null;
$footerText = null;
$exampleParameters = [];
foreach ($components as $component) {
$type = $component['type'] ?? '';
if ($type === 'BODY') {
$bodyText = $component['text'] ?? null;
// Extraer ejemplos de parámetros del body
if (isset($component['example']['body_text'])) {
$exampleParameters['body'] = $component['example']['body_text'];
}
} elseif ($type === 'HEADER') {
$headerText = $component['text'] ?? null;
$headerType = strtolower($component['format'] ?? 'text');
// Extraer ejemplos del header
if (isset($component['example']['header_text'])) {
$exampleParameters['header'] = $component['example']['header_text'];
}
} elseif ($type === 'FOOTER') {
$footerText = $component['text'] ?? null;
}
}
// NUEVO: Extraer variables automáticamente del body_text
$variables = [];
if ($bodyText) {
// Buscar tanto variables numéricas {{1}} como con nombres {{nombre_tema}}
preg_match_all('/\{\{([^\}]+)\}\}/', $bodyText, $matches);
if (!empty($matches[1])) {
$uniqueVars = array_unique($matches[1]);
error_log("🔍 Template '{$templateName}' - Variables encontradas: " . json_encode($matches[1]));
$index = 1;
foreach ($uniqueVars as $varName) {
$example = null;
// Si la variable es numérica, usar su índice
if (is_numeric($varName)) {
$varIndex = (int)$varName;
// Los ejemplos de WhatsApp vienen como array de arrays: [["valor"]]
if (isset($exampleParameters['body'][$varIndex - 1])) {
$exampleData = $exampleParameters['body'][$varIndex - 1];
$example = is_array($exampleData) ? $exampleData[0] : $exampleData;
}
} else {
// Para variables con nombre, usar índice secuencial
$varIndex = $index;
if (isset($exampleParameters['body'][$index - 1])) {
$exampleData = $exampleParameters['body'][$index - 1];
$example = is_array($exampleData) ? $exampleData[0] : $exampleData;
}
$index++;
}
$variables[] = [
'index' => $varIndex,
'placeholder' => "{{" . $varName . "}}",
'name' => $varName,
'example' => $example
];
}
error_log("✅ Variables procesadas para '{$templateName}': " . json_encode($variables, JSON_UNESCAPED_UNICODE));
}
}
// Si encontramos variables, agregarlas a example_parameters
if (!empty($variables)) {
$exampleParameters['variables'] = $variables;
error_log("💾 example_parameters final para '{$templateName}': " . json_encode($exampleParameters, JSON_UNESCAPED_UNICODE));
}
// Preparar datos para guardar
$componentsJson = !empty($components) ? json_encode($components, JSON_UNESCAPED_UNICODE) : null;
$exampleJson = !empty($exampleParameters) ? json_encode($exampleParameters, JSON_UNESCAPED_UNICODE) : null;
// Verificar si la plantilla ya existe
$existing = $db->fetch(
"SELECT id, status FROM message_templates WHERE template_name = ? AND language_code = ?",
[$templateName, $language]
);
if ($existing) {
// Actualizar plantilla existente con nuevos campos
$db->execute(
"UPDATE message_templates SET
status = ?,
category = ?,
body_text = ?,
header_text = ?,
header_type = ?,
footer_text = ?,
components = ?,
example_parameters = ?,
updated_at = NOW()
WHERE id = ?",
[
strtolower($status),
strtolower($category),
$bodyText,
$headerText,
$headerType,
$footerText,
$componentsJson,
$exampleJson,
$existing['id']
]
);
$updatedCount++;
} else {
// Insertar nueva plantilla con componentes
$db->execute(
"INSERT INTO message_templates (
name,
template_name,
language_code,
category,
status,
body_text,
header_text,
header_type,
footer_text,
components,
example_parameters,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())",
[
$templateName, // nombre descriptivo igual al nombre técnico
$templateName,
$language,
strtolower($category),
strtolower($status),
$bodyText,
$headerText,
$headerType,
$footerText,
$componentsJson,
$exampleJson
]
);
$syncedCount++;
}
} catch (Exception $e) {
$errors[] = "Error procesando plantilla '{$templateName}': " . $e->getMessage();
}
}
// Log de la sincronización
writeLog('INFO', "Plantillas sincronizadas desde Facebook: {$syncedCount} nuevas, {$updatedCount} actualizadas, {$skippedCount} sin cambios");
echo json_encode([
'success' => true,
'message' => 'Plantillas sincronizadas correctamente',
'data' => [
'total' => count($templates),
'synced' => $syncedCount,
'updated' => $updatedCount,
'skipped' => $skippedCount,
'errors' => $errors
]
]);
} catch (Exception $e) {
error_log("Error in sync_templates_from_facebook.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+50
View File
@@ -0,0 +1,50 @@
<?php
/**
* API - Activar/Desactivar usuario administrador
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Leer datos del request
$input = json_decode(file_get_contents('php://input'), true);
$userId = $input['user_id'] ?? null;
$isActive = $input['is_active'] ?? null;
if (!$userId) {
throw new Exception('ID de usuario requerido');
}
// No permitir desactivar el propio usuario
if ($userId == $_SESSION['user_id']) {
throw new Exception('No puedes desactivar tu propio usuario');
}
// Actualizar estado
$db->execute(
"UPDATE admin_users SET is_active = ? WHERE id = ?",
[$isActive ? 1 : 0, $userId]
);
echo json_encode([
'success' => true,
'message' => 'Estado actualizado correctamente'
]);
} catch (Exception $e) {
error_log('Error toggling admin user status: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+61
View File
@@ -0,0 +1,61 @@
<?php
/**
* API - Actualizar usuario administrador
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Leer datos del request
$input = json_decode(file_get_contents('php://input'), true);
$userId = $input['user_id'] ?? null;
$username = trim($input['username'] ?? '');
$fullName = trim($input['full_name'] ?? '');
$email = trim($input['email'] ?? '');
if (!$userId) {
throw new Exception('ID de usuario requerido');
}
if (empty($username)) {
throw new Exception('El nombre de usuario es requerido');
}
// Verificar si el username ya existe (excepto para el usuario actual)
$existing = $db->fetch(
"SELECT id FROM admin_users WHERE username = ? AND id != ?",
[$username, $userId]
);
if ($existing) {
throw new Exception('El nombre de usuario ya está en uso');
}
// Actualizar usuario
$db->execute(
"UPDATE admin_users SET username = ?, full_name = ?, email = ? WHERE id = ?",
[$username, $fullName, $email, $userId]
);
echo json_encode([
'success' => true,
'message' => 'Usuario actualizado correctamente'
]);
} catch (Exception $e) {
error_log('Error updating admin user: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+101
View File
@@ -0,0 +1,101 @@
<?php
/**
* API - Actualizar mensaje programado
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: PUT, POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Datos inválidos');
}
$id = isset($input['id']) ? intval($input['id']) : null;
if (!$id) {
throw new Exception('ID de mensaje programado requerido');
}
$db = Database::getInstance();
// Verificar que existe
$existing = $db->fetch("SELECT * FROM scheduled_messages WHERE id = ?", [$id]);
if (!$existing) {
throw new Exception('Mensaje programado no encontrado');
}
// No permitir editar mensajes ya enviados
if ($existing['status'] === 'sent') {
throw new Exception('No se puede editar un mensaje ya enviado');
}
// Preparar campos a actualizar
$updates = [];
$params = [];
if (isset($input['scheduled_date'])) {
$updates[] = "scheduled_date = ?";
$params[] = $input['scheduled_date'];
}
if (isset($input['scheduled_time'])) {
$updates[] = "scheduled_time = ?";
$params[] = $input['scheduled_time'];
}
if (isset($input['template_parameters'])) {
$updates[] = "template_parameters = ?";
$params[] = is_array($input['template_parameters'])
? json_encode($input['template_parameters'], JSON_UNESCAPED_UNICODE)
: $input['template_parameters'];
}
if (isset($input['message_content'])) {
$updates[] = "message_content = ?";
$params[] = $input['message_content'];
}
if (isset($input['status'])) {
$updates[] = "status = ?";
$params[] = $input['status'];
}
if (empty($updates)) {
throw new Exception('No hay campos para actualizar');
}
// Agregar updated_at
$updates[] = "updated_at = NOW()";
// Agregar ID al final de params
$params[] = $id;
// Ejecutar actualización
$sql = "UPDATE scheduled_messages SET " . implode(', ', $updates) . " WHERE id = ?";
$db->execute($sql, $params);
writeLog('INFO', "Mensaje programado actualizado: ID={$id}");
echo json_encode([
'success' => true,
'message' => 'Mensaje programado actualizado exitosamente'
]);
} catch (Exception $e) {
error_log("Error in update_scheduled_message.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+12 -4
View File
@@ -163,10 +163,18 @@ try {
}
}
// Construir URL pública
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$publicUrl = $protocol . '://' . $host . '/uploads/' . $uniqueName;
// Construir URL pública usando APP_URL del .env si está disponible
// Esto asegura que los archivos sean accesibles públicamente desde WhatsApp (datos móviles)
$appUrl = getenv('APP_URL');
if ($appUrl && $appUrl !== '') {
// Usar APP_URL del .env (ej: https://bot.u-s.app/)
$publicUrl = rtrim($appUrl, '/') . '/uploads/' . $uniqueName;
} else {
// Fallback: auto-detectar desde HTTP_HOST (desarrollo local)
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$publicUrl = $protocol . '://' . $host . '/uploads/' . $uniqueName;
}
// Determinar tipo de medio
$mediaType = 'document';
+135
View File
@@ -279,3 +279,138 @@
[2026-01-28 03:19:02] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-01-28 03:19:02] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1769588642&hash=ARkCJbkmByU2tdLOv2EHpSH0F12MJjM_Oqj-hiGgNVSYAg
[2026-01-28 03:19:02] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1769588642&hash=ARl75SVpu4vAnsH1AXoBGSv5sjftMr7JPocagoN5kIdMWQ
[2026-02-03 08:52:36] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 08:52:36] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:52:36] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:52:36] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127056&hash=ARmn_DQsFhxynN1KXT-Ge4Xf8WLnjXtDlWoDsUomB4lLKg
[2026-02-03 08:52:36] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127056&hash=ARnKjD4rWdn1Lf97robC2d7AyEHpxH6a2_oPLhmgZ0I9pA
[2026-02-03 08:52:57] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:52:57] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 08:52:57] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:52:57] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127077&hash=ARldzoeUm09JzYXDKy0ypZS4VLSt5A541gqPHgUu-u9geA
[2026-02-03 08:52:57] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127077&hash=ARmAMwa-nPL80hEmwrHHiSHiMhllm6JUl3UG413ZbMkpnA
[2026-02-03 08:52:59] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:52:59] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:52:59] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 08:52:59] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127079&hash=ARmB9s1otyY0Zp-uU577Ezen_iMY_waheMG8EMOMTf4yvA
[2026-02-03 08:52:59] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127079&hash=ARlMcDHlgltZRpheK41ar-GLM_sSPXV_8yudSsLXa4IOJw
[2026-02-03 08:53:52] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:53:52] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:53:52] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 08:53:53] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127133&hash=ARmVgexderjai88OImGBkEGRonyefGBrm1-ow633oVjF5w
[2026-02-03 08:53:53] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127133&hash=ARleR9CvdH_ZXgcEKan0BggNv6osT2pUwtC3qumta4gttA
[2026-02-03 08:53:53] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:53:53] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:53:53] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 08:53:53] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127133&hash=ARmVgexderjai88OImGBkEGRonyefGBrm1-ow633oVjF5w
[2026-02-03 08:53:53] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127133&hash=ARleR9CvdH_ZXgcEKan0BggNv6osT2pUwtC3qumta4gttA
[2026-02-03 11:15:58] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 11:15:58] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 11:15:58] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 11:15:59] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770135659&hash=ARlKSkH_tfyiy1Ctx7mwxi3aZ2JrsWdWcK9I73EldYmKeg
[2026-02-03 11:15:59] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770135659&hash=ARmOinKL4W0exQVUyOxwpLCkmPghiI0rzBv22kDEYwbePA
[2026-02-03 12:00:15] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 12:00:15] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 12:00:15] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 12:00:15] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770138315&hash=ARnn0qsPekd9pOK3dw85bzArN5oMXws99mc_zs6cdK0wtQ
[2026-02-03 12:00:15] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770138315&hash=ARlAM-vxY5Y6ST1Lwp8ox6SBOAefCdZ9yWVZKM-04kcgqQ
[2026-02-03 12:45:14] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 12:45:14] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 12:45:14] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 12:45:14] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770141014&hash=ARn79fTfe65Rz38MXO0hhmyLSuVJsGuIzM30zAngMKVSeA
[2026-02-03 12:45:14] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770141014&hash=ARknEbp3Lf9RYQ2daDdcExwkmvWlMb1F_DLBhGQYJhqXtQ
[2026-02-03 14:22:03] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:22:03] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:22:03] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 14:22:03] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770146823&hash=ARmkpGGK9nwQCAqwRqTNu8hZdxaLvFe_QqKCZDa60vjQMw
[2026-02-03 14:22:03] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770146823&hash=ARkmJ5x9KoRtE0e7E2j3ao1uDWPpEzVsJ3mSCUza8UjbKQ
[2026-02-03 14:23:56] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:23:56] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:23:56] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 14:23:57] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770146937&hash=ARlFlA7KVAre979qNou-DdifbKg_QVenRMQ-ocl3p7vqjg
[2026-02-03 14:23:57] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770146937&hash=ARnJ0xX1NUbsvypvvoZA6UwXnFioUbFugAFUqwrBpQh23Q
[2026-02-03 14:28:39] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:28:39] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 14:28:39] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:28:39] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770147219&hash=ARkHFZc5AuDMwNI48w_-THsJM1CoGx3G8CtP-N7fcdBBtQ
[2026-02-03 14:28:39] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770147219&hash=ARmjcb4sF70-VmiQiXeLl3p8XSaAjjGF9zbh339aUlnFOg
[2026-02-03 14:34:25] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:34:25] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:34:25] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 14:34:25] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770147565&hash=ARk8Q6oLaHEvxv7WNOGtOwkgB8RDWPs7xk672ACpSuC-3A
[2026-02-03 14:34:25] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770147565&hash=ARkNv8rIgcXUHoj2i9ZwZQBW88W7p1yab6a_arnqGzbJtw
[2026-02-03 14:39:57] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:39:57] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:39:57] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 14:39:58] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770147898&hash=ARnRWSW5FuBObUTXyIRf_5mEV90eLmf4Kx4hft5MAQH7fw
[2026-02-03 14:39:58] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770147898&hash=ARlVjspTqGVhZzOq1D-cMbpj7AXP3bNSacmtV3ZIg_ALqA
[2026-02-03 14:49:54] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:49:54] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:49:54] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 14:49:55] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770148494&hash=ARka8CBIIFFl2uNVBdAnHVJldB8vBjgU0NbvGVDabWBfsw
[2026-02-03 14:49:55] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770148494&hash=ARnz6olwrZgd3SpTw-AbpdOo1lcbFr72hX3T6CcVDX9yzw
[2026-02-03 14:50:00] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:50:00] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:50:00] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 14:50:00] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770148500&hash=ARkBigqY86ltDKoAFz_5B1g5uDgpegUSHtL4OGJeQME8rw
[2026-02-03 14:50:00] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770148500&hash=ARmtsjCTyftZSmwlVDnK0QCoYEzA8-NtYX6avOCtr-belQ
[2026-02-03 15:43:01] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:43:01] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:43:01] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 15:43:02] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151682&hash=ARnTTvbQWWCa7LYWf9S55Dg6MSLjjFKtI7nWR0bN2zvDQQ
[2026-02-03 15:43:02] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151682&hash=ARlo5HOK4ztNMJITAnXP2SkjsasuE9VDkgnxXYFUC-t4ng
[2026-02-03 15:46:16] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:46:16] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:46:16] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 15:46:16] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151876&hash=ARkTtYYdsUFAiZwrYv2rgdGGiEc-D59t4NMElrHDHtA_Vw
[2026-02-03 15:46:16] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151876&hash=ARlQUE5zvswz5tBhKIWtAYtD-E7qr9nTCcpuEoYi3nzDLA
[2026-02-03 15:46:33] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:46:33] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:46:33] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 15:46:34] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151894&hash=ARm8Ao4ZAndoLc5sPn6u44DIOtYTW9ck-en0zxlbgOCIRA
[2026-02-03 15:46:34] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151894&hash=ARlLvqyHLBLf1wJwmDZZTlqQT0BpMvqj_hqmhijSVdCbxw
[2026-02-03 15:47:11] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:47:11] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 15:47:11] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:47:11] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151931&hash=ARmC5M11laF_p8bRXwSW0aUa6BZ5p6HLJ4xFt5i_rhGelg
[2026-02-03 15:47:11] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151931&hash=ARklCUN4q5XJ2DcsvgXtl1clURKnFJGZ9FB1VMXZ4JUTlA
[2026-02-03 15:52:33] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 15:52:33] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:52:33] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:52:34] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770152254&hash=ARkc8acjikfeqPK2EEA-lsA6To-kLYCbC0G1N5I-sgDklw
[2026-02-03 15:52:34] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770152254&hash=ARkq5Bv1Yx0I8YZVnRPl1GY3mRSjiTZsTeKecOm3WWP6fA
[2026-02-03 15:54:38] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:54:38] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:54:38] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 15:54:38] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770152378&hash=ARkd0Smk6IQrA7Z-TtMBMGK080-7tiill2lA697bnJdzQQ
[2026-02-03 15:54:38] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770152378&hash=ARmCvtVqy3cbEBhreSiBlGMz_k0H0C1q3FglE1cAzOMtsA
[2026-02-03 15:54:55] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:54:55] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:54:56] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 15:54:56] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770152396&hash=ARm4I-TH2FgoFVdYU6vyaEldYprMRcQMSUZD7-mteyE2bQ
[2026-02-03 15:54:56] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770152396&hash=ARmySCPJWQXfIqv5WakKH2es6o2rlrkmd_QeRWbDRDhMjA
[2026-02-03 16:17:56] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 16:17:56] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 16:17:56] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 16:17:56] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153776&hash=ARkpL17QRsFrIZ9gL6jP87dZLx9WL80OCkUG2QOBFeE6Iw
[2026-02-03 16:17:56] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153776&hash=ARnVTurshL8o0fva9f7ThLQBCNVvOwYex9n-d0Guelq_3w
[2026-02-03 16:18:30] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 16:18:30] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 16:18:30] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 16:18:30] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153810&hash=ARkFailq-_F7K1gQIj0Tyqh1KJ_mD8HHlC79mPeupj6g7A
[2026-02-03 16:18:30] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153810&hash=ARmcNab_e2NsTTxNcuj_VUmj1_G1h2pdSPXQmEF_T7IZ7A
[2026-02-03 16:20:02] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 16:20:02] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 16:20:02] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 16:20:03] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153902&hash=ARmkQUNq3ncSrBHcO337pHLH9_Mrv89BMga1M52iHJjAAA
[2026-02-03 16:20:03] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153903&hash=ARkcsEFiF-VZtOHz8UIqPbUlftOVB3pxOmlyOhHWvHwNUA
[2026-02-03 16:20:10] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 16:20:10] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 16:20:10] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 16:20:11] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153911&hash=ARlsB6UeRRokyVUn_K3GY56WCvw4M7IiTI-0YYDgWKtDXA
[2026-02-03 16:20:11] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153911&hash=ARnhrJuYCnG-1FpfxhUU638YidJDdCp_9Pusp4nmWCCepQ
[2026-02-03 20:31:47] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
[2026-02-03 20:31:47] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 20:31:47] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 20:31:48] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770169008&hash=ARmSF2NK4GqmW53yaJd3NCLYub_AGbau7EHf5WOOETVxbw
[2026-02-03 20:31:48] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770169008&hash=ARlAZuDk9UsmFsMvn4qiAOOAk45RwETVe04h9vkJdDruXg
+82 -3
View File
@@ -87,6 +87,15 @@ class WhatsAppBotManager {
});
}
// Contador de caracteres para broadcast
const broadcastMessage = document.getElementById('broadcast-message');
const charCount = document.getElementById('char-count');
if (broadcastMessage && charCount) {
broadcastMessage.addEventListener('input', (e) => {
charCount.textContent = e.target.value.length;
});
}
// Cambio de tipo de mensaje
const messageType = document.getElementById('message-type');
if (messageType) {
@@ -695,9 +704,79 @@ class WhatsAppBotManager {
}
async sendBroadcast() {
// Funcionalidad de envío masivo deshabilitada temporalmente desde UI
this.showError('Envío masivo deshabilitado temporalmente');
return;
const selectionType = document.getElementById('broadcast-selection-type').value;
const messageType = document.getElementById('broadcast-message-type').value;
let payload = {
selection_type: selectionType,
message_type: messageType
};
// Validar según tipo de selección
if (selectionType === 'filter') {
payload.filter = document.getElementById('broadcast-filter').value;
} else {
const selectedUsers = $('#broadcast-users').val();
if (!selectedUsers || selectedUsers.length === 0) {
this.showError('Debes seleccionar al menos un usuario');
return;
}
payload.user_ids = selectedUsers;
}
// Validar según tipo de mensaje
if (messageType === 'text') {
const message = document.getElementById('broadcast-message').value;
if (!message.trim()) {
this.showError('El mensaje no puede estar vacío');
return;
}
payload.message = message;
} else {
const templateId = document.getElementById('broadcast-template').value;
if (!templateId) {
this.showError('Debes seleccionar una plantilla');
return;
}
payload.template_id = templateId;
// Recoger variables
const variables = {};
document.querySelectorAll('.broadcast-template-var').forEach(input => {
variables[input.dataset.var] = input.value;
});
payload.template_variables = variables;
}
// Confirmar envío
const targetText = selectionType === 'filter'
? `usuarios filtrados por "${payload.filter}"`
: `${payload.user_ids.length} usuario(s) seleccionado(s)`;
const messageText = messageType === 'text' ? 'mensaje de texto' : 'plantilla';
if (!confirm(`¿Enviar este ${messageText} a ${targetText}?`)) {
return;
}
try {
this.showLoading('Enviando mensajes masivos...');
const result = await this.apiCall('send_broadcast.php', 'POST', payload);
if (result.success) {
this.showSuccess(`✅ Mensajes enviados exitosamente!\nEnviados: ${result.sent_count}\nErrores: ${result.error_count}\nTotal usuarios: ${result.total_users}`);
document.getElementById('broadcast-form').reset();
$('#broadcast-users').val(null).trigger('change');
document.getElementById('broadcast-template-variables').style.display = 'none';
document.getElementById('broadcast-template-preview').style.display = 'none';
} else {
this.showError(result.error || 'Error enviando broadcast');
}
} catch (error) {
this.showError('Error enviando broadcast: ' + error.message);
} finally {
this.hideLoading();
}
}
async saveMenu() {
+754 -34
View File
@@ -112,12 +112,21 @@ class SimpleWhatsAppManager {
case 'users':
this.loadUsers();
break;
case 'admin_users':
this.loadAdminUsers();
break;
case 'send_message':
this.loadconversations();
break;
case 'templates':
this.loadTemplates();
break;
case 'broadcast':
// Recargar plantillas cuando se abre el tab de broadcast
if (typeof loadBroadcastTemplates === 'function') {
loadBroadcastTemplates();
}
break;
case 'menus':
this.loadMenus();
break;
@@ -403,14 +412,19 @@ class SimpleWhatsAppManager {
}
}
async loadUsers() {
this.log('Cargando usuarios');
async loadUsers(page = 1, search = '') {
this.log('Cargando usuarios - página ' + page);
try {
const response = await this.apiCall('get_users.php');
let url = `get_users.php?page=${page}&limit=20`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
const response = await this.apiCall(url);
if (response && response.success) {
this.updateUsersList(response.data || []);
this.updateUsersList(response.data || [], response.pagination || {});
} else {
this.showError('No se pudieron cargar los usuarios');
}
@@ -420,15 +434,17 @@ class SimpleWhatsAppManager {
}
}
updateUsersList(users) {
updateUsersList(users, pagination = {}) {
const container = document.getElementById('users-table');
if (!container) return;
// Guardar datos para uso posterior
this.usersData = users;
this.usersPagination = pagination;
if (users.length === 0) {
container.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No hay usuarios registrados</td></tr>';
this.updateUsersPagination({});
return;
}
@@ -459,6 +475,75 @@ class SimpleWhatsAppManager {
});
container.innerHTML = html;
this.updateUsersPagination(pagination);
}
updateUsersPagination(pagination) {
const paginationContainer = document.getElementById('users-pagination');
if (!paginationContainer) return;
if (!pagination.totalPages || pagination.totalPages <= 1) {
paginationContainer.innerHTML = '';
return;
}
const currentPage = pagination.page || 1;
const totalPages = pagination.totalPages;
const total = pagination.total || 0;
let html = `
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="text-muted">
Mostrando página ${currentPage} de ${totalPages} (${total} usuarios en total)
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item ${!pagination.hasPrev ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(1); return false;">
<i class="fas fa-angle-double-left"></i>
</a>
</li>
<li class="page-item ${!pagination.hasPrev ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(${currentPage - 1}); return false;">
<i class="fas fa-angle-left"></i>
</a>
</li>
`;
// Mostrar páginas alrededor de la actual
const maxPages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxPages / 2));
let endPage = Math.min(totalPages, startPage + maxPages - 1);
if (endPage - startPage < maxPages - 1) {
startPage = Math.max(1, endPage - maxPages + 1);
}
for (let i = startPage; i <= endPage; i++) {
html += `
<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(${i}); return false;">${i}</a>
</li>
`;
}
html += `
<li class="page-item ${!pagination.hasNext ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(${currentPage + 1}); return false;">
<i class="fas fa-angle-right"></i>
</a>
</li>
<li class="page-item ${!pagination.hasNext ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="window.whatsappManager.loadUsers(${totalPages}); return false;">
<i class="fas fa-angle-double-right"></i>
</a>
</li>
</ul>
</nav>
</div>
`;
paginationContainer.innerHTML = html;
}
async loadConversations() {
@@ -716,6 +801,7 @@ class SimpleWhatsAppManager {
const fields = [
{ id: 'whatsapp-token', value: data.whatsapp_token },
{ id: 'phone-number-id', value: data.phone_number_id },
{ id: 'business-account-id', value: data.business_account_id },
{ id: 'webhook-token', value: data.webhook_verify_token },
{ id: 'api-url', value: data.api_url },
{ id: 'business-name', value: data.business_name },
@@ -739,6 +825,7 @@ class SimpleWhatsAppManager {
const system_config = {
token: document.getElementById('whatsapp-token').value,
phone_number_id: document.getElementById('phone-number-id').value,
business_account_id: document.getElementById('business-account-id').value,
webhook_verify_token: document.getElementById('webhook-token').value,
api_url: document.getElementById('api-url').value,
business_name: document.getElementById('business-name').value,
@@ -832,6 +919,9 @@ class SimpleWhatsAppManager {
this.log(`Usuarios cargados: ${users.length}, Plantillas cargadas: ${templates.length}`);
// Guardar plantillas para uso posterior
this.templatesData = templates;
if (users.length > 0) {
this.populateUsersSelect(users);
} else if (userSelect) {
@@ -875,13 +965,15 @@ class SimpleWhatsAppManager {
// Limpiar opciones existentes (excepto la primera)
select.innerHTML = '<option value="">Seleccionar plantilla...</option>';
// Agregar plantillas
templates.forEach(template => {
const option = document.createElement('option');
option.value = template.template_name;
option.textContent = `${template.name} - ${template.category || 'Sin descripción'}`;
select.appendChild(option);
});
// Agregar plantillas (solo las aprobadas)
templates
.filter(template => template.status && template.status.toUpperCase() === 'APPROVED')
.forEach(template => {
const option = document.createElement('option');
option.value = template.id; // Usar ID en lugar de template_name
option.textContent = `${template.name} - ${template.category || 'Sin descripción'}`;
select.appendChild(option);
});
}
async loadTemplates() {
@@ -1367,38 +1459,90 @@ class SimpleWhatsAppManager {
return;
}
// Prepare template payload; support named parameters (e.g. { name: 'Juan' })
const tplName = templateSelect.value;
let tplParams = null;
// Obtener ID y nombre de la plantilla
const templateId = templateSelect.value;
const selectedOption = templateSelect.options[templateSelect.selectedIndex];
// If there's a hidden input with JSON parameters (index.php), use it
const paramsInput = document.getElementById('template-params');
if (paramsInput && paramsInput.value) {
try {
tplParams = JSON.parse(paramsInput.value);
} catch (e) {
console.warn('Invalid JSON in #template-params, ignoring', e);
tplParams = null;
// Buscar la plantilla en templatesData para obtener el template_name
let templateName = null;
if (this.templatesData) {
const template = this.templatesData.find(t => t.id == templateId);
if (template) {
templateName = template.template_name;
}
}
// If no params provided and template is contacto_nuevo, prompt operator for name
if ((!tplParams || (typeof tplParams === 'object' && Object.keys(tplParams).length === 0)) && tplName === 'contacto_nuevo') {
const defaultName = '';
const inputName = prompt('Ingrese el nombre para la plantilla contacto_nuevo (dejar vacío para usar el número):', defaultName);
if (inputName === null) {
// user cancelled
return;
if (!templateName) {
this.showError('No se pudo obtener el nombre de la plantilla');
return;
}
// Recopilar variables de los inputs
let tplParams = {};
const varInputs = document.querySelectorAll('.message-template-var');
if (varInputs.length > 0) {
console.log(`🔍 Recopilando ${varInputs.length} variables de la plantilla`);
// Detectar si las variables son numéricas o con nombres
let hasNumericVars = false;
let hasNamedVars = false;
varInputs.forEach(input => {
const placeholder = input.dataset.placeholder || '';
if (/^\{\{\d+\}\}$/.test(placeholder)) {
hasNumericVars = true;
} else {
hasNamedVars = true;
}
});
console.log(`📊 Tipo de variables detectado: ${hasNumericVars ? 'numéricas' : 'con nombres'}`);
if (hasNumericVars && !hasNamedVars) {
// Variables numéricas: crear array ordenado por índice
const tempObj = {};
varInputs.forEach(input => {
const index = parseInt(input.dataset.var);
tempObj[index] = input.value || '';
});
// Convertir a array ordenado
const sortedKeys = Object.keys(tempObj).map(Number).sort((a, b) => a - b);
tplParams = sortedKeys.map(key => tempObj[key]);
console.log('📊 Enviando como array ordenado:', tplParams);
} else {
// Variables con nombres: crear objeto
varInputs.forEach(input => {
const placeholder = input.dataset.placeholder || '';
// Extraer nombre de la variable: {{nombre_tema}} -> nombre_tema
const varName = placeholder.replace(/\{\{|\}\}/g, '');
tplParams[varName] = input.value || '';
});
console.log('📦 Enviando como objeto con nombres:', tplParams);
}
} else {
console.log('️ Plantilla sin variables');
}
// Si hay input oculto con parámetros (para compatibilidad), usarlo
const paramsInput = document.getElementById('template-params');
if (paramsInput && paramsInput.value) {
try {
const hiddenParams = JSON.parse(paramsInput.value);
if (Object.keys(hiddenParams).length > 0) {
tplParams = hiddenParams;
console.log('📝 Usando parámetros del input oculto:', tplParams);
}
} catch (e) {
console.warn('Invalid JSON in #template-params, ignoring', e);
}
const clean = (typeof inputName === 'string') ? inputName.trim() : '';
tplParams = { name: clean || recipient };
}
messageData = {
recipient: recipient,
type: 'template',
template_name: tplName,
parameters: tplParams || {}
template_name: templateName, // Usar template_name en lugar de ID
parameters: tplParams
};
}
@@ -1651,6 +1795,164 @@ class SimpleWhatsAppManager {
this.showError('Error eliminando plantilla: ' + error.message);
}
}
async sendBroadcast() {
const selectionType = document.getElementById('broadcast-selection-type').value;
const messageType = document.getElementById('broadcast-message-type').value;
let payload = {
selection_type: selectionType,
message_type: messageType
};
// Validar según tipo de selección
if (selectionType === 'filter') {
payload.filter = document.getElementById('broadcast-filter').value;
} else {
const selectedUsers = $('#broadcast-users').val();
if (!selectedUsers || selectedUsers.length === 0) {
this.showError('Debes seleccionar al menos un usuario');
return;
}
payload.user_ids = selectedUsers;
}
// Validar según tipo de mensaje
if (messageType === 'text') {
const message = document.getElementById('broadcast-message').value;
if (!message.trim()) {
this.showError('El mensaje no puede estar vacío');
return;
}
payload.message = message;
} else {
const templateId = document.getElementById('broadcast-template').value;
if (!templateId) {
this.showError('Debes seleccionar una plantilla');
return;
}
payload.template_id = templateId;
// Recoger y validar variables
const variablesInputs = document.querySelectorAll('.broadcast-template-var');
let hasEmpty = false;
// Detectar si las variables son numéricas o con nombres usando el placeholder
const firstInput = variablesInputs[0];
const placeholder = firstInput?.dataset?.placeholder || '';
// Si el placeholder es {{1}}, {{2}}, etc. -> numéricas
// Si es {{nombre_tema}}, {{fecha}}, etc. -> con nombres
const isNumericVar = /^\{\{\d+\}\}$/.test(placeholder);
console.log('🔍 Detectando tipo de variables:');
console.log(' - Placeholder ejemplo:', placeholder);
console.log(' - Es numérica?', isNumericVar);
let variables;
if (isNumericVar) {
// Variables numéricas {{1}}, {{2}} - usar array ordenado
const tempVars = {};
variablesInputs.forEach(input => {
const varIndex = parseInt(input.dataset.var);
if (!input.value.trim()) {
input.classList.add('is-invalid');
hasEmpty = true;
} else {
input.classList.remove('is-invalid');
tempVars[varIndex] = input.value;
}
});
if (hasEmpty) {
this.showError('Por favor completa todos los campos de variables de la plantilla');
return;
}
// Convertir a array ordenado por índice
variables = [];
const sortedKeys = Object.keys(tempVars).map(k => parseInt(k)).sort((a, b) => a - b);
sortedKeys.forEach(key => {
variables.push(tempVars[key]);
});
console.log('📝 Variables numéricas recopiladas:', tempVars);
console.log('📊 Array ordenado:', variables);
} else {
// Variables con nombres {{fecha}}, {{motivo}} - usar objeto
variables = {};
variablesInputs.forEach(input => {
const placeholder = input.dataset.placeholder || '';
const varName = placeholder.replace(/\{\{|\}\}/g, ''); // Extraer nombre de {{fecha}}
if (!input.value.trim()) {
input.classList.add('is-invalid');
hasEmpty = true;
} else {
input.classList.remove('is-invalid');
variables[varName] = input.value;
}
});
if (hasEmpty) {
this.showError('Por favor completa todos los campos de variables de la plantilla');
return;
}
console.log('📝 Variables con nombres recopiladas:', variables);
}
payload.template_variables = variables;
}
// Confirmar envío
const targetText = selectionType === 'filter'
? `usuarios filtrados por "${payload.filter}"`
: `${payload.user_ids.length} usuario(s) seleccionado(s)`;
const messageText = messageType === 'text' ? 'mensaje de texto' : 'plantilla';
if (!confirm(`¿Enviar este ${messageText} a ${targetText}?`)) {
return;
}
try {
this.showInfo('Enviando mensajes masivos...');
const result = await this.apiCall('send_broadcast.php', {
method: 'POST',
body: payload
});
if (result.success) {
this.showSuccess(`✅ Mensajes enviados exitosamente!\nEnviados: ${result.sent_count}\nErrores: ${result.error_count}\nTotal usuarios: ${result.total_users}`);
document.getElementById('broadcast-form').reset();
$('#broadcast-users').val(null).trigger('change');
document.getElementById('broadcast-template-variables').style.display = 'none';
document.getElementById('broadcast-template-preview').style.display = 'none';
} else {
this.showError(result.error || 'Error enviando broadcast');
}
} catch (error) {
this.showError('Error enviando broadcast: ' + error.message);
}
}
// Cargar usuarios administradores del sistema
async loadAdminUsers() {
this.log('Cargando usuarios administradores...');
try {
const response = await this.apiCall('list_admin_users.php');
if (response && response.success && response.data) {
renderAdminUsersTable(response.data);
} else {
this.showError('Error cargando usuarios administradores');
}
} catch (error) {
this.log('Error cargando usuarios administradores: ' + error.message, 'error');
this.showError('Error cargando usuarios: ' + error.message);
}
}
}
// Inicializar cuando el DOM esté listo
@@ -2839,9 +3141,121 @@ window.toggleMessageType = function () {
} else {
textGroup.style.display = 'block';
templateGroup.style.display = 'none';
// Ocultar variables y preview cuando no es plantilla
const varsContainer = document.getElementById('message-template-variables');
const previewContainer = document.getElementById('message-template-preview');
if (varsContainer) varsContainer.style.display = 'none';
if (previewContainer) previewContainer.style.display = 'none';
}
};
// Cargar detalles de plantilla para send_message
window.loadMessageTemplateDetails = async function() {
console.log('🔵 loadMessageTemplateDetails llamado');
const select = document.getElementById('message-template');
const templateId = select.value;
console.log('Template ID seleccionado:', templateId);
if (!templateId) {
document.getElementById('message-template-variables').style.display = 'none';
document.getElementById('message-template-preview').style.display = 'none';
return;
}
try {
const response = await window.whatsappManager.apiCall(`get_template_details.php?id=${templateId}`);
console.log('✅ Detalles de plantilla:', response);
if (response && response.success) {
const template = response.template || response.data;
if (!template) {
console.error('❌ No se encontró template en la respuesta');
return;
}
console.log('📋 Template data:', template);
console.log('📋 Variables:', template.variables);
const variablesContainer = document.getElementById('message-template-variables');
if (template.variables && template.variables.length > 0) {
console.log(`✅ Se encontraron ${template.variables.length} variables`);
let html = '<label class="form-label fw-bold"><i class="fas fa-edit"></i> Variables de la Plantilla</label>';
template.variables.forEach((variable, index) => {
const varLabel = variable.label || variable.name || `Variable ${variable.index || index + 1}`;
const placeholder = variable.placeholder || `{{${variable.index || index + 1}}}`;
const example = variable.example || '';
html += `
<div class="mb-2">
<label class="form-label small text-muted">${varLabel}</label>
<input
type="text"
class="form-control form-control-sm message-template-var"
data-var="${variable.index || index + 1}"
data-placeholder="${placeholder}"
placeholder="${example || 'Ingrese ' + varLabel.toLowerCase()}"
oninput="updateMessagePreview()"
>
</div>
`;
});
console.log('📝 HTML generado para variables:', html);
variablesContainer.innerHTML = html;
variablesContainer.style.display = 'block';
console.log('✅ Variables container mostrado');
} else {
console.log('⚠️ Template sin variables');
variablesContainer.innerHTML = '<p class="text-muted small">Esta plantilla no tiene variables</p>';
variablesContainer.style.display = 'block';
}
updateMessagePreview();
} else {
console.error('❌ Respuesta sin success:', response);
}
} catch (error) {
console.error('❌ Error cargando detalles de plantilla:', error);
window.whatsappManager.showError('Error cargando plantilla');
}
};
// Actualizar preview de plantilla para send_message
window.updateMessagePreview = async function() {
const templateId = document.getElementById('message-template').value;
if (!templateId) return;
// Recolectar variables como array (por índice)
const variablesArray = [];
document.querySelectorAll('.message-template-var').forEach(input => {
const index = parseInt(input.dataset.var) - 1; // Convertir a índice de array (0-based)
variablesArray[index] = input.value || `{{${input.dataset.var}}}`;
});
try {
const response = await window.whatsappManager.apiCall('preview_template.php', {
method: 'POST',
body: { template_id: templateId, parameters: variablesArray }
});
if (response && response.success && response.preview) {
const previewContainer = document.getElementById('message-template-preview');
const previewContent = document.getElementById('message-preview-content');
// Usar el HTML pre-formateado del servidor
previewContent.innerHTML = response.preview.html || response.preview.body.replace(/\\n/g, '<br>');
previewContainer.style.display = 'block';
} else {
console.error('Error en preview:', response);
}
} catch (error) {
console.error('Error generando vista previa:', error);
}
};
// Función de diagnóstico
window.runDiagnostic = function () {
const resultsDiv = document.getElementById('diagnostic-results');
@@ -4113,6 +4527,90 @@ window.exportUsers = function() {
}
};
// Función para buscar usuarios con debounce
let searchUsersTimeout;
window.searchUsers = function() {
clearTimeout(searchUsersTimeout);
searchUsersTimeout = setTimeout(() => {
const searchInput = document.getElementById('users-search');
const searchTerm = searchInput ? searchInput.value.trim() : '';
if (window.whatsappManager && window.whatsappManager.loadUsers) {
window.whatsappManager.loadUsers(1, searchTerm);
}
}, 500);
};
// Función para sincronizar plantillas desde Facebook
window.syncTemplatesFromFacebook = async function() {
console.log('Sincronizando plantillas desde Facebook...');
if (!window.whatsappManager) {
alert('Error: Sistema no inicializado');
return;
}
const btn = document.getElementById('btn-sync-templates');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sincronizando...';
}
try {
window.whatsappManager.showInfo('Sincronizando plantillas desde Facebook/WhatsApp Business...');
const response = await window.whatsappManager.apiCall('sync_templates_from_facebook.php', {
method: 'POST'
});
if (response && response.success) {
const data = response.data || {};
const message = `
✅ Sincronización completada exitosamente:
${data.synced || 0} plantillas nuevas
${data.updated || 0} plantillas actualizadas
${data.skipped || 0} sin cambios
${data.errors && data.errors.length > 0 ? '\n⚠️ ' + data.errors.length + ' errores encontrados' : ''}
`;
window.whatsappManager.showSuccess(message);
// Recargar la lista de plantillas
if (window.whatsappManager.loadTemplates) {
window.whatsappManager.loadTemplates();
}
// Mostrar errores si existen
if (data.errors && data.errors.length > 0) {
console.warn('Errores durante la sincronización:', data.errors);
}
} else {
const errorMsg = response?.error || 'Error desconocido al sincronizar';
window.whatsappManager.showError(`Error al sincronizar plantillas: ${errorMsg}`);
// Si el error es por falta de WABA ID, dar instrucciones
if (errorMsg.includes('Business Account ID')) {
setTimeout(() => {
alert('💡 Para sincronizar plantillas necesitas configurar el Business Account ID (WABA ID).\n\n' +
'1. Ve a la pestaña "Configuración"\n' +
'2. Ingresa tu WABA ID en el campo correspondiente\n' +
'3. Guarda la configuración\n' +
'4. Vuelve a intentar la sincronización\n\n' +
'Puedes encontrar tu WABA ID en Meta Business Manager.');
}, 500);
}
}
} catch (error) {
console.error('Error sincronizando plantillas:', error);
window.whatsappManager.showError('Error de conexión al sincronizar plantillas: ' + error.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-sync-alt"></i> Sincronizar desde Facebook';
}
}
};
// Función para refrescar logs
window.refreshLogs = function() {
console.log('Refrescando logs...');
@@ -4190,3 +4688,225 @@ window.clearLogs = async function() {
alert('Error: Sistema no inicializado');
}
};
// ============================================
// GESTIÓN DE USUARIOS ADMINISTRADORES
// ============================================
// Renderizar tabla de usuarios administradores
function renderAdminUsersTable(users) {
const tbody = document.getElementById('admin-users-table');
if (!tbody) return;
if (!users || users.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center text-muted">
<i class="fas fa-inbox"></i> No hay usuarios registrados
</td>
</tr>
`;
return;
}
tbody.innerHTML = users.map(user => {
const isActive = parseInt(user.is_active) === 1;
const lastLogin = user.last_login ?
new Date(user.last_login).toLocaleString('es-CO') :
'<span class="text-muted">Nunca</span>';
const createdAt = new Date(user.created_at).toLocaleDateString('es-CO');
return `
<tr>
<td><strong>${escapeHtml(user.username)}</strong></td>
<td>${escapeHtml(user.full_name || '-')}</td>
<td>${escapeHtml(user.email || '-')}</td>
<td>
<span class="badge bg-${isActive ? 'success' : 'secondary'}">
${isActive ? 'Activo' : 'Inactivo'}
</span>
</td>
<td><small>${lastLogin}</small></td>
<td><small>${createdAt}</small></td>
<td>
<button class="btn btn-sm btn-primary"
onclick="editAdminUser(${user.id})"
title="Editar">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-warning"
onclick="changeAdminPassword(${user.id}, '${escapeHtml(user.username)}')"
title="Cambiar contraseña">
<i class="fas fa-key"></i>
</button>
<button class="btn btn-sm btn-${isActive ? 'secondary' : 'success'}"
onclick="toggleAdminUserStatus(${user.id}, ${!isActive})"
title="${isActive ? 'Desactivar' : 'Activar'}">
<i class="fas fa-${isActive ? 'ban' : 'check'}"></i>
</button>
</td>
</tr>
`;
}).join('');
}
// Mostrar modal para crear usuario
window.showCreateAdminUserModal = function() {
const modal = new bootstrap.Modal(document.getElementById('adminUserModal'));
document.getElementById('admin-user-modal-title').textContent = 'Nuevo Usuario';
document.getElementById('admin-user-form').reset();
document.getElementById('admin-user-id').value = '';
document.getElementById('password-group').style.display = 'block';
modal.show();
};
// Editar usuario existente
window.editAdminUser = async function(userId) {
try {
const response = await window.whatsappManager.apiCall('list_admin_users.php');
if (response && response.success && response.data) {
const user = response.data.find(u => parseInt(u.id) === parseInt(userId));
if (user) {
document.getElementById('admin-user-modal-title').textContent = 'Editar Usuario';
document.getElementById('admin-user-id').value = user.id;
document.getElementById('admin-username').value = user.username;
document.getElementById('admin-fullname').value = user.full_name || '';
document.getElementById('admin-email').value = user.email || '';
document.getElementById('password-group').style.display = 'none';
const modal = new bootstrap.Modal(document.getElementById('adminUserModal'));
modal.show();
}
}
} catch (error) {
console.error('Error cargando usuario:', error);
alert('Error al cargar datos del usuario');
}
};
// Guardar usuario (crear o actualizar)
window.saveAdminUser = async function() {
const userId = document.getElementById('admin-user-id').value;
const username = document.getElementById('admin-username').value.trim();
const fullName = document.getElementById('admin-fullname').value.trim();
const email = document.getElementById('admin-email').value.trim();
const password = document.getElementById('admin-password').value;
if (!username) {
alert('El nombre de usuario es requerido');
return;
}
// Si es nuevo usuario, validar contraseña
if (!userId && !password) {
alert('La contraseña es requerida para usuarios nuevos');
return;
}
if (!userId && password.length < 6) {
alert('La contraseña debe tener al menos 6 caracteres');
return;
}
try {
let response;
if (userId) {
// Actualizar usuario existente
response = await window.whatsappManager.apiCall('update_admin_user.php', {
method: 'POST',
body: { user_id: userId, username, full_name: fullName, email }
});
} else {
// Crear nuevo usuario
response = await window.whatsappManager.apiCall('create_admin_user.php', {
method: 'POST',
body: { username, full_name: fullName, email, password }
});
}
if (response && response.success) {
window.whatsappManager.showSuccess(response.message || 'Usuario guardado correctamente');
bootstrap.Modal.getInstance(document.getElementById('adminUserModal')).hide();
window.whatsappManager.loadAdminUsers();
} else {
alert(response?.error || 'Error al guardar usuario');
}
} catch (error) {
console.error('Error guardando usuario:', error);
alert('Error al guardar usuario: ' + error.message);
}
};
// Cambiar contraseña de usuario
window.changeAdminPassword = function(userId, username) {
document.getElementById('change-password-user-id').value = userId;
document.getElementById('change-password-username').textContent = username;
document.getElementById('new-password').value = '';
document.getElementById('confirm-password').value = '';
const modal = new bootstrap.Modal(document.getElementById('changePasswordModal'));
modal.show();
};
// Guardar nueva contraseña
window.saveNewPassword = async function() {
const userId = document.getElementById('change-password-user-id').value;
const newPassword = document.getElementById('new-password').value;
const confirmPassword = document.getElementById('confirm-password').value;
if (!newPassword || newPassword.length < 6) {
alert('La contraseña debe tener al menos 6 caracteres');
return;
}
if (newPassword !== confirmPassword) {
alert('Las contraseñas no coinciden');
return;
}
try {
const response = await window.whatsappManager.apiCall('change_admin_password.php', {
method: 'POST',
body: { user_id: userId, new_password: newPassword }
});
if (response && response.success) {
window.whatsappManager.showSuccess('Contraseña actualizada correctamente');
bootstrap.Modal.getInstance(document.getElementById('changePasswordModal')).hide();
} else {
alert(response?.error || 'Error al cambiar contraseña');
}
} catch (error) {
console.error('Error cambiando contraseña:', error);
alert('Error al cambiar contraseña: ' + error.message);
}
};
// Activar/Desactivar usuario
window.toggleAdminUserStatus = async function(userId, isActive) {
const action = isActive ? 'activar' : 'desactivar';
if (!confirm(`¿Está seguro de ${action} este usuario?`)) {
return;
}
try {
const response = await window.whatsappManager.apiCall('toggle_admin_user_status.php', {
method: 'POST',
body: { user_id: userId, is_active: isActive }
});
if (response && response.success) {
window.whatsappManager.showSuccess(`Usuario ${isActive ? 'activado' : 'desactivado'} correctamente`);
window.whatsappManager.loadAdminUsers();
} else {
alert(response?.error || `Error al ${action} usuario`);
}
} catch (error) {
console.error(`Error ${action}ando usuario:`, error);
alert(`Error al ${action} usuario: ` + error.message);
}
};
+219 -4
View File
@@ -1119,21 +1119,25 @@ if (empty($user_id)) {
option.value = template.name;
option.textContent = `${display} (${lang})`;
option.dataset.language = lang;
option.dataset.templateId = template.id || '';
templateSelect.appendChild(option);
});
// Valor por defecto global para la plantilla seleccionada
window.language = 'en_US';
window.currentTemplateId = null;
// Actualizar el language global cuando cambie la plantilla seleccionada
templateSelect.addEventListener('change', function() {
const sel = this.selectedOptions[0];
window.language = sel && sel.dataset && sel.dataset.language ? sel.dataset.language : 'en_US';
window.currentTemplateId = sel && sel.dataset && sel.dataset.templateId ? sel.dataset.templateId : null;
});
// Si hay una plantilla seleccionada por defecto, establecer language acorde
if (templateSelect.selectedOptions.length && templateSelect.selectedOptions[0].dataset.language) {
window.language = templateSelect.selectedOptions[0].dataset.language;
window.currentTemplateId = templateSelect.selectedOptions[0].dataset.templateId;
}
}
} catch (error) {
@@ -1167,7 +1171,13 @@ if (empty($user_id)) {
return;
}
await sendTemplateMessage(template,language, message);
// Verificar si la plantilla tiene variables
if (window.currentTemplateId) {
await checkTemplateVariables(window.currentTemplateId, template, language);
} else {
// Plantilla sin variables, enviar directo
await sendTemplateMessage(template, language, message);
}
} else {
await sendTextMessage(message);
}
@@ -1216,22 +1226,33 @@ if (empty($user_id)) {
}
// Enviar mensaje de plantilla
async function sendTemplateMessage(template,language, parameters) {
async function sendTemplateMessage(template, language, parameters) {
try {
showTyping();
console.log('Enviando plantilla:', {
template,
language,
parameters,
user: currentUser
});
// Preparar parámetros según el formato
let paramArray = [];
if (Array.isArray(parameters)) {
paramArray = parameters;
} else if (typeof parameters === 'string' && parameters) {
paramArray = parameters.split(',').map(p => p.trim());
} else {
paramArray = [];
}
const requestBody = {
recipient: currentUser.phone_number,
type: 'template',
template: template,
language: language || 'en_US',
parameters: parameters ? parameters.split(',') : []
parameters: paramArray
};
console.log('Request body:', requestBody);
@@ -1245,7 +1266,10 @@ if (empty($user_id)) {
hideTyping();
if (response && response.success) {
addMessageToView(`Plantilla: ${template}`, 'outgoing');
const displayMsg = paramArray.length > 0 ?
`Plantilla: ${template} (con ${paramArray.length} variable(s))` :
`Plantilla: ${template}`;
addMessageToView(displayMsg, 'outgoing');
showAlert('Mensaje de plantilla enviado correctamente', 'success');
} else {
throw new Error(response.error || 'Error enviando plantilla');
@@ -1925,6 +1949,197 @@ if (empty($user_id)) {
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
}
// Variables globales para el modal de plantillas
let currentTemplateData = null;
// Verificar si la plantilla tiene variables
async function checkTemplateVariables(templateId, templateName, language) {
try {
const response = await whatsappManager.apiCall(`get_template_details.php?id=${templateId}`);
if (response && response.success && response.template) {
currentTemplateData = response.template;
if (response.template.has_variables) {
// Mostrar modal para llenar variables
showTemplateVariablesModal(response.template);
} else {
// Enviar directo sin variables
await sendTemplateMessage(templateName, language, []);
}
} else {
throw new Error('No se pudo obtener detalles de la plantilla');
}
} catch (error) {
console.error('Error checking template variables:', error);
showAlert('Error al cargar detalles de plantilla: ' + error.message, 'danger');
}
}
// Mostrar modal con campos para variables
function showTemplateVariablesModal(template) {
// Actualizar información del encabezado
document.getElementById('templateNameDisplay').textContent = template.name;
document.getElementById('templateLanguageDisplay').textContent = `Idioma: ${template.language_code}`;
document.getElementById('templateVariablesCount').textContent = `${template.variables_count} variable(s) requerida(s)`;
// Generar formulario de variables
const formContainer = document.getElementById('variablesForm');
formContainer.innerHTML = '';
template.variables.forEach((variable, index) => {
const fieldHtml = `
<div class="mb-3">
<label class="form-label">
<strong>${variable.label}</strong>
${variable.example ? `<small class="text-muted">(ej: ${variable.example})</small>` : ''}
</label>
<input type="text"
class="form-control template-variable-input"
data-index="${index}"
data-var-number="${variable.index}"
placeholder="${variable.example || 'Ingrese valor...'}"
required>
</div>
`;
formContainer.insertAdjacentHTML('beforeend', fieldHtml);
});
// Event listeners para actualizar preview en tiempo real
const inputs = formContainer.querySelectorAll('.template-variable-input');
inputs.forEach(input => {
input.addEventListener('input', updateTemplatePreview);
});
// Inicializar preview vacío
updateTemplatePreview();
// Mostrar modal
const modal = new bootstrap.Modal(document.getElementById('templateVariablesModal'));
modal.show();
// Event listener para botón de envío
document.getElementById('btnSendTemplateWithVariables').onclick = sendTemplateWithVariables;
}
// Actualizar vista previa de la plantilla
async function updateTemplatePreview() {
if (!currentTemplateData) return;
const inputs = document.querySelectorAll('.template-variable-input');
const parameters = [];
inputs.forEach(input => {
parameters.push(input.value || '');
});
try {
const response = await whatsappManager.apiCall('preview_template.php', {
body: {
template_id: currentTemplateData.id,
parameters: parameters
}
});
if (response && response.success && response.preview) {
const previewContainer = document.getElementById('templatePreview');
if (response.preview.is_complete) {
previewContainer.innerHTML = response.preview.html;
previewContainer.className = 'p-3 bg-light rounded border border-success';
} else {
previewContainer.innerHTML = response.preview.html +
'<div class="text-warning mt-2"><small><i class="fas fa-exclamation-triangle"></i> Faltan variables por completar</small></div>';
previewContainer.className = 'p-3 bg-light rounded border border-warning';
}
}
} catch (error) {
console.error('Error updating preview:', error);
}
}
// Enviar plantilla con variables
async function sendTemplateWithVariables() {
if (!currentTemplateData) return;
const inputs = document.querySelectorAll('.template-variable-input');
const parameters = [];
let allFilled = true;
inputs.forEach(input => {
const value = input.value.trim();
if (!value) {
allFilled = false;
input.classList.add('is-invalid');
} else {
input.classList.remove('is-invalid');
}
parameters.push(value);
});
if (!allFilled) {
showAlert('Por favor complete todas las variables requeridas', 'warning');
return;
}
// Cerrar modal
const modal = bootstrap.Modal.getInstance(document.getElementById('templateVariablesModal'));
modal.hide();
// Enviar plantilla con parámetros
await sendTemplateMessage(
currentTemplateData.template_name,
currentTemplateData.language_code,
parameters
);
// Limpiar datos
currentTemplateData = null;
}
</script>
<!-- Modal para Variables de Plantilla -->
<div class="modal fade" id="templateVariablesModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title">
<i class="fas fa-edit"></i> Completar Variables de Plantilla
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="templateInfo" class="alert alert-info mb-3">
<strong id="templateNameDisplay"></strong>
<div class="small mt-1">
<span id="templateLanguageDisplay"></span> |
<span id="templateVariablesCount"></span>
</div>
</div>
<div id="variablesForm"></div>
<div class="mt-3">
<h6 class="border-bottom pb-2">
<i class="fas fa-eye"></i> Vista Previa
</h6>
<div id="templatePreview" class="p-3 bg-light rounded" style="min-height: 80px;">
<em class="text-muted">Complete los campos para ver la vista previa...</em>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancelar
</button>
<button type="button" class="btn btn-success" id="btnSendTemplateWithVariables">
<i class="fas fa-paper-plane"></i> Enviar Mensaje
</button>
</div>
</div>
</div>
</div>
</body>
</html>
-153
View File
@@ -1,153 +0,0 @@
#!/bin/bash
# Script de verificación SSE para HestiaCP
# Ejecutar en el servidor: bash check_sse.sh
echo "🔍 Verificación de SSE para HestiaCP"
echo "===================================="
echo ""
# 1. Verificar que el archivo existe
echo "1️⃣ Verificando archivos..."
if [ -f "api/sse_events.php" ]; then
echo " ✅ api/sse_events.php existe"
else
echo " ❌ api/sse_events.php NO encontrado"
echo " → Asegúrate de subir el archivo al servidor"
exit 1
fi
# 2. Verificar permisos
echo ""
echo "2️⃣ Verificando permisos..."
PERMS=$(stat -c "%a" api/sse_events.php 2>/dev/null || stat -f "%A" api/sse_events.php 2>/dev/null)
if [ "$PERMS" -ge "644" ]; then
echo " ✅ Permisos correctos ($PERMS)"
else
echo " ⚠️ Permisos: $PERMS (se recomienda 644)"
echo " Ejecutar: chmod 644 api/sse_events.php"
fi
# 3. Verificar versión PHP
echo ""
echo "3️⃣ Verificando PHP..."
PHP_VERSION=$(php -v | head -n 1)
echo " $PHP_VERSION"
if php -v | grep -q "PHP 8"; then
echo " ✅ PHP 8.x detectado"
elif php -v | grep -q "PHP 7"; then
echo " ⚠️ PHP 7.x (recomendado PHP 8.x)"
else
echo " ❌ Versión PHP desconocida"
fi
# 4. Verificar sintaxis PHP
echo ""
echo "4️⃣ Verificando sintaxis PHP..."
if php -l api/sse_events.php > /dev/null 2>&1; then
echo " ✅ Sin errores de sintaxis"
else
echo " ❌ Error de sintaxis en sse_events.php"
php -l api/sse_events.php
exit 1
fi
# 5. Verificar configuración PHP-FPM
echo ""
echo "5️⃣ Verificando PHP-FPM..."
if systemctl is-active --quiet php8.2-fpm || systemctl is-active --quiet php8.1-fpm || systemctl is-active --quiet php-fpm; then
echo " ✅ PHP-FPM está corriendo"
else
echo " ⚠️ PHP-FPM no detectado o no corriendo"
fi
# 6. Probar conexión SSE local
echo ""
echo "6️⃣ Probando conexión SSE..."
echo " Conectando (3 segundos)..."
RESPONSE=$(timeout 3 curl -N -s 'http://localhost/api/sse_events.php?token=demo_token' 2>&1 || echo "timeout")
if echo "$RESPONSE" | grep -q "event: connected"; then
echo " ✅ SSE funciona correctamente"
echo " Respuesta:"
echo "$RESPONSE" | head -5 | sed 's/^/ /'
elif echo "$RESPONSE" | grep -q "timeout"; then
echo " ⚠️ Timeout - SSE puede no estar configurado correctamente"
echo " → Revisar configuración Apache/Nginx"
elif echo "$RESPONSE" | grep -q "401"; then
echo " ⚠️ HTTP 401 - Problema de autenticación"
echo " → Revisar config/auth.php"
else
echo " ❌ Error inesperado"
echo " Respuesta:"
echo "$RESPONSE" | head -10 | sed 's/^/ /'
fi
# 7. Verificar .htaccess
echo ""
echo "7️⃣ Verificando .htaccess..."
if [ -f "api/.htaccess" ]; then
echo " ✅ api/.htaccess existe"
if grep -q "X-Accel-Buffering" api/.htaccess; then
echo " ✅ Configuración SSE encontrada"
else
echo " ⚠️ Falta configuración SSE en .htaccess"
echo " → Ver GUIA_HESTIA_SSE.md"
fi
else
echo " ⚠️ api/.htaccess no encontrado"
echo " → Crear según GUIA_HESTIA_SSE.md"
fi
# 8. Verificar configuración base de datos
echo ""
echo "8️⃣ Verificando configuración..."
if [ -f "config/config.php" ]; then
echo " ✅ config/config.php existe"
if php -r "require_once 'config/config.php'; echo 'OK';" 2>/dev/null | grep -q "OK"; then
echo " ✅ config.php carga correctamente"
else
echo " ⚠️ Error al cargar config.php"
fi
else
echo " ❌ config/config.php NO encontrado"
exit 1
fi
# 9. Verificar tabla notifications
echo ""
echo "9️⃣ Verificando tabla notifications..."
if php -r "require_once 'config/config.php'; \$db = Database::getInstance(); \$r = \$db->fetchAll('SHOW TABLES LIKE \"notifications\"'); echo (count(\$r) > 0 ? 'OK' : 'NO');" 2>/dev/null | grep -q "OK"; then
echo " ✅ Tabla notifications existe"
else
echo " ⚠️ Tabla notifications no encontrada"
echo " → Ejecutar migration para crear tabla"
fi
# Resumen final
echo ""
echo "===================================="
echo "📊 Resumen"
echo "===================================="
echo ""
if echo "$RESPONSE" | grep -q "event: connected"; then
echo "✅ SSE está funcionando correctamente"
echo ""
echo "Próximos pasos:"
echo "1. Abrir https://tu-dominio.com/conversations.php"
echo "2. Abrir consola del navegador (F12)"
echo "3. Verificar: '✅ SSE conectado'"
echo ""
echo "🚀 Sistema de notificaciones en tiempo real activo!"
else
echo "⚠️ SSE necesita configuración adicional"
echo ""
echo "Próximos pasos:"
echo "1. Revisar GUIA_HESTIA_SSE.md"
echo "2. Configurar .htaccess para SSE"
echo "3. Ajustar PHP-FPM timeout (300s)"
echo "4. Ejecutar: bash check_sse.sh"
fi
echo ""
-48
View File
@@ -1,48 +0,0 @@
<?php
/**
* Ver estructura de la tabla message_templates
*/
require_once 'config/config.php';
header('Content-Type: text/plain; charset=utf-8');
try {
$db = Database::getInstance();
echo "=== ESTRUCTURA DE TABLA message_templates ===\n\n";
// Describir la tabla
$stmt = $db->query("DESCRIBE message_templates");
$columns = $stmt->fetchAll();
echo "Columnas disponibles:\n";
foreach ($columns as $column) {
echo "{$column['Field']} ({$column['Type']}) - {$column['Null']} - {$column['Default']}\n";
}
echo "\n=== INTENTAR INSERCIÓN SIMPLE ===\n";
// Intentar inserción con campos mínimos
$stmt = $db->query(
"INSERT INTO message_templates (template_name, language_code, status) VALUES (?, ?, ?)",
['hello_world', 'en_US', 'approved']
);
if ($stmt) {
echo "✅ Plantilla 'hello_world' agregada exitosamente\n";
}
// Verificar que se agregó
echo "\n📋 Plantillas en la BD:\n";
$stmt = $db->query("SELECT * FROM message_templates");
$templates = $stmt->fetchAll();
foreach ($templates as $template) {
echo "{$template['template_name']} ({$template['language_code']}) - {$template['status']}\n";
}
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage() . "\n";
}
?>
+5
View File
@@ -628,6 +628,10 @@ function saveWhatsAppConfigToDB($whatsappConfig) {
$success &= saveConfigToDB('whatsapp_phone_number_id', $whatsappConfig['phone_number_id']);
}
if (isset($whatsappConfig['business_account_id'])) {
$success &= saveConfigToDB('whatsapp_business_account_id', $whatsappConfig['business_account_id']);
}
if (isset($whatsappConfig['api_url'])) {
$success &= saveConfigToDB('whatsapp_api_url', $whatsappConfig['api_url']);
}
@@ -657,6 +661,7 @@ function getWhatsAppConfigFromDB() {
'token' => getConfigFromDB('whatsapp_token', ''),
'whatsapp_token' => getConfigFromDB('whatsapp_token', ''),
'phone_number_id' => getConfigFromDB('whatsapp_phone_number_id', ''),
'business_account_id' => getConfigFromDB('whatsapp_business_account_id', ''),
'api_url' => getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'),
'whatsapp_api_url' => getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'),
'webhook_verify_token' => getConfigFromDB('webhook_verify_token', ''),
+759 -25
View File
@@ -904,6 +904,10 @@ if (!isset($_SESSION['user_id'])) {
<div id="bot-toggle-container" style="display:flex;align-items:center;gap:6px;margin-right:8px;">
<button class="btn btn-sm btn-outline-secondary" id="bot-toggle">Bot: On</button>
</div>
<!-- Botón programar recordatorio -->
<button class="btn btn-sm btn-success" id="schedule-reminder-btn" title="Programar recordatorio">
<i class="fas fa-calendar-plus"></i>
</button>
<!-- Botón actualizar mensajes -->
<button class="btn btn-sm btn-outline-light" id="refresh-messages-btn" title="Actualizar mensajes"><i class="fas fa-sync-alt"></i></button>
<button class="btn btn-sm btn-outline-secondary" id="mark-unread-btn" title="Marcar conversación como no leída" style="display:none;">Marcar no leído</button>
@@ -1010,9 +1014,9 @@ if (!isset($_SESSION['user_id'])) {
// ESTE LOG DEBE APARECER PRIMERO
console.clear();
console.log('%c═══════════════════════════════════════════', 'color: #25d366; font-weight: bold;');
console.log('%c🚀 WHATSAPP BOT v1.2.0 - Desarrollado por U-Site.app', 'background: #25d366; color: white; padding: 10px 20px; font-size: 18px; font-weight: bold; border-radius: 5px;');
console.log('%c🚀 WHATSAPP BOT v1.3.0 - Desarrollado por U-Site.app', 'background: #25d366; color: white; padding: 10px 20px; font-size: 18px; font-weight: bold; border-radius: 5px;');
console.log('%c═══════════════════════════════════════════', 'color: #25d366; font-weight: bold;');
window.__APP_VERSION__ = '1.2.0';
window.__APP_VERSION__ = '1.3.0';
</script>
<?php
@@ -1030,10 +1034,10 @@ if (!isset($_SESSION['user_id'])) {
<script src="assets/js/chat-common.js?v=<?php echo $asset_v; ?>"></script>
<script>
// ============================================
// 🚀 VERSIÓN ACTUALIZADA - 27 ENERO 2026
// 🚀 VERSIÓN ACTUALIZADA - 3 FEBRERO 2026
// ============================================
console.log('%c📦 Asset Version:', 'font-weight: bold; color: #2575fc;', '<?php echo $asset_v; ?>');
console.log('%c✨ Cambios: SSE mejorado, updateConversationInList con logging completo', 'color: #666;');
console.log('%c✨ Cambios: Gestión de usuarios del sistema, búsqueda mejorada en BD, plantillas optimizadas', 'color: #666;');
console.log('============================================');
// Fallback ligero para showAlert (si no existe una implementación global)
@@ -1783,9 +1787,13 @@ if (!isset($_SESSION['user_id'])) {
}
setupEventListeners() {
// Búsqueda de conversaciones
// Búsqueda de conversaciones con debounce
let searchTimeout;
document.getElementById('search-input').addEventListener('input', (e) => {
this.searchConversations(e.target.value);
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.searchConversations(e.target.value);
}, 500); // Esperar 500ms después de que el usuario deje de escribir
});
// Envío de mensajes
@@ -2257,6 +2265,30 @@ if (!isset($_SESSION['user_id'])) {
});
});
}
// Botón de programar recordatorio
const reminderBtn = document.getElementById('schedule-reminder-btn');
if (reminderBtn) {
reminderBtn.addEventListener('click', () => this.showReminderModal());
}
// Botón de guardar recordatorio
const saveReminderBtn = document.getElementById('save-reminder-btn');
if (saveReminderBtn) {
saveReminderBtn.addEventListener('click', () => this.saveReminder());
}
// Selector de plantilla en recordatorio
const reminderTemplate = document.getElementById('reminder-template');
if (reminderTemplate) {
reminderTemplate.addEventListener('change', () => this.handleReminderTemplateChange());
}
// Botón de enviar plantilla con variables
const sendTemplateWithVarsBtn = document.getElementById('send-template-with-vars-btn');
if (sendTemplateWithVarsBtn) {
sendTemplateWithVarsBtn.addEventListener('click', () => this.sendTemplateWithVariables());
}
}
setupAutoRefresh() {
@@ -4398,6 +4430,309 @@ if (!isset($_SESSION['user_id'])) {
}
}
// ========== FUNCIONES DE RECORDATORIO ==========
async showReminderModal() {
console.log('🔵 showReminderModal - currentUserId:', this.currentUserId);
console.log('📋 conversations:', this.conversations);
if (!this.currentUserId) {
alert('Selecciona una conversación primero');
return;
}
// Buscar usuario en conversaciones - probar múltiples formas
let user = this.conversations.find(c => c.id == this.currentUserId);
if (!user) {
user = this.conversations.find(c => c.user_id == this.currentUserId);
}
console.log('👤 Usuario encontrado:', user);
if (user) {
const userName = user.name || user.user_name || 'Sin nombre';
const phoneNumber = user.phone_number || user.phone || 'Sin teléfono';
document.getElementById('reminder-user-name').value = `${userName} (${phoneNumber})`;
console.log('✅ Usuario establecido:', userName, phoneNumber);
} else {
console.error('❌ Usuario no encontrado en conversaciones');
// Intentar obtener desde el DOM
const nameEl = document.getElementById('chat-name-text') || document.getElementById('chat-name');
const phoneEl = document.getElementById('chat-phone');
if (nameEl && phoneEl) {
document.getElementById('reminder-user-name').value = `${nameEl.textContent} (${phoneEl.textContent})`;
console.log('✅ Usuario obtenido del DOM');
} else {
document.getElementById('reminder-user-name').value = 'Usuario actual';
console.warn('⚠️ No se pudo obtener nombre del usuario');
}
}
// Establecer fecha mínima (hoy)
const today = new Date().toISOString().split('T')[0];
document.getElementById('reminder-date').setAttribute('min', today);
document.getElementById('reminder-date').value = today;
// Cargar plantillas aprobadas
await this.loadReminderTemplates();
// Mostrar modal
const modal = new bootstrap.Modal(document.getElementById('reminderModal'));
modal.show();
}
async loadReminderTemplates() {
try {
const response = await this.apiCall('get_templates.php?approved_only=1');
const select = document.getElementById('reminder-template');
if (response && response.success && response.data) {
select.innerHTML = '<option value="">Seleccionar plantilla...</option>';
response.data.forEach(template => {
const option = document.createElement('option');
option.value = template.id;
option.textContent = `${template.name} (${template.language_code})`;
option.dataset.templateId = template.id;
option.dataset.templateName = template.template_name;
option.dataset.language = template.language_code;
option.dataset.bodyText = template.body_text || '';
// Detectar variables tanto numéricas como con nombres
option.dataset.hasVariables = template.body_text && /\{\{[^\}]+\}\}/.test(template.body_text);
select.appendChild(option);
});
}
} catch (error) {
console.error('Error loading templates:', error);
}
}
async handleReminderTemplateChange() {
const select = document.getElementById('reminder-template');
const selectedOption = select.options[select.selectedIndex];
console.log('🔵 handleReminderTemplateChange llamado');
console.log('📋 Option seleccionada:', selectedOption);
if (!selectedOption || !selectedOption.value) {
document.getElementById('reminder-variables-container').style.display = 'none';
document.getElementById('reminder-preview').style.display = 'none';
return;
}
const hasVariables = selectedOption.dataset.hasVariables === 'true';
const bodyText = selectedOption.dataset.bodyText || '';
console.log('🔍 hasVariables:', hasVariables);
console.log('📝 bodyText:', bodyText);
if (hasVariables) {
// Usar get_template_details.php para obtener variables con sus metadatos
try {
const templateId = selectedOption.dataset.templateId;
const detailsResp = await this.apiCall(`get_template_details.php?id=${templateId}`);
if (detailsResp && detailsResp.success && detailsResp.template) {
const variables = detailsResp.template.variables || [];
console.log('✅ Variables obtenidas de API:', variables);
// Generar campos
const fieldsContainer = document.getElementById('reminder-variables-fields');
fieldsContainer.innerHTML = '';
variables.forEach(v => {
const div = document.createElement('div');
div.className = 'mb-2';
div.innerHTML = `
<label class="form-label small">${v.label}</label>
<input type="text"
class="form-control form-control-sm reminder-variable-input"
data-index="${v.index}"
data-placeholder="${v.placeholder || ''}"
placeholder="${v.example || 'Ingrese valor...'}"
required>
`;
fieldsContainer.appendChild(div);
});
// Event listeners para actualizar preview
fieldsContainer.querySelectorAll('.reminder-variable-input').forEach(input => {
input.addEventListener('input', () => this.updateReminderPreview());
});
}
} catch (error) {
console.error('Error obteniendo detalles de plantilla:', error);
}
document.getElementById('reminder-variables-container').style.display = 'block';
} else {
document.getElementById('reminder-variables-container').style.display = 'none';
}
// Actualizar preview
this.updateReminderPreview();
}
updateReminderPreview() {
const select = document.getElementById('reminder-template');
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption || !selectedOption.value) {
document.getElementById('reminder-preview').style.display = 'none';
return;
}
let bodyText = selectedOption.dataset.bodyText || '';
const hasVariables = selectedOption.dataset.hasVariables === 'true';
if (hasVariables) {
const inputs = document.querySelectorAll('.reminder-variable-input');
inputs.forEach(input => {
const index = input.dataset.index;
const value = input.value || `{{${index}}}`;
bodyText = bodyText.replace(new RegExp(`\\{\\{${index}\\}\\}`, 'g'), value);
});
}
const previewContainer = document.getElementById('reminder-preview-content');
previewContainer.innerHTML = bodyText.replace(/\n/g, '<br>');
document.getElementById('reminder-preview').style.display = 'block';
}
async saveReminder() {
if (!this.currentUserId) return;
const date = document.getElementById('reminder-date').value;
const time = document.getElementById('reminder-time').value;
const select = document.getElementById('reminder-template');
const selectedOption = select.options[select.selectedIndex];
if (!date || !time) {
alert('Por favor completa fecha y hora');
return;
}
if (!selectedOption || !selectedOption.value) {
alert('Por favor selecciona una plantilla');
return;
}
// Recopilar parámetros si hay variables
const hasVariables = selectedOption.dataset.hasVariables === 'true' || selectedOption.dataset.hasVariables === true;
console.log('🔍 hasVariables:', hasVariables, 'tipo:', typeof selectedOption.dataset.hasVariables);
let parameters = null;
if (hasVariables) {
const inputs = document.querySelectorAll('.reminder-variable-input');
console.log('📝 Inputs encontrados:', inputs.length);
if (inputs.length === 0) {
console.log('⚠️ No hay inputs de variables');
} else {
// Detectar tipo de variables usando el placeholder del primer input
const firstInput = inputs[0];
const placeholder = firstInput?.dataset?.placeholder || '';
const isNumericVar = /^\{\{\d+\}\}$/.test(placeholder);
console.log('🔍 Detectando tipo de variables en recordatorio:');
console.log(' - Placeholder ejemplo:', placeholder);
console.log(' - Es numérica?', isNumericVar);
let allFilled = true;
if (isNumericVar) {
// Variables numéricas: crear array ordenado
const tempObj = {};
inputs.forEach(input => {
const value = input.value.trim();
const index = parseInt(input.dataset.index || input.dataset.var);
if (!value) {
allFilled = false;
input.classList.add('is-invalid');
} else {
input.classList.remove('is-invalid');
tempObj[index] = value;
}
});
// Convertir a array ordenado
const sortedKeys = Object.keys(tempObj).map(Number).sort((a, b) => a - b);
parameters = sortedKeys.map(key => tempObj[key]);
console.log('📊 Enviando como array ordenado:', parameters);
} else {
// Variables con nombres: crear objeto
parameters = {};
inputs.forEach(input => {
const value = input.value.trim();
const placeholder = input.dataset.placeholder || '';
const varName = placeholder.replace(/\{\{|\}\}/g, '');
if (!value) {
allFilled = false;
input.classList.add('is-invalid');
} else {
input.classList.remove('is-invalid');
parameters[varName] = value;
}
});
console.log('📦 Enviando como objeto con nombres:', parameters);
}
if (!allFilled) {
alert('Por favor completa todas las variables');
return;
}
}
} else {
console.log('⚠️ Plantilla sin variables');
}
const templateId = selectedOption.dataset.templateId;
const templateName = selectedOption.dataset.templateName;
const language = selectedOption.dataset.language;
const payload = {
user_id: parseInt(this.currentUserId),
message_type: 'template',
template_id: parseInt(templateId),
template_name: templateName,
template_language: language,
template_parameters: parameters,
scheduled_date: date,
scheduled_time: time
};
console.log('💾 Guardando recordatorio:', payload);
console.log('📊 Parámetros detalle:', {
esNull: parameters === null,
esArray: Array.isArray(parameters),
longitud: parameters ? parameters.length : 0,
valores: parameters
});
try {
const response = await this.apiCall('schedule_message.php', {
body: payload
});
console.log('✅ Respuesta del servidor:', response);
if (response && response.success) {
alert('✅ Recordatorio programado exitosamente');
bootstrap.Modal.getInstance(document.getElementById('reminderModal')).hide();
// Limpiar formulario
document.getElementById('reminder-template').value = '';
document.getElementById('reminder-variables-container').style.display = 'none';
document.getElementById('reminder-preview').style.display = 'none';
} else {
alert('Error: ' + (response.error || 'Error desconocido'));
}
} catch (error) {
console.error('Error saving reminder:', error);
alert('Error al programar recordatorio: ' + error.message);
}
}
async loadQuickReplies() {
const panel = document.getElementById('quick-replies-panel');
if (!panel) return;
@@ -4474,8 +4809,210 @@ if (!isset($_SESSION['user_id'])) {
}
async sendTemplateQuick(templateName, language) {
// delegate to unified sendTemplateMessage helper
await this.sendTemplateMessage(templateName, language, null);
console.log('🔵 sendTemplateQuick:', templateName, language);
// Primero, cargar detalles de la plantilla para verificar si tiene variables
try {
// Buscar el template_id desde la caché o hacer una petición
const templatesResp = await this.apiCall('get_templates.php?approved_only=1');
const template = templatesResp.data.find(t => t.template_name === templateName);
if (!template) {
throw new Error('Plantilla no encontrada');
}
console.log('📋 Template encontrado:', template);
// Verificar si tiene variables (tanto numéricas como con nombres)
const hasVariables = template.body_text && /\{\{[^\}]+\}\}/.test(template.body_text);
console.log('🔍 Tiene variables?', hasVariables);
if (hasVariables) {
// Mostrar modal para pedir variables
await this.showTemplateVariablesModal(template);
} else {
// Enviar directamente sin variables
await this.sendTemplateMessage(templateName, language, null);
}
} catch (error) {
console.error('❌ Error en sendTemplateQuick:', error);
alert('Error al cargar plantilla: ' + error.message);
}
}
async showTemplateVariablesModal(template) {
console.log('🔵 showTemplateVariablesModal:', template);
// Guardar template actual para usarlo al enviar
this.currentTemplate = template;
// Establecer nombre de plantilla en modal
document.getElementById('template-modal-name').textContent = template.name || template.template_name;
try {
// Usar get_template_details.php para obtener variables con metadatos
const detailsResp = await this.apiCall(`get_template_details.php?id=${template.id}`);
if (!detailsResp || !detailsResp.success) {
throw new Error('No se pudieron cargar los detalles de la plantilla');
}
const variables = detailsResp.template.variables || [];
console.log('📝 Variables obtenidas:', variables);
// Guardar template completo con variables para usarlo después
this.currentTemplate = { ...template, variables: variables };
// Generar campos de entrada
const container = document.getElementById('template-variables-container');
let html = '';
variables.forEach((variable) => {
html += `
<div class="mb-3">
<label class="form-label"><strong>${variable.label}</strong>
${variable.example ? `<small class="text-muted">(ej: ${variable.example})</small>` : ''}
</label>
<input
type="text"
class="form-control template-var-input"
data-var="${variable.index}"
data-placeholder="${variable.placeholder}"
placeholder="${variable.example || 'Ingrese valor...'}"
onkeyup="window.chatApp.updateTemplatePreview()"
required
>
</div>
`;
});
container.innerHTML = html;
// Generar vista previa inicial
this.updateTemplatePreview();
// Mostrar modal
const modal = new bootstrap.Modal(document.getElementById('templateVariablesModal'));
modal.show();
} catch (error) {
console.error('❌ Error cargando detalles de plantilla:', error);
alert('Error al cargar plantilla: ' + error.message);
}
}
async updateTemplatePreview() {
if (!this.currentTemplate) return;
const previewContainer = document.getElementById('template-preview');
const previewContent = document.getElementById('template-preview-content');
// Recopilar parámetros
const inputs = document.querySelectorAll('.template-var-input');
const parameters = [];
inputs.forEach(input => {
const varIndex = parseInt(input.dataset.var) - 1;
parameters[varIndex] = input.value || '';
});
try {
// Usar preview_template.php para renderizar correctamente
const response = await this.apiCall('preview_template.php', {
body: {
template_id: this.currentTemplate.id,
parameters: parameters
}
});
if (response && response.success && response.preview) {
previewContent.innerHTML = response.preview.html || response.preview.body.replace(/\n/g, '<br>');
previewContainer.style.display = 'block';
}
} catch (error) {
console.error('Error generando preview:', error);
// Fallback: mostrar body_text sin procesar
previewContent.innerHTML = this.currentTemplate.body_text.replace(/\n/g, '<br>');
previewContainer.style.display = 'block';
}
}
async sendTemplateWithVariables() {
console.log('🔵 sendTemplateWithVariables');
if (!this.currentTemplate) {
alert('No hay plantilla seleccionada');
return;
}
// Recopilar variables
const inputs = document.querySelectorAll('.template-var-input');
let allFilled = true;
// Detectar si son variables numéricas o con nombres usando el placeholder
const firstInput = inputs[0];
const placeholder = firstInput?.dataset?.placeholder || '';
// Si el placeholder es {{1}}, {{2}}, etc. -> numéricas
// Si es {{nombre_tema}}, {{fecha}}, etc. -> con nombres
const isNumericVar = /^\{\{\d+\}\}$/.test(placeholder);
console.log('🔍 Detectando tipo de variables:');
console.log(' - Placeholder ejemplo:', placeholder);
console.log(' - Es numérica?', isNumericVar);
let parameters;
if (isNumericVar) {
// Variables numéricas {{1}}, {{2}} - usar array
parameters = [];
inputs.forEach(input => {
const value = input.value.trim();
if (!value) {
allFilled = false;
input.classList.add('is-invalid');
} else {
input.classList.remove('is-invalid');
parameters.push(value);
}
});
console.log('📊 Enviando como array:', parameters);
} else {
// Variables con nombres {{fecha}}, {{motivo}} - usar objeto
parameters = {};
inputs.forEach(input => {
const value = input.value.trim();
const placeholder = input.dataset.placeholder || '';
// Extraer nombre de {{fecha}} -> "fecha"
const varName = placeholder.replace(/\{\{|\}\}/g, '');
if (!value) {
allFilled = false;
input.classList.add('is-invalid');
} else {
input.classList.remove('is-invalid');
parameters[varName] = value;
}
});
console.log('📦 Enviando como objeto:', parameters);
}
if (!allFilled) {
alert('Por favor completa todas las variables');
return;
}
console.log('📝 Parámetros finales:', parameters);
// Cerrar modal
const modal = bootstrap.Modal.getInstance(document.getElementById('templateVariablesModal'));
if (modal) modal.hide();
// Enviar plantilla con parámetros
await this.sendTemplateMessage(
this.currentTemplate.template_name,
this.currentTemplate.language_code || 'es',
parameters
);
// Limpiar template actual
this.currentTemplate = null;
}
async sendTemplateMessage(templateName, language = 'es', parameters = null) {
@@ -4547,10 +5084,49 @@ if (!isset($_SESSION['user_id'])) {
async sendQuickReply(text) {
if (!text) return;
if (!this.currentUserId) return;
const input = document.getElementById('message-input');
if (!input) return;
input.value = text;
await this.sendMessage();
// No copiar al input para preservar saltos de línea
// Enviar directamente usando la API
input.disabled = true;
const sendBtn = document.getElementById('send-btn');
if (sendBtn) sendBtn.disabled = true;
try {
const recipient = this.getConversationPhone(this.currentUserId);
if (!recipient) {
alert('No se pudo determinar el número de destino');
return;
}
const requestBody = {
recipient: recipient,
type: 'text',
message: text
};
const result = await this.apiCall('send_message.php', { body: requestBody });
if (result && result.success) {
// Mostrar inmediatamente en la vista
this.addMessageToView(text, 'outgoing');
typeof showAlert !== 'undefined' && showAlert('Mensaje enviado correctamente', 'success');
// Recargar mensajes en background
this.loadconversations(this.currentUserId, false).catch(e=>console.warn(e));
} else {
throw new Error(result && result.error ? result.error : 'Error desconocido');
}
} catch (error) {
console.error('Error sending quick reply:', error);
alert('Error al enviar respuesta rápida: ' + (error.message || error));
} finally {
input.disabled = false;
if (sendBtn) sendBtn.disabled = false;
input.focus();
}
}
scrollToBottom() {
@@ -4784,7 +5360,52 @@ if (!isset($_SESSION['user_id'])) {
}
}
searchConversations(query) {
async searchConversations(query) {
console.log('🔍 Buscando conversaciones:', query);
// Limpiar búsqueda si el query está vacío
if (!query || query.trim() === '') {
// Recargar conversaciones normales
await this.loadConversations(1, false);
return;
}
// Buscar en la base de datos
try {
const response = await this.apiCall(`get_conversations.php?search=${encodeURIComponent(query.trim())}`);
if (response && response.success) {
this.conversations = response.data || [];
this.currentPage = 1;
this.hasMore = false; // Desactivar paginación en búsqueda
this.renderConversations();
console.log(`✅ Búsqueda completada: ${this.conversations.length} resultados`);
if (this.conversations.length === 0) {
const container = document.getElementById('conversation-list');
container.innerHTML = `
<div style="padding: 20px; text-align: center; color: #999;">
<i class="fas fa-search" style="font-size: 48px; margin-bottom: 10px; opacity: 0.3;"></i>
<p>No se encontraron conversaciones con "${query}"</p>
<small>Intenta buscar por nombre, teléfono o contenido del mensaje</small>
</div>
`;
}
} else {
console.error('Error en búsqueda:', response);
// Fallback a búsqueda local si falla el servidor
this.searchConversationsLocal(query);
}
} catch (error) {
console.error('Error buscando conversaciones:', error);
// Fallback a búsqueda local
this.searchConversationsLocal(query);
}
}
// Búsqueda local (fallback) - solo filtra las conversaciones ya cargadas
searchConversationsLocal(query) {
const items = document.querySelectorAll('.conversation-item');
items.forEach(item => {
const name = item.querySelector('.conversation-name').textContent.toLowerCase();
@@ -5041,6 +5662,9 @@ if (!isset($_SESSION['user_id'])) {
const localThumb = message.local_thumb || '';
const messageId = message.id || message.message_id || '';
// Obtener la URL base del sitio para URLs absolutas
const baseUrl = window.location.origin;
// Debug completo del mensaje
if (mediaType === 'audio' || mediaType === 'image' || mediaType === 'video') {
console.log(`🎵 Media message (${mediaType}) - Datos completos:`, {
@@ -5050,7 +5674,8 @@ if (!isset($_SESSION['user_id'])) {
mediaUrl: mediaUrl,
mediaUrlExternal: mediaUrlExternal,
hasLocalFile: !!localFile,
messageType: mediaType
messageType: mediaType,
baseUrl: baseUrl
});
}
@@ -5059,23 +5684,27 @@ if (!isset($_SESSION['user_id'])) {
let thumb = '';
if (localFile) {
// ✅ PRIORIDAD 1: Archivo local guardado
full = `/${localFile}`;
thumb = localThumb ? `/${localThumb}` : `/${localFile}`;
console.log('✅ Usando archivo local:', full);
// ✅ PRIORIDAD 1: Archivo local guardado (usar URL absoluta)
full = localFile.startsWith('http') ? localFile : `${baseUrl}/${localFile.replace(/^\//, '')}`;
thumb = localThumb
? (localThumb.startsWith('http') ? localThumb : `${baseUrl}/${localThumb.replace(/^\//, '')}`)
: full;
console.log('✅ Usando archivo local (URL absoluta):', full);
} else if (messageId && !mediaUrlExternal) {
// ⚠️ PRIORIDAD 2: Usar proxy con el ID de la base de datos solo si no hay media_url_external
full = `/api/version/media-url.php?id=${encodeURIComponent(messageId)}`;
// ⚠️ PRIORIDAD 2: Usar proxy con el ID de la base de datos (URL absoluta)
full = `${baseUrl}/api/version/media-url.php?id=${encodeURIComponent(messageId)}`;
thumb = full;
console.log('⚠️ No hay local_file, usando proxy con messageId:', messageId);
} else if (mediaUrlExternal) {
// 🔄 PRIORIDAD 3: Usar media_url_external (puede ser api/get_media.php o URL directa)
full = mediaUrlExternal.startsWith('/') ? mediaUrlExternal : `/${mediaUrlExternal}`;
// 🔄 PRIORIDAD 3: Usar media_url_external (convertir a URL absoluta si es relativa)
full = mediaUrlExternal.startsWith('http')
? mediaUrlExternal
: `${baseUrl}/${mediaUrlExternal.replace(/^\//, '')}`;
thumb = full;
console.log('🔄 Usando media_url_external:', full);
console.log('🔄 Usando media_url_external (URL absoluta):', full);
} else if (mediaUrl && /^\d+$/.test(mediaUrl)) {
// 📱 PRIORIDAD 4: Es un ID de WhatsApp (solo números)
full = `/api/get_media.php?id=${encodeURIComponent(mediaUrl)}`;
// 📱 PRIORIDAD 4: Es un ID de WhatsApp (solo números) - usar URL absoluta
full = `${baseUrl}/api/get_media.php?id=${encodeURIComponent(mediaUrl)}`;
thumb = full;
console.log('📱 Usando whatsapp_media_id con get_media.php:', mediaUrl);
} else if (mediaUrl && /^https?:\/\//i.test(mediaUrl)) {
@@ -5115,11 +5744,25 @@ if (!isset($_SESSION['user_id'])) {
case 'audio': {
// Para audio, usar la misma lógica de full
let audioSrc = full;
if (!audioSrc) {
return `
<div class="message-media">
<div class="alert alert-warning mb-0" style="font-size: 13px;">
<i class="fas fa-exclamation-triangle"></i> Audio no disponible
<br><small>El audio puede haber expirado o no está disponible</small>
</div>
</div>
`;
}
return `
<div class="message-media">
<audio controls>
<audio controls controlsList="nodownload" preload="metadata" onerror="this.parentElement.innerHTML='<div class=\\'alert alert-danger mb-0\\' style=\\'font-size:13px;\\'><i class=\\'fas fa-times-circle\\'></i> Error al cargar audio<br><small>Archivo no encontrado o no disponible</small></div>'">
<source src="${escapeHtml(audioSrc)}" type="audio/mpeg">
Tu navegador no soporta audio.
<source src="${escapeHtml(audioSrc)}" type="audio/ogg">
<source src="${escapeHtml(audioSrc)}" type="audio/wav">
Tu navegador no soporta reproducción de audio.
</audio>
</div>
`;
@@ -5364,5 +6007,96 @@ if (!isset($_SESSION['user_id'])) {
</div>
</div>
<!-- Modal para programar recordatorio -->
<div class="modal fade" id="reminderModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title">
<i class="fas fa-calendar-plus"></i> Programar Recordatorio
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Cliente</label>
<input type="text" class="form-control" id="reminder-user-name" readonly>
</div>
<div class="mb-3">
<label class="form-label">Fecha</label>
<input type="date" class="form-control" id="reminder-date" required>
</div>
<div class="mb-3">
<label class="form-label">Hora</label>
<input type="time" class="form-control" id="reminder-time" value="09:00" required>
</div>
<div class="mb-3">
<label class="form-label">Seleccionar Plantilla</label>
<select class="form-select" id="reminder-template" required>
<option value="">Cargando plantillas...</option>
</select>
</div>
<div id="reminder-variables-container" style="display:none;">
<label class="form-label">Variables de la Plantilla</label>
<div id="reminder-variables-fields"></div>
</div>
<div id="reminder-preview" class="mt-3" style="display:none;">
<h6 class="border-bottom pb-2">Vista Previa</h6>
<div id="reminder-preview-content" class="p-3 bg-light rounded"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancelar
</button>
<button type="button" class="btn btn-success" id="save-reminder-btn">
<i class="fas fa-save"></i> Programar Recordatorio
</button>
</div>
</div>
</div>
</div>
<!-- Modal para variables de plantilla (envío normal) -->
<div class="modal fade" id="templateVariablesModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">
<i class="fas fa-edit"></i> Variables de la Plantilla
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<strong id="template-modal-name"></strong>
</div>
<div id="template-variables-container">
<!-- Variables dinámicas -->
</div>
<div id="template-preview" class="mt-3" style="display:none;">
<h6 class="border-bottom pb-2">Vista Previa</h6>
<div id="template-preview-content" class="p-3 bg-light rounded"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancelar
</button>
<button type="button" class="btn btn-primary" id="send-template-with-vars-btn">
<i class="fas fa-paper-plane"></i> Enviar Plantilla
</button>
</div>
</div>
</div>
</div>
</body>
</html>
+45
View File
@@ -0,0 +1,45 @@
-- ============================================
-- MIGRACIÓN: 02_add_template_components
-- Fecha: 2026-02-03
-- Descripción: Agregar columnas para almacenar componentes de plantillas de WhatsApp
-- Esto permite capturar variables/placeholders y generar vistas previas
-- Nota: Usa IF NOT EXISTS - Es seguro ejecutar múltiples veces
-- ============================================
-- Agregar columna para el texto del body de la plantilla
ALTER TABLE message_templates
ADD COLUMN IF NOT EXISTS body_text TEXT DEFAULT NULL
COMMENT 'Texto del cuerpo de la plantilla con variables {{1}}, {{2}}, etc.';
-- Agregar columna para almacenar componentes completos en formato JSON
ALTER TABLE message_templates
ADD COLUMN IF NOT EXISTS components JSON DEFAULT NULL
COMMENT 'Componentes completos de la plantilla desde WhatsApp API (header, body, footer, buttons)';
-- Agregar columna para almacenar ejemplo de parámetros
ALTER TABLE message_templates
ADD COLUMN IF NOT EXISTS example_parameters JSON DEFAULT NULL
COMMENT 'Ejemplos de valores para los parámetros de la plantilla';
-- Agregar columna para el texto del header si existe
ALTER TABLE message_templates
ADD COLUMN IF NOT EXISTS header_text VARCHAR(255) DEFAULT NULL
COMMENT 'Texto del header de la plantilla';
-- Agregar columna para el tipo de header
ALTER TABLE message_templates
ADD COLUMN IF NOT EXISTS header_type ENUM('text', 'image', 'video', 'document') DEFAULT NULL
COMMENT 'Tipo de header de la plantilla';
-- Agregar columna para el texto del footer
ALTER TABLE message_templates
ADD COLUMN IF NOT EXISTS footer_text VARCHAR(255) DEFAULT NULL
COMMENT 'Texto del footer de la plantilla';
-- Agregar índice para búsquedas por plantilla y lenguaje
ALTER TABLE message_templates
ADD INDEX IF NOT EXISTS idx_template_language (template_name, language_code);
-- Actualizar timestamp
ALTER TABLE message_templates
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
+48
View File
@@ -0,0 +1,48 @@
-- ============================================
-- MIGRACIÓN: 03_create_scheduled_messages
-- Fecha: 2026-02-03
-- Descripción: Crear tabla y vista para sistema de mensajes programados/recordatorios
-- Permite programar envío de plantillas WhatsApp en fechas/horas específicas
-- Nota: Usa IF NOT EXISTS / OR REPLACE - Es seguro ejecutar múltiples veces
-- ============================================
-- Tabla para mensajes programados y recordatorios
CREATE TABLE IF NOT EXISTS scheduled_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
template_id INT NULL,
template_name VARCHAR(100) NULL,
template_language VARCHAR(10) DEFAULT 'es',
template_parameters JSON NULL COMMENT 'Parámetros para las variables de la plantilla',
message_type ENUM('text', 'template') DEFAULT 'template',
message_content TEXT NULL COMMENT 'Contenido del mensaje si es tipo text',
scheduled_date DATE NOT NULL,
scheduled_time TIME NOT NULL,
status ENUM('pending', 'sent', 'failed', 'cancelled') DEFAULT 'pending',
sent_at DATETIME NULL,
error_message TEXT NULL,
created_by INT NULL COMMENT 'ID del admin que creó el recordatorio',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_scheduled_date (scheduled_date),
INDEX idx_status (status),
INDEX idx_scheduled_datetime (scheduled_date, scheduled_time, status),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Vista para facilitar consultas con información del usuario
CREATE OR REPLACE VIEW scheduled_messages_view AS
SELECT
sm.*,
u.name as user_name,
u.phone_number,
mt.name as template_display_name,
mt.body_text as template_body,
mt.status as template_status
FROM scheduled_messages sm
LEFT JOIN users u ON sm.user_id = u.id
LEFT JOIN message_templates mt ON sm.template_id = mt.id
ORDER BY sm.scheduled_date DESC, sm.scheduled_time DESC;
+1 -1
View File
@@ -218,7 +218,7 @@ deploy() {
log_info "Construyendo imágenes Docker..."
docker compose -f "$COMPOSE_FILE" build --no-cache
# Detener contenedores existentes
# Detener contenedores existentes (NO borra volúmenes, los datos persisten)
log_info "Deteniendo contenedores anteriores..."
docker compose -f "$COMPOSE_FILE" down --remove-orphans 2>/dev/null || true
-121
View File
@@ -1,121 +0,0 @@
#!/bin/bash
# Script de diagnóstico para errores de fetch
# Fecha: 27 de enero de 2026
echo "🔍 Diagnóstico de errores 'Failed to fetch'"
echo "============================================"
echo ""
BASE_URL="http://localhost:8000"
# Colores
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo "📍 Base URL: $BASE_URL"
echo ""
# Test 1: Verificar que los endpoints existan
echo "1️⃣ Verificando endpoints..."
if [ -f "api/get_conversations.php" ]; then
echo -e " ${GREEN}${NC} api/get_conversations.php existe"
else
echo -e " ${RED}${NC} api/get_conversations.php NO existe"
fi
if [ -f "api/get_notifications.php" ]; then
echo -e " ${GREEN}${NC} api/get_notifications.php existe"
else
echo -e " ${RED}${NC} api/get_notifications.php NO existe"
fi
echo ""
# Test 2: Verificar sintaxis PHP
echo "2️⃣ Verificando sintaxis PHP..."
if php -l api/get_conversations.php > /dev/null 2>&1; then
echo -e " ${GREEN}${NC} get_conversations.php sintaxis correcta"
else
echo -e " ${RED}${NC} get_conversations.php tiene errores de sintaxis"
php -l api/get_conversations.php
fi
if php -l api/get_notifications.php > /dev/null 2>&1; then
echo -e " ${GREEN}${NC} get_notifications.php sintaxis correcta"
else
echo -e " ${RED}${NC} get_notifications.php tiene errores de sintaxis"
php -l api/get_notifications.php
fi
echo ""
# Test 3: Verificar servidor PHP
echo "3️⃣ Verificando servidor PHP..."
if curl -s "$BASE_URL" > /dev/null 2>&1; then
echo -e " ${GREEN}${NC} Servidor PHP respondiendo en $BASE_URL"
else
echo -e " ${RED}${NC} Servidor PHP NO responde en $BASE_URL"
echo " Inicia el servidor con: php -S localhost:8000"
fi
echo ""
# Test 4: Verificar que config.php exista
echo "4️⃣ Verificando configuración..."
if [ -f "config/config.php" ]; then
echo -e " ${GREEN}${NC} config/config.php existe"
else
echo -e " ${RED}${NC} config/config.php NO existe"
fi
echo ""
# Test 5: Intentar acceder a los endpoints (sin autenticación)
echo "5️⃣ Probando acceso a endpoints..."
RESP=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/get_conversations.php")
if [ "$RESP" = "200" ]; then
echo -e " ${GREEN}${NC} get_conversations.php responde 200"
elif [ "$RESP" = "401" ]; then
echo -e " ${YELLOW}${NC} get_conversations.php requiere autenticación (401)"
else
echo -e " ${RED}${NC} get_conversations.php responde $RESP"
fi
RESP=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/get_notifications.php")
if [ "$RESP" = "200" ]; then
echo -e " ${GREEN}${NC} get_notifications.php responde 200"
elif [ "$RESP" = "401" ]; then
echo -e " ${YELLOW}${NC} get_notifications.php requiere autenticación (401)"
else
echo -e " ${RED}${NC} get_notifications.php responde $RESP"
fi
echo ""
# Test 6: Verificar logs de error
echo "6️⃣ Revisando logs de error recientes..."
if [ -f "/tmp/php_errors.log" ]; then
echo " Últimas 5 líneas del log:"
tail -5 /tmp/php_errors.log 2>/dev/null || echo " No se pudieron leer los logs"
else
echo " No se encontró archivo de log en /tmp/php_errors.log"
fi
echo ""
echo "============================================"
echo "✅ Diagnóstico completo"
echo ""
echo "📌 Soluciones comunes:"
echo " 1. Si los endpoints requieren autenticación (401):"
echo " - Asegúrate de estar logueado en conversations.php"
echo " - Verifica que las cookies de sesión se envíen"
echo ""
echo " 2. Si el servidor no responde:"
echo " - Inicia: cd /Users/lizandro/Documents/GitHub/whatsapp && php -S localhost:8000"
echo ""
echo " 3. Si hay errores de sintaxis:"
echo " - Revisa el archivo indicado y corrige los errores"
echo ""
echo " 4. Para ver errores en tiempo real:"
echo " - tail -f /tmp/php_errors.log"
echo ""
+38
View File
@@ -58,6 +58,9 @@ services:
- APP_ENV=development
- APP_DEBUG=true
# URL pública de la aplicación (para generar URLs absolutas en archivos multimedia)
- APP_URL=${APP_URL:-https://bot.u-s.app/}
# Base de datos - apuntar a MySQL local
- DB_HOST=mysql
- DB_PORT=${DB_PORT:-3306}
@@ -224,6 +227,39 @@ services:
networks:
- whatsapp-dev-network
# Contenedor de mantenimiento automatizado
maintenance:
build:
context: .
dockerfile: Dockerfile.maintenance
container_name: whatsapp-dev-maintenance
restart: unless-stopped
cap_add:
- SYS_NICE
- SETGID
- SETUID
volumes:
- .:/var/www/html
- ./logs:/var/www/html/logs
- ./backups:/backups
- maintenance-cron-logs:/var/log/cron
environment:
- TZ=America/Bogota
- DB_HOST=mysql
- DB_NAME=${DB_NAME:-usite_whatsapp_bot}
- DB_USER=${DB_USER:-usite_whatsapp_user}
- DB_PASS=${DB_PASS:-6q!Vio0fn@PFmsLyHjH5}
- REDIS_HOST=redis
- REDIS_PORT=6379
- APP_URL=http://app/health.php
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- whatsapp-dev-network
volumes:
mysql-dev-data:
driver: local
@@ -233,6 +269,8 @@ volumes:
driver: local
php-socket:
driver: local
maintenance-cron-logs:
driver: local
networks:
whatsapp-dev-network:
+37
View File
@@ -145,6 +145,41 @@ services:
- whatsapp-network
command: php worker.php --daemon
# Mantenimiento automatizado - PRODUCCIÓN
maintenance:
build:
context: .
dockerfile: Dockerfile.maintenance
container_name: whatsapp-maintenance-prod
restart: always
cap_add:
- SYS_NICE
- SETGID
- SETUID
volumes:
- .:/var/www/html
- ./logs:/var/www/html/logs
- ./backups:/backups
- maintenance-cron-logs:/var/log/cron
environment:
- TZ=America/Bogota
- APP_ENV=production
- DB_HOST=mysql
- DB_PORT=3306
- DB_NAME=${DB_NAME:-usite_whatsapp_bot}
- DB_USER=${DB_USER:-usite_whatsapp_user}
- DB_PASS=${DB_PASS:-6q!Vio0fn@PFmsLyHjH5}
- REDIS_HOST=redis
- REDIS_PORT=6379
- APP_URL=http://app
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- whatsapp-network
volumes:
mysql-data:
driver: local
@@ -152,6 +187,8 @@ volumes:
driver: local
ssl-certs:
driver: local
maintenance-cron-logs:
driver: local
networks:
whatsapp-network:
+20
View File
@@ -0,0 +1,20 @@
# Crontab para tareas de mantenimiento
# Formato: minute hour day month weekday command
# Backup diario de base de datos a las 2:00 AM
0 2 * * * /scripts/backup-database.sh >> /var/log/cron/backup.log 2>&1
# Limpieza de logs a las 3:00 AM
0 3 * * * /scripts/clean-logs.sh >> /var/log/cron/clean.log 2>&1
# Verificación de salud cada hora
0 * * * * /scripts/health-check.sh >> /var/log/cron/health.log 2>&1
# Envío de mensajes programados cada minuto
* * * * * cd /var/www/html && php send_scheduled_messages.php >> /var/log/cron/scheduled.log 2>&1
# Limpiar logs de cron cada semana (domingo a las 4:00 AM)
0 4 * * 0 find /var/log/cron -name "*.log" -mtime +7 -delete
# Log de inicio de cron
@reboot echo "[$(date)] Cron iniciado" >> /var/log/cron/system.log
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Entrypoint para contenedor de mantenimiento
set -e
echo "=========================================="
echo "WhatsApp Bot - Contenedor de Mantenimiento"
echo "=========================================="
echo "Fecha: $(date)"
echo "Zona Horaria: $TZ"
echo ""
# Verificar conexión a base de datos
echo "Verificando conexión a base de datos..."
if mariadb-admin ping -h "${DB_HOST}" -u "${DB_USER}" --password="${DB_PASS}" --silent; then
echo "✅ Conexión a base de datos OK"
else
echo "❌ No se pudo conectar a la base de datos"
exit 1
fi
# Verificar Redis
echo "Verificando conexión a Redis..."
if echo "PING" | nc -w 2 "${REDIS_HOST}" 6379 | grep -q PONG; then
echo "✅ Conexión a Redis OK"
else
echo "⚠️ Redis no disponible (no crítico)"
fi
# Crear directorios necesarios
mkdir -p /backups/database
mkdir -p /var/log/cron
# Ejecutar migraciones de base de datos
echo ""
echo "Ejecutando migraciones de base de datos..."
/scripts/run-migrations.sh
echo ""
echo "Tareas programadas:"
echo "- 02:00 AM: Backup de base de datos"
echo "- 03:00 AM: Limpieza de logs"
echo "- Cada hora: Verificación de salud"
echo "- Cada minuto: Envío de mensajes programados"
echo ""
echo "Logs disponibles en /var/log/cron/"
echo "=========================================="
echo ""
# Iniciar cron en segundo plano y seguir logs
crond -b -d 8
touch /var/log/cron/scheduled.log
tail -f /var/log/cron/*.log
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
# Script de backup diario de base de datos
# Ejecuta a las 2:00 AM
set -e
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Iniciando backup de base de datos..."
# Variables de entorno
DB_HOST=${DB_HOST:-mysql}
DB_NAME=${DB_NAME:-usite_whatsapp_bot}
DB_USER=${DB_USER:-usite_whatsapp_user}
DB_PASS=${DB_PASS}
# Directorio de backups
BACKUP_DIR="/backups/database"
mkdir -p "$BACKUP_DIR"
# Nombre del archivo con timestamp
BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql.gz"
# Ejecutar backup comprimido
if mariadb-dump \
--host="$DB_HOST" \
--user="$DB_USER" \
--password="$DB_PASS" \
--single-transaction \
--routines \
--triggers \
--events \
"$DB_NAME" | gzip > "$BACKUP_FILE"; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ Backup creado exitosamente: $BACKUP_FILE"
# Obtener tamaño del backup
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tamaño del backup: $SIZE"
# Eliminar backups antiguos (más de 7 días)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Limpiando backups antiguos..."
find "$BACKUP_DIR" -name "backup_*.sql.gz" -type f -mtime +7 -delete
# Contar backups restantes
BACKUP_COUNT=$(find "$BACKUP_DIR" -name "backup_*.sql.gz" -type f | wc -l)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backups disponibles: $BACKUP_COUNT"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ❌ Error al crear backup"
exit 1
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup completado"
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
# Script de limpieza de logs
# Ejecuta a las 3:00 AM
set -e
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Iniciando limpieza de logs..."
# Directorio de logs
LOGS_DIR="/var/www/html/logs"
if [ ! -d "$LOGS_DIR" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ Directorio de logs no encontrado: $LOGS_DIR"
exit 0
fi
# Contar archivos antes de limpiar
BEFORE_COUNT=$(find "$LOGS_DIR" -name "*.log" -type f | wc -l)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Archivos de log encontrados: $BEFORE_COUNT"
# Eliminar logs más antiguos de 30 días
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Eliminando logs antiguos (>30 días)..."
find "$LOGS_DIR" -name "*.log" -type f -mtime +30 -delete
# Comprimir logs entre 7-30 días
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Comprimiendo logs antiguos (7-30 días)..."
find "$LOGS_DIR" -name "*.log" -type f -mtime +7 -mtime -30 -exec gzip {} \; 2>/dev/null || true
# Limpiar logs .gz más antiguos de 30 días
find "$LOGS_DIR" -name "*.log.gz" -type f -mtime +30 -delete
# Contar archivos después de limpiar
AFTER_COUNT=$(find "$LOGS_DIR" -name "*.log" -o -name "*.log.gz" -type f | wc -l)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Archivos de log restantes: $AFTER_COUNT"
# Calcular espacio liberado
SPACE_USED=$(du -sh "$LOGS_DIR" | cut -f1)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Espacio usado por logs: $SPACE_USED"
# Truncar logs muy grandes (>100MB)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Truncando logs muy grandes..."
find "$LOGS_DIR" -name "*.log" -type f -size +100M -exec sh -c 'echo "Log truncado el $(date)" > "$1"' _ {} \;
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ Limpieza de logs completada"
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# Script de verificación de salud del sistema
# Ejecuta cada hora
set -e
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando salud del sistema..."
# URLs a verificar
APP_URL=${APP_URL:-http://app/health.php}
REDIS_HOST=${REDIS_HOST:-redis}
# 1. Verificar aplicación web
echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando aplicación web... "
if curl -s -f "$APP_URL" > /dev/null 2>&1; then
echo "✅ OK"
else
echo "❌ FALLO"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ La aplicación web no responde"
fi
# 2. Verificar base de datos
echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando base de datos... "
if mariadb-admin ping -h "${DB_HOST:-mysql}" -u "${DB_USER}" --password="${DB_PASS}" --silent > /dev/null 2>&1; then
echo "✅ OK"
else
echo "❌ FALLO"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ La base de datos no responde"
fi
# 3. Verificar Redis
echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando Redis... "
if echo "PING" | nc -w 2 "$REDIS_HOST" 6379 | grep -q PONG 2>/dev/null; then
echo "✅ OK"
else
echo "❌ FALLO"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ Redis no responde"
fi
# 4. Verificar espacio en disco
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando espacio en disco..."
df -h / | tail -1 | awk '{print "[" strftime("%Y-%m-%d %H:%M:%S") "] Espacio usado: " $5 " (" $3 " de " $2 ")"}'
# Advertencia si el espacio usado es mayor al 85%
DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 85 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ ADVERTENCIA: Espacio en disco mayor al 85%"
fi
# 5. Verificar memoria
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Uso de memoria:"
free -h | grep -E "Mem|Swap" || echo "No disponible en este contenedor"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verificación completada"
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# Script para ejecutar migraciones de base de datos
# Se ejecuta automáticamente al iniciar el contenedor de mantenimiento
LOG_FILE="/var/log/cron/migrations.log"
MIGRATIONS_DIR="/var/www/html/database"
MIGRATIONS_APPLIED="/var/www/html/logs/.migrations_applied"
echo "========================================" | tee -a "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Verificando migraciones..." | tee -a "$LOG_FILE"
echo "========================================" | tee -a "$LOG_FILE"
# Crear archivo de control si no existe
touch "$MIGRATIONS_APPLIED"
# Verificar conexión a BD
if ! mariadb-admin ping -h "${DB_HOST}" -u "${DB_USER}" --password="${DB_PASS}" --silent 2>/dev/null; then
echo "[ERROR] No se pudo conectar a la base de datos" | tee -a "$LOG_FILE"
exit 1
fi
# Ejecutar cada archivo .sql en orden alfabético
for migration_file in $(ls -1 "$MIGRATIONS_DIR"/*.sql 2>/dev/null | sort); do
filename=$(basename "$migration_file")
# Verificar si ya fue aplicada
if grep -q "^${filename}$" "$MIGRATIONS_APPLIED" 2>/dev/null; then
echo "[SKIP] $filename - Ya aplicada" | tee -a "$LOG_FILE"
continue
fi
echo "[RUN] Ejecutando migración: $filename" | tee -a "$LOG_FILE"
# Ejecutar la migración
if mysql -h "${DB_HOST}" -u "${DB_USER}" --password="${DB_PASS}" "${DB_NAME}" < "$migration_file" 2>&1 | tee -a "$LOG_FILE"; then
echo "[OK] $filename - Aplicada correctamente" | tee -a "$LOG_FILE"
# Marcar como aplicada
echo "$filename" >> "$MIGRATIONS_APPLIED"
else
echo "[ERROR] $filename - Falló la migración" | tee -a "$LOG_FILE"
exit 1
fi
done
echo "========================================" | tee -a "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Migraciones completadas" | tee -a "$LOG_FILE"
echo "========================================" | tee -a "$LOG_FILE"
-70
View File
@@ -1,70 +0,0 @@
# Descripción del Uso del Permiso whatsapp_business_management
## WhatsApp Bot Manager by U-Site.app
---
**Descripción detallada del uso del permiso:**
Nuestra aplicación "WhatsApp Bot Manager" utiliza el permiso whatsapp_business_management para proporcionar una plataforma completa de administración y gestión de activos comerciales de WhatsApp Business. La aplicación permite a las empresas configurar, administrar y optimizar todos sus recursos de WhatsApp Business desde un panel centralizado, facilitando la gestión operativa y el análisis de rendimiento de sus comunicaciones empresariales.
**Uso específico del permiso:**
1. **Administración de Cuentas WhatsApp Business:** La app gestiona la configuración y administración de múltiples cuentas de WhatsApp Business, permitiendo a empresas con varios puntos de contacto centralizar su gestión desde una sola plataforma.
2. **Gestión de Números de Teléfono:** Administra la configuración, verificación y asignación de números de teléfono comerciales, incluyendo la configuración de perfiles comerciales, horarios de atención y información de contacto empresarial.
3. **Administración de Plantillas de Mensajes:** Crea, edita, organiza y gestiona plantillas de mensajes pre-aprobadas por WhatsApp, permitiendo a las empresas mantener comunicaciones consistentes y cumplir con las políticas de mensajería masiva.
4. **Configuración de Webhooks:** Gestiona las suscripciones a webhooks para recibir notificaciones en tiempo real de mensajes entrantes, cambios de estado de mensajes y eventos importantes, asegurando comunicación bidireccional efectiva.
5. **Gestión de Códigos QR Comerciales:** Genera, personaliza y administra códigos QR para que los clientes puedan iniciar conversaciones comerciales fácilmente, facilitando la captación de nuevos contactos y leads.
6. **Panel de Análisis y Estadísticas:** Recopila y presenta métricas agregadas y anónimas sobre el rendimiento de las comunicaciones empresariales, incluyendo tasas de respuesta, horarios de mayor actividad y efectividad de plantillas.
**Valor para los usuarios finales:**
- **Gestión Centralizada:** Las empresas pueden administrar todos sus activos de WhatsApp Business desde un solo panel, eliminando la necesidad de gestionar múltiples plataformas por separado.
- **Configuración Simplificada:** Interface intuitiva para configurar números comerciales, plantillas y webhooks sin conocimientos técnicos avanzados.
- **Optimización Basada en Datos:** Acceso a análisis detallados que ayudan a las empresas a optimizar sus estrategias de comunicación y mejorar la satisfacción del cliente.
- **Escalabilidad Empresarial:** Capacidad de gestionar múltiples números y cuentas comerciales desde una sola plataforma, ideal para empresas en crecimiento.
- **Cumplimiento Automatizado:** Gestión automática de plantillas y configuraciones que aseguran cumplimiento con las políticas de WhatsApp Business.
**Necesidad para el funcionamiento de la app:**
Este permiso es fundamental para el funcionamiento de nuestra aplicación porque:
1. **Configuración Inicial:** Sin acceso a la gestión de activos comerciales, las empresas no pueden configurar adecuadamente sus cuentas de WhatsApp Business para usar la plataforma de automatización.
2. **Administración de Plantillas:** La gestión de plantillas es esencial para enviar mensajes estructurados y cumplir con las regulaciones de WhatsApp sobre mensajes comerciales.
3. **Gestión de Webhooks:** El acceso a la configuración de webhooks es crítico para recibir mensajes entrantes y mantener conversaciones bidireccionales en tiempo real.
4. **Análisis de Rendimiento:** Los datos de análisis son necesarios para proporcionar insights valiosos a las empresas sobre la efectividad de sus comunicaciones y identificar áreas de mejora.
5. **Escalabilidad:** La capacidad de gestionar múltiples números y cuentas es esencial para empresas que operan en diferentes mercados o tienen múltiples líneas de negocio.
**Casos de uso específicos:**
- **Cadena de Restaurantes:** Gestiona números de WhatsApp para cada sucursal, plantillas de menús y promociones, y analiza el rendimiento de cada ubicación.
- **Empresa de E-commerce:** Administra múltiples números para diferentes categorías de productos, plantillas de confirmación de pedidos y seguimiento de entregas.
- **Centro Médico:** Gestiona números especializados por departamento, plantillas para recordatorios de citas y análisis de comunicación por especialidad.
- **Agencia de Marketing:** Administra cuentas de WhatsApp Business de múltiples clientes desde una plataforma centralizada.
**Beneficios de análisis agregado:**
La aplicación utiliza datos agregados y anónimos para:
- Identificar patrones de comunicación más efectivos
- Optimizar horarios de envío de mensajes
- Mejorar tasas de respuesta mediante análisis de plantillas
- Proporcionar benchmarks de industria para mejorar estrategias de comunicación
La aplicación está diseñada para ser una solución integral de gestión de WhatsApp Business, proporcionando a las empresas las herramientas necesarias para administrar eficientemente sus activos comerciales y optimizar sus comunicaciones con clientes, cumpliendo siempre con las políticas y regulaciones de WhatsApp Business.
---
**Información de contacto:**
- Desarrollador: U-Site.app
- Email: support@u-site.app
- Sitio web: https://u-site.app
- Política de privacidad: https://tudominio.com/privacy-policy.html
-57
View File
@@ -1,57 +0,0 @@
# Descripción del Uso del Permiso whatsapp_business_messaging
## WhatsApp Bot Manager by U-Site.app
---
**Descripción detallada del uso del permiso:**
Nuestra aplicación "WhatsApp Bot Manager" utiliza el permiso whatsapp_business_messaging para proporcionar una plataforma integral de automatización y gestión de comunicaciones empresariales a través de WhatsApp Business API. La aplicación funciona como un sistema de gestión de conversaciones que permite a las empresas automatizar respuestas, gestionar múltiples conversaciones de clientes y mejorar su atención al cliente.
**Uso específico del permiso:**
1. **Envío de Mensajes Automatizados:** La app envía mensajes de texto automáticos a clientes basados en triggers predefinidos, respuestas a preguntas frecuentes, notificaciones de servicios, confirmaciones de citas y seguimiento post-venta.
2. **Gestión de Conversaciones:** Administra y almacena historial completo de conversaciones entre empresas y clientes para proporcionar contexto continuo y seguimiento personalizado de cada interacción.
3. **Respuestas Inteligentes:** Utiliza un sistema de bot automatizado que responde instantáneamente a consultas comunes de clientes, reduciendo tiempos de respuesta y mejorando la experiencia del usuario.
4. **Notificaciones Empresariales:** Envía alertas importantes como confirmaciones de pedidos, recordatorios de citas, actualizaciones de estado de servicios y comunicaciones transaccionales.
5. **Gestión de Multimedia:** Procesa, almacena y responde con contenido multimedia (imágenes, documentos, audios) enviado por clientes para brindar soporte técnico y atención personalizada.
**Valor para los usuarios finales:**
- **Respuestas Inmediatas:** Los clientes reciben respuestas instantáneas las 24/7, mejorando significativamente la experiencia de atención al cliente.
- **Comunicación Personalizada:** Cada conversación mantiene contexto histórico, permitiendo interacciones más relevantes y personalizadas.
- **Eficiencia Operativa:** Las empresas pueden manejar múltiples conversaciones simultáneamente sin pérdida de calidad en el servicio.
- **Disponibilidad Continua:** El sistema proporciona atención automatizada fuera del horario comercial, capturando leads y resolviendo consultas básicas.
**Necesidad para el funcionamiento de la app:**
Este permiso es absolutamente esencial para el funcionamiento de nuestra aplicación porque:
1. **Funcionalidad Principal:** Sin la capacidad de enviar mensajes, la app no puede cumplir su propósito fundamental de automatización de comunicaciones empresariales.
2. **Gestión de Conversaciones:** Necesitamos acceder al historial y metadata de mensajes para mantener contexto conversacional y proporcionar respuestas relevantes.
3. **Experiencia de Usuario Continua:** La gestión de multimedia es crucial para resolver consultas que requieren intercambio de imágenes, documentos o archivos de audio.
4. **Análisis y Mejora:** Los datos agregados y anónimos nos permiten optimizar las respuestas automáticas y mejorar la precisión del bot, beneficiando directamente a todos los usuarios.
**Casos de uso específicos:**
- Empresa de delivery que envía confirmaciones automáticas de pedidos y tracking de entregas
- Clínica médica que envía recordatorios de citas y confirmaciones de disponibilidad
- Tienda online que responde consultas sobre productos y procesa órdenes de compra
- Empresa de servicios que proporciona soporte técnico 24/7 con respuestas automatizadas
La aplicación está diseñada exclusivamente para mejorar la comunicación empresa-cliente, cumpliendo con todas las políticas de WhatsApp Business y proporcionando valor real tanto a empresas como a sus clientes finales a través de comunicaciones más eficientes y efectivas.
---
**Información de contacto:**
- Desarrollador: U-Site.app
- Email: support@u-site.app
- Sitio web: https://u-site.app
- Política de privacidad: https://tudominio.com/privacy-policy.html
+191
View File
@@ -0,0 +1,191 @@
#!/bin/bash
# Script de mantenimiento para HOST (fuera de Docker)
# Instalación: sudo cp host-maintenance-cron.sh /usr/local/bin/
# Crontab: sudo crontab -e
# 0 2 * * * /usr/local/bin/host-maintenance-cron.sh backup
# 0 3 * * * /usr/local/bin/host-maintenance-cron.sh clean-logs
# 0 * * * * /usr/local/bin/host-maintenance-cron.sh health-check
set -e
# Configuración
DOCKER_COMPOSE_FILE="/ruta/a/tu/proyecto/docker-compose.dev.yml"
PROJECT_DIR="/ruta/a/tu/proyecto"
BACKUP_DIR="$PROJECT_DIR/backups"
LOGS_DIR="$PROJECT_DIR/logs"
# Colores para logs
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() {
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}
error() {
echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ERROR:${NC} $1"
}
warning() {
echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] WARNING:${NC} $1"
}
# Función: Backup de base de datos
backup_database() {
log "Iniciando backup de base de datos..."
mkdir -p "$BACKUP_DIR/database"
BACKUP_FILE="$BACKUP_DIR/database/backup_$(date +%Y%m%d_%H%M%S).sql.gz"
# Ejecutar backup desde el contenedor MySQL
if docker compose -f "$DOCKER_COMPOSE_FILE" exec -T mysql \
mariadb-dump \
--single-transaction \
--routines \
--triggers \
--events \
usite_whatsapp_bot | gzip > "$BACKUP_FILE"; then
log "✅ Backup creado: $BACKUP_FILE"
# Mostrar tamaño
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log "Tamaño: $SIZE"
# Eliminar backups antiguos (>7 días)
find "$BACKUP_DIR/database" -name "backup_*.sql.gz" -mtime +7 -delete
log "Backups antiguos eliminados"
# Contar backups
COUNT=$(find "$BACKUP_DIR/database" -name "backup_*.sql.gz" | wc -l)
log "Backups disponibles: $COUNT"
else
error "Fallo al crear backup"
return 1
fi
}
# Función: Limpieza de logs
clean_logs() {
log "Iniciando limpieza de logs..."
if [ ! -d "$LOGS_DIR" ]; then
warning "Directorio de logs no encontrado: $LOGS_DIR"
return 0
fi
# Contar archivos antes
BEFORE=$(find "$LOGS_DIR" -name "*.log" | wc -l)
log "Archivos de log: $BEFORE"
# Eliminar logs >30 días
find "$LOGS_DIR" -name "*.log" -mtime +30 -delete
# Comprimir logs 7-30 días
find "$LOGS_DIR" -name "*.log" -mtime +7 -mtime -30 -exec gzip {} \; 2>/dev/null || true
# Eliminar .gz >30 días
find "$LOGS_DIR" -name "*.log.gz" -mtime +30 -delete
# Truncar logs muy grandes
find "$LOGS_DIR" -name "*.log" -size +100M -exec sh -c 'echo "Log truncado el $(date)" > "$1"' _ {} \;
AFTER=$(find "$LOGS_DIR" -name "*.log" -o -name "*.log.gz" | wc -l)
SPACE=$(du -sh "$LOGS_DIR" | cut -f1)
log "Archivos restantes: $AFTER"
log "Espacio usado: $SPACE"
log "✅ Limpieza completada"
}
# Función: Health Check
health_check() {
log "Verificando salud del sistema..."
# Verificar contenedores
CONTAINERS=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps --services --filter "status=running" | wc -l)
EXPECTED=7 # Ajustar según tus servicios
if [ "$CONTAINERS" -ge "$EXPECTED" ]; then
log "✅ Contenedores: $CONTAINERS activos"
else
warning "Solo $CONTAINERS/$EXPECTED contenedores activos"
fi
# Verificar espacio en disco
DISK_USAGE=$(df -h "$PROJECT_DIR" | tail -1 | awk '{print $5}' | sed 's/%//')
DISK_SPACE=$(df -h "$PROJECT_DIR" | tail -1 | awk '{print $4}')
log "Espacio en disco: $DISK_USAGE% usado, $DISK_SPACE disponible"
if [ "$DISK_USAGE" -gt 85 ]; then
warning "⚠️ Espacio en disco mayor al 85%"
fi
# Verificar logs de Docker
ERROR_COUNT=$(docker compose -f "$DOCKER_COMPOSE_FILE" logs --tail=100 2>&1 | grep -i "error" | wc -l)
if [ "$ERROR_COUNT" -gt 0 ]; then
warning "Se encontraron $ERROR_COUNT errores en logs recientes"
else
log "✅ No hay errores recientes"
fi
log "Verificación completada"
}
# Función: Restart services si es necesario
auto_restart() {
log "Verificando servicios para auto-restart..."
# Verificar contenedores con reintentos excesivos
UNHEALTHY=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps --format json | \
jq -r 'select(.Health == "unhealthy") | .Service' 2>/dev/null || echo "")
if [ -n "$UNHEALTHY" ]; then
warning "Contenedores no saludables: $UNHEALTHY"
log "Reiniciando servicios no saludables..."
docker compose -f "$DOCKER_COMPOSE_FILE" restart $UNHEALTHY
log "✅ Servicios reiniciados"
else
log "✅ Todos los servicios saludables"
fi
}
# Menú principal
case "$1" in
backup)
backup_database
;;
clean-logs)
clean_logs
;;
health-check)
health_check
;;
auto-restart)
auto_restart
;;
all)
backup_database
clean_logs
health_check
;;
*)
echo "Uso: $0 {backup|clean-logs|health-check|auto-restart|all}"
echo ""
echo "Comandos:"
echo " backup - Backup de base de datos"
echo " clean-logs - Limpieza de logs"
echo " health-check - Verificación de salud"
echo " auto-restart - Reiniciar servicios no saludables"
echo " all - Ejecutar todas las tareas"
exit 1
;;
esac
exit 0
+640 -26
View File
@@ -30,8 +30,10 @@ try {
<title>WhatsApp Bot Manager</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<link href="./assets/css/styles.css?v=11" rel="stylesheet">
<link href="./assets/css/diagnostic.css?v=10" rel="stylesheet">
<link href="./assets/css/diagnostic.css?v=11" rel="stylesheet">
</head>
<body>
@@ -45,7 +47,10 @@ try {
<li><a href="#conversations" class="nav-link" data-tab="conversations"><i class="fas fa-comments"></i> Conversaciones <span id="conversations-count" class="sidebar-badge badge bg-danger" style="display:none; font-size:0.72rem; padding:0.15rem 0.4rem;">0</span></a></li>
<li><a href="#users" class="nav-link" data-tab="users"><i class="fas fa-users"></i> Usuarios</a></li>
<li><a href="#menus" class="nav-link" data-tab="menus"><i class="fas fa-list"></i> Menús</a></li>
<li><a href="#admin_users" class="nav-link" data-tab="admin_users"><i class="fas fa-user-shield"></i> Usuarios del Sistema</a></li>
<li><a href="#send_message" class="nav-link" data-tab="send_message"><i class="fas fa-paper-plane"></i> Enviar Mensaje</a></li>
<li><a href="#broadcast" class="nav-link" data-tab="broadcast"><i class="fas fa-broadcast-tower"></i> Envío Masivo <span class="badge bg-success ms-1">NUEVO</span></a></li>
<li><a href="scheduled_messages.php" class="nav-link"><i class="fas fa-clock"></i> Mensajes Programados</a></li>
<li><a href="#templates" class="nav-link" data-tab="templates"><i class="fas fa-file-text"></i> Plantillas</a></li>
<li><a href="#autoresponses" class="nav-link" data-tab="autoresponses"><i class="fas fa-robot"></i> Respuestas Auto</a></li>
<li><a href="#system_config" class="nav-link" data-tab="system_config"><i class="fas fa-cog"></i> Configuración</a></li>
@@ -236,6 +241,22 @@ try {
</button>
</div>
<div class="card-body">
<!-- Búsqueda de usuarios -->
<div class="mb-3">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input
type="text"
class="form-control"
id="users-search"
placeholder="Buscar por teléfono, nombre o email..."
onkeyup="searchUsers()"
>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
@@ -254,6 +275,9 @@ try {
</tbody>
</table>
</div>
<!-- Paginación -->
<div id="users-pagination"></div>
</div>
</div>
</div>
@@ -290,6 +314,46 @@ try {
</div>
</div>
<!-- Admin Users Tab -->
<div id="admin_users" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-user-shield"></i> Usuarios del Sistema</h5>
<button class="btn btn-primary" onclick="showCreateAdminUserModal()">
<i class="fas fa-plus"></i> Nuevo Usuario
</button>
</div>
<div class="card-body">
<div class="alert alert-info mb-3">
<i class="fas fa-info-circle"></i>
Gestiona los usuarios administradores que tienen acceso al sistema.
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Usuario</th>
<th>Nombre Completo</th>
<th>Email</th>
<th>Estado</th>
<th>Último Acceso</th>
<th>Fecha Creación</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="admin-users-table">
<tr>
<td colspan="7" class="text-center">
<i class="fas fa-spinner fa-spin"></i> Cargando usuarios...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- conversations Tab -->
<div id="send_message" class="tab-content">
<div class="row">
@@ -336,10 +400,26 @@ try {
</div>
<div class="mb-3" id="template-group" style="display: none;">
<label class="form-label">Plantilla</label>
<select class="form-control" id="message-template">
<select class="form-control" id="message-template" onchange="loadMessageTemplateDetails()">
<option value="">Seleccionar plantilla...</option>
</select>
</div>
<!-- Variables de la plantilla -->
<div id="message-template-variables" class="mb-3" style="display: none;">
<!-- Se llenarán dinámicamente -->
</div>
<!-- Preview de la plantilla -->
<div id="message-template-preview" class="mb-3" style="display: none;">
<label class="form-label"><i class="fas fa-eye"></i> Vista Previa</label>
<div class="card bg-light">
<div class="card-body">
<div id="message-preview-content"></div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> Enviar Mensaje
</button>
@@ -348,33 +428,154 @@ try {
</div>
</div>
</div>
<div class="col-lg-6" id="broadcast-section" style="display:none;">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-broadcast-tower"></i> Mensaje Masivo (oculto)</h5>
</div>
</div>
<!-- Broadcast Tab -->
<div id="broadcast" class="tab-content">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow-lg">
<div class="card-header bg-warning text-white">
<h4 class="mb-0"><i class="fas fa-broadcast-tower"></i> Envío Masivo de Mensajes</h4>
</div>
<div class="card-body">
<form id="broadcast-form">
<div class="mb-3">
<label class="form-label">Filtro de Usuarios</label>
<select class="form-control" id="broadcast-filter">
<option value="all">Todos los usuarios</option>
<option value="active">Solo usuarios activos</option>
<option value="recent">Usuarios recientes (30 días)</option>
<div class="alert alert-success mb-4">
<i class="fas fa-check-circle"></i>
<strong>¡Envío Masivo Activo!</strong> Ahora puedes enviar mensajes a múltiples usuarios simultáneamente.
</div>
<form id="broadcast-form" onsubmit="return handleBroadcastSubmit(event)">
<div class="mb-4">
<label class="form-label fw-bold">
<i class="fas fa-filter"></i> Tipo de Selección
</label>
<select class="form-control form-control-lg" id="broadcast-selection-type" onchange="toggleBroadcastSelectionType()">
<option value="filter">📋 Por filtro de usuarios</option>
<option value="specific">👥 Usuarios específicos</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Mensaje</label>
<textarea class="form-control" id="broadcast-message" rows="4" placeholder="Mensaje para envío masivo..."></textarea>
<!-- Filtro de usuarios -->
<div class="mb-4" id="broadcast-filter-container">
<label class="form-label fw-bold">
<i class="fas fa-filter"></i> Filtro de Usuarios
</label>
<select class="form-control form-control-lg" id="broadcast-filter">
<option value="all">📋 Todos los usuarios activos</option>
<option value="active">🔥 Solo usuarios con conversaciones recientes (7 días)</option>
<option value="recent">✨ Usuarios recientes (30 días)</option>
</select>
<small class="form-text text-muted">
<i class="fas fa-info-circle"></i> Selecciona qué usuarios recibirán el mensaje
</small>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>Cuidado:</strong> Este mensaje se enviará a múltiples usuarios.
<!-- Selección específica de usuarios -->
<div class="mb-4" id="broadcast-users-container" style="display: none;">
<label class="form-label fw-bold">
<i class="fas fa-users"></i> Seleccionar Usuarios
</label>
<select class="form-control" id="broadcast-users" multiple="multiple">
<!-- Opciones cargadas dinámicamente -->
</select>
<small class="form-text text-muted">
<i class="fas fa-info-circle"></i> Selecciona uno o más usuarios para enviar el mensaje
</small>
</div>
<div class="mb-4">
<label class="form-label fw-bold">
<i class="fas fa-envelope"></i> Tipo de Mensaje
</label>
<select class="form-control form-control-lg" id="broadcast-message-type" onchange="toggleBroadcastMessageType()">
<option value="text">📝 Mensaje de texto</option>
<option value="template">📋 Plantilla de WhatsApp</option>
</select>
</div>
<!-- Mensaje de texto -->
<div class="mb-4" id="broadcast-text-container">
<label class="form-label fw-bold">
<i class="fas fa-comment-dots"></i> Mensaje
</label>
<textarea
class="form-control"
id="broadcast-message"
rows="6"
placeholder="Escribe el mensaje que deseas enviar a todos los usuarios...
Ejemplo:
¡Hola! 👋 Tenemos una promoción especial para ti..."
maxlength="4096"></textarea>
<div class="d-flex justify-content-between mt-2">
<small class="form-text text-muted">
<i class="fas fa-keyboard"></i> Máximo 4096 caracteres
</small>
<small class="form-text text-muted">
<span id="char-count">0</span> / 4096
</small>
</div>
</div>
<!-- Selección de plantilla -->
<div class="mb-4" id="broadcast-template-container" style="display: none;">
<label class="form-label fw-bold">
<i class="fas fa-file-text"></i> Seleccionar Plantilla
</label>
<select class="form-control form-control-lg" id="broadcast-template" onchange="loadBroadcastTemplateDetails()">
<option value="">-- Selecciona una plantilla --</option>
</select>
<small class="form-text text-muted">
<i class="fas fa-info-circle"></i> Solo plantillas aprobadas
</small>
</div>
<!-- Variables de plantilla -->
<div id="broadcast-template-variables" class="mb-4" style="display: none;">
<!-- Variables dinámicas -->
</div>
<!-- Vista previa de plantilla -->
<div id="broadcast-template-preview" class="mb-4" style="display: none;">
<label class="form-label fw-bold">
<i class="fas fa-eye"></i> Vista Previa
</label>
<div class="alert alert-info" id="broadcast-preview-content">
<!-- Contenido de vista previa -->
</div>
</div>
<div class="alert alert-warning d-flex align-items-center">
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
<div>
<strong>Importante:</strong> Este mensaje se enviará a múltiples usuarios.
<br>Verifica cuidadosamente el contenido antes de enviar.
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-warning btn-lg">
<i class="fas fa-broadcast-tower"></i> Enviar Mensaje Masivo
</button>
</div>
<button type="submit" class="btn btn-warning">
<i class="fas fa-broadcast-tower"></i> Envío Masivo
</button>
</form>
<div class="mt-4">
<div class="card bg-light">
<div class="card-body">
<h6 class="fw-bold"><i class="fas fa-lightbulb text-warning"></i> Consejos para envío masivo:</h6>
<ul class="mb-0 small">
<li>Personaliza tu mensaje para que sea relevante</li>
<li>Evita enviar mensajes muy frecuentes</li>
<li>Respeta la ventana de 24 horas de WhatsApp</li>
<li>Usa plantillas aprobadas para nuevos contactos</li>
<li>Prueba con un grupo pequeño primero</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -386,11 +587,24 @@ try {
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-file-text"></i> Plantillas de Mensaje</h5>
<button class="btn btn-primary" id="btn-new-template" onclick="showCreateTemplateModal()">
<i class="fas fa-plus"></i> Nueva Plantilla
</button>
<div>
<button class="btn btn-success" id="btn-sync-templates" onclick="syncTemplatesFromFacebook()">
<i class="fas fa-sync-alt"></i> Sincronizar desde Facebook
</button>
</div>
</div>
<div class="card-body">
<!-- Mensaje informativo sobre creación de plantillas -->
<div class="alert alert-info" role="alert">
<h6 class="alert-heading"><i class="fas fa-info-circle"></i> ¿Cómo crear nuevas plantillas?</h6>
<p class="mb-2">Las plantillas de WhatsApp deben crearse directamente en el Administrador de Meta Business.</p>
<p class="mb-3">Una vez creadas y aprobadas por Meta, puedes sincronizarlas aquí usando el botón "Sincronizar desde Facebook".</p>
<a href="https://business.facebook.com/latest/whatsapp_manager/message_templates?business_id=2144688622601603"
class="btn btn-primary btn-sm" target="_blank" rel="noopener noreferrer">
<i class="fas fa-external-link-alt"></i> Ir al Administrador de Plantillas de Meta
</a>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
@@ -462,6 +676,11 @@ try {
<label class="form-label">Phone Number ID</label>
<input type="text" class="form-control" id="phone-number-id" placeholder="ID del número de teléfono">
</div>
<div class="mb-3">
<label class="form-label">Business Account ID (WABA ID)</label>
<input type="text" class="form-control" id="business-account-id" placeholder="ID de la cuenta de WhatsApp Business">
<small class="form-text text-muted">Necesario para sincronizar plantillas desde Facebook</small>
</div>
<div class="mb-3">
<label class="form-label">Webhook Verify Token</label>
<input type="text" class="form-control" id="webhook-token" placeholder="Token de verificación del webhook">
@@ -667,10 +886,104 @@ try {
</div>
</div>
<!-- Modal para crear/editar usuario administrador -->
<div class="modal fade" id="adminUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="adminUserModalLabel">
<i class="fas fa-user-shield"></i> <span id="admin-user-modal-title">Nuevo Usuario</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="admin-user-form">
<input type="hidden" id="admin-user-id">
<div class="mb-3">
<label class="form-label">Nombre de Usuario *</label>
<input type="text" class="form-control" id="admin-username" required
placeholder="usuario123">
<small class="text-muted">Usado para iniciar sesión</small>
</div>
<div class="mb-3">
<label class="form-label">Nombre Completo</label>
<input type="text" class="form-control" id="admin-fullname"
placeholder="Juan Pérez">
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="admin-email"
placeholder="usuario@ejemplo.com">
</div>
<div class="mb-3" id="password-group">
<label class="form-label">Contraseña *</label>
<input type="password" class="form-control" id="admin-password"
placeholder="Mínimo 6 caracteres">
<small class="text-muted">Mínimo 6 caracteres</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
<button type="button" class="btn btn-primary" onclick="saveAdminUser()">
<i class="fas fa-save"></i> Guardar
</button>
</div>
</div>
</div>
</div>
<!-- Modal para cambiar contraseña -->
<div class="modal fade" id="changePasswordModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-key"></i> Cambiar Contraseña
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="change-password-user-id">
<p class="mb-3">Usuario: <strong id="change-password-username"></strong></p>
<div class="mb-3">
<label class="form-label">Nueva Contraseña *</label>
<input type="password" class="form-control" id="new-password"
placeholder="Mínimo 6 caracteres" required>
<small class="text-muted">Mínimo 6 caracteres</small>
</div>
<div class="mb-3">
<label class="form-label">Confirmar Contraseña *</label>
<input type="password" class="form-control" id="confirm-password"
placeholder="Repite la contraseña" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
<button type="button" class="btn btn-primary" onclick="saveNewPassword()">
<i class="fas fa-key"></i> Cambiar Contraseña
</button>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="./assets/js/app_simple.js?v=14"></script>
<script src="./assets/js/app_simple.js?v=18"></script>
<script>
// Función de diagnóstico mejorada
function runDiagnostic() {
@@ -1192,9 +1505,310 @@ try {
}
};
// ====== Funciones para Broadcast con Plantillas y Múltiples Usuarios ======
// Manejador del formulario de broadcast
async function handleBroadcastSubmit(event) {
event.preventDefault();
event.stopPropagation();
console.log('Formulario de broadcast enviado');
// Validar que existe el manager
if (!window.whatsappManager || !window.whatsappManager.sendBroadcast) {
console.error('whatsappManager no disponible');
alert('Error: Sistema no inicializado. Recarga la página.');
return false;
}
// Validar variables de plantilla si se seleccionó plantilla
const messageType = document.getElementById('broadcast-message-type').value;
if (messageType === 'template') {
const templateId = document.getElementById('broadcast-template').value;
if (!templateId) {
alert('Por favor selecciona una plantilla');
return false;
}
// Validar que las variables requeridas estén llenas
const varInputs = document.querySelectorAll('.broadcast-template-var');
let hasEmpty = false;
varInputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('is-invalid');
hasEmpty = true;
} else {
input.classList.remove('is-invalid');
}
});
if (hasEmpty) {
alert('Por favor completa todos los campos de variables de la plantilla');
return false;
}
}
// Llamar al método del manager
await window.whatsappManager.sendBroadcast();
return false;
}
function initializeBroadcastSelect2() {
// Inicializar Select2 para usuarios con transport personalizado
$('#broadcast-users').select2({
theme: 'bootstrap-5',
placeholder: 'Buscar y seleccionar usuarios...',
allowClear: true,
ajax: {
transport: function (params, success, failure) {
const searchTerm = params.data.term || '';
const page = params.data.page || 1;
fetch(`api/get_users.php?search=${encodeURIComponent(searchTerm)}&page=${page}&limit=50`, {
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
success(data);
} else {
failure();
}
})
.catch(error => {
console.error('Error cargando usuarios:', error);
failure();
});
},
delay: 250,
processResults: function (data, params) {
params.page = params.page || 1;
if (!data || !data.data) {
return { results: [] };
}
return {
results: data.data.map(user => ({
id: user.id,
text: `${user.name || 'Sin nombre'} (${user.phone_number})`
})),
pagination: {
more: data.pagination && data.pagination.hasNext
}
};
},
cache: true
},
minimumInputLength: 0
});
// Cargar plantillas aprobadas
loadBroadcastTemplates();
}
function toggleBroadcastSelectionType() {
const selectionType = document.getElementById('broadcast-selection-type').value;
const filterContainer = document.getElementById('broadcast-filter-container');
const usersContainer = document.getElementById('broadcast-users-container');
if (selectionType === 'filter') {
filterContainer.style.display = 'block';
usersContainer.style.display = 'none';
} else {
filterContainer.style.display = 'none';
usersContainer.style.display = 'block';
}
}
function toggleBroadcastMessageType() {
const messageType = document.getElementById('broadcast-message-type').value;
const textContainer = document.getElementById('broadcast-text-container');
const templateContainer = document.getElementById('broadcast-template-container');
if (messageType === 'text') {
textContainer.style.display = 'block';
templateContainer.style.display = 'none';
document.getElementById('broadcast-template-variables').style.display = 'none';
document.getElementById('broadcast-template-preview').style.display = 'none';
} else {
textContainer.style.display = 'none';
templateContainer.style.display = 'block';
}
}
async function loadBroadcastTemplates() {
try {
console.log('🔵 Cargando plantillas para broadcast...');
if (!window.whatsappManager) {
console.error('❌ whatsappManager no está disponible');
return;
}
const response = await window.whatsappManager.apiCall('get_templates.php');
console.log('Respuesta de get_templates:', response);
if (response && response.success && response.data) {
const select = document.getElementById('broadcast-template');
if (!select) {
console.error('Elemento broadcast-template no encontrado');
return;
}
select.innerHTML = '<option value="">-- Selecciona una plantilla --</option>';
const approvedTemplates = response.data.filter(t =>
t.status && (t.status.toUpperCase() === 'APPROVED' || t.status.toLowerCase() === 'approved')
);
console.log('Plantillas aprobadas encontradas:', approvedTemplates.length);
approvedTemplates.forEach(template => {
const option = document.createElement('option');
option.value = template.id;
option.textContent = `${template.name} (${template.language_code || template.language || 'es'})`;
option.dataset.templateId = template.id;
select.appendChild(option);
});
if (approvedTemplates.length === 0) {
console.warn('No hay plantillas aprobadas disponibles');
select.innerHTML = '<option value="">No hay plantillas aprobadas</option>';
}
} else {
console.error('Respuesta inválida del API:', response);
}
} catch (error) {
console.error('Error cargando plantillas:', error);
}
}
async function loadBroadcastTemplateDetails() {
console.log('🔵 loadBroadcastTemplateDetails llamado');
const select = document.getElementById('broadcast-template');
const templateId = select.value;
console.log('Template ID seleccionado:', templateId);
if (!templateId) {
document.getElementById('broadcast-template-variables').style.display = 'none';
document.getElementById('broadcast-template-preview').style.display = 'none';
return;
}
try {
const response = await window.whatsappManager.apiCall(`get_template_details.php?id=${templateId}`);
console.log('✅ Detalles de plantilla:', response);
if (response && response.success) {
const template = response.template || response.data;
if (!template) {
console.error('❌ No se encontró template en la respuesta');
return;
}
console.log('📋 Template data:', template);
console.log('📋 Variables:', template.variables);
const variablesContainer = document.getElementById('broadcast-template-variables');
if (template.variables && template.variables.length > 0) {
console.log(`✅ Se encontraron ${template.variables.length} variables`);
let html = '<label class="form-label fw-bold"><i class="fas fa-edit"></i> Variables de la Plantilla</label>';
template.variables.forEach((variable, index) => {
console.log(`Variable ${index}:`, variable);
const placeholder = variable.example || `Valor para Variable ${variable.index}`;
const label = variable.label || `Variable ${variable.index}`;
html += `
<div class="mb-2">
<label class="form-label small">${label}</label>
<input
type="text"
class="form-control broadcast-template-var"
data-var="${variable.index}"
data-placeholder="${variable.placeholder}"
placeholder="${placeholder}"
onkeyup="updateBroadcastPreview()"
>
</div>
`;
});
console.log('📝 HTML generado para variables:', html);
variablesContainer.innerHTML = html;
variablesContainer.style.display = 'block';
console.log('✅ Variables container mostrado');
} else {
console.log('⚠️ Template sin variables');
variablesContainer.innerHTML = '<p class="text-muted small">Esta plantilla no tiene variables</p>';
variablesContainer.style.display = 'block';
}
updateBroadcastPreview();
} else {
console.error('❌ Respuesta sin success:', response);
}
} catch (error) {
console.error('❌ Error cargando detalles de plantilla:', error);
window.whatsappManager.showError('Error cargando plantilla');
}
}
async function updateBroadcastPreview() {
const templateId = document.getElementById('broadcast-template').value;
if (!templateId) return;
// Recolectar variables como array (por índice)
const variablesArray = [];
document.querySelectorAll('.broadcast-template-var').forEach(input => {
const index = parseInt(input.dataset.var) - 1; // Convertir a índice de array (0-based)
variablesArray[index] = input.value || `{{${input.dataset.var}}}`;
});
try {
const response = await window.whatsappManager.apiCall('preview_template.php', {
method: 'POST',
body: { template_id: templateId, parameters: variablesArray }
});
if (response && response.success && response.preview) {
const previewContainer = document.getElementById('broadcast-template-preview');
const previewContent = document.getElementById('broadcast-preview-content');
// Usar el HTML pre-formateado del servidor
previewContent.innerHTML = response.preview.html || response.preview.body.replace(/\n/g, '<br>');
previewContainer.style.display = 'block';
} else {
console.error('Error en preview:', response);
}
} catch (error) {
console.error('Error generando vista previa:', error);
}
}
// ====== Fin de funciones de Broadcast ======
document.addEventListener('DOMContentLoaded', () => {
try { NotificationManager.init(); } catch(e) { console.error('NotificationManager init failed', e); }
// Inicializar Select2 para broadcast cuando el manager esté listo
const waitForManager = setInterval(() => {
if (window.whatsappManager) {
clearInterval(waitForManager);
console.log('Inicializando broadcast Select2...');
try {
initializeBroadcastSelect2();
} catch(e) {
console.error('Error inicializando broadcast Select2:', e);
}
}
}, 100);
// Timeout de seguridad
setTimeout(() => clearInterval(waitForManager), 5000);
// Sidebar toggle logic for responsive/mobile
const sidebar = document.querySelector('.sidebar');
const toggleBtn = document.getElementById('sidebar-toggle');
+1 -4
View File
@@ -318,9 +318,6 @@ if ($_POST && !$loginBlocked) {
<?php endif; ?>
</script>
<!-- Footer U-Site -->
<footer style="position: fixed; bottom: 0; left: 0; right: 0; background: transparent; padding: 15px; text-align: center; font-size: 12px; color: rgba(255,255,255,0.6);">
Desarrollado con 💚 por <a href="https://u-site.app" target="_blank" style="color: #25d366; text-decoration: none; font-weight: 600;">U-Site.app</a>
</footer>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
01_backup_full.sql
02_add_template_components.sql
03_create_scheduled_messages.sql
+153
View File
@@ -78,3 +78,156 @@ Stack trace:
[2026-01-27 14:41:33] [INFO] Cleared advisor_requested for user 3250 after outgoing message by operator
[2026-01-27 23:37:33] [INFO] Cleared advisor_requested for user 3277 after outgoing message by operator
[2026-01-27 23:42:21] [INFO] Cleared advisor_requested for user 3277 after outgoing message by operator
[2026-01-29 09:02:07] [INFO] Cleared advisor_requested for user 3250 after outgoing message by operator
[2026-01-29 09:05:30] [INFO] Cleared advisor_requested for user 3250 after outgoing message by operator
[2026-02-02 21:27:38] [INFO] Configuración de WhatsApp actualizada {"configured_fields":{"token":true,"phone_id":true,"webhook_token":true},"user":"debug_user"}
[2026-02-02 21:46:53] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.112.5","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-02 21:46:53] [INFO] delete_template.php start {"raw_input":{"id":14},"query":{"debug":"true"}}
[2026-02-02 21:46:53] [INFO] delete_template.php parsed ids {"ids":[14]}
[2026-02-02 21:46:53] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-02 21:46:53] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-02 21:46:58] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.112.5","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-02 21:46:58] [INFO] delete_template.php start {"raw_input":{"id":10},"query":{"debug":"true"}}
[2026-02-02 21:46:58] [INFO] delete_template.php parsed ids {"ids":[10]}
[2026-02-02 21:46:58] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-02 21:46:58] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-02 21:47:01] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 0 actualizadas, 3 sin cambios
[2026-02-03 11:07:32] [INFO] Configuración de WhatsApp actualizada {"configured_fields":{"token":true,"phone_id":true,"webhook_token":true},"user":"debug_user"}
[2026-02-03 11:07:41] [INFO] Plantillas sincronizadas desde Facebook: 6 nuevas, 1 actualizadas, 0 sin cambios
[2026-02-03 11:21:31] [INFO] Mensaje programado eliminado: ID=1
[2026-02-03 12:42:05] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
[2026-02-03 14:22:57] [INFO] Mensaje programado creado: ID=2, Usuario=3277, Fecha=2026-02-03 14:23
[2026-02-03 14:29:08] [INFO] Mensaje programado creado: ID=3, Usuario=3277, Fecha=2026-02-03 14:30
[2026-02-03 14:30:22] [INFO] Mensaje programado eliminado: ID=3
[2026-02-03 14:35:06] [INFO] Mensaje programado creado: ID=4, Usuario=3277, Fecha=2026-02-03 14:36
[2026-02-03 14:38:15] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
[2026-02-03 14:39:05] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:39:05] [INFO] delete_template.php start {"raw_input":{"id":16},"query":{"debug":"true"}}
[2026-02-03 14:39:05] [INFO] delete_template.php parsed ids {"ids":[16]}
[2026-02-03 14:39:05] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:39:05] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:39:09] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:39:09] [INFO] delete_template.php start {"raw_input":{"id":13},"query":{"debug":"true"}}
[2026-02-03 14:39:09] [INFO] delete_template.php parsed ids {"ids":[13]}
[2026-02-03 14:39:09] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:39:09] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:39:12] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:39:12] [INFO] delete_template.php start {"raw_input":{"id":12},"query":{"debug":"true"}}
[2026-02-03 14:39:12] [INFO] delete_template.php parsed ids {"ids":[12]}
[2026-02-03 14:39:12] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:39:12] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:39:16] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:39:16] [INFO] delete_template.php start {"raw_input":{"id":17},"query":{"debug":"true"}}
[2026-02-03 14:39:16] [INFO] delete_template.php parsed ids {"ids":[17]}
[2026-02-03 14:39:16] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:39:16] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:39:18] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:39:18] [INFO] delete_template.php start {"raw_input":{"id":10},"query":{"debug":"true"}}
[2026-02-03 14:39:18] [INFO] delete_template.php parsed ids {"ids":[10]}
[2026-02-03 14:39:18] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:39:18] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:39:21] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:39:21] [INFO] delete_template.php start {"raw_input":{"id":18},"query":{"debug":"true"}}
[2026-02-03 14:39:21] [INFO] delete_template.php parsed ids {"ids":[18]}
[2026-02-03 14:39:21] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:39:21] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:39:39] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:39:39] [INFO] delete_template.php start {"raw_input":{"id":14},"query":{"debug":"true"}}
[2026-02-03 14:39:39] [INFO] delete_template.php parsed ids {"ids":[14]}
[2026-02-03 14:39:39] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:39:39] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:39:42] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:39:42] [INFO] delete_template.php start {"raw_input":{"id":15},"query":{"debug":"true"}}
[2026-02-03 14:39:42] [INFO] delete_template.php parsed ids {"ids":[15]}
[2026-02-03 14:39:42] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:39:42] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:39:43] [INFO] Plantillas sincronizadas desde Facebook: 7 nuevas, 0 actualizadas, 0 sin cambios
[2026-02-03 14:40:15] [INFO] Mensaje programado creado: ID=5, Usuario=3277, Fecha=2026-02-04 09:00
[2026-02-03 14:51:39] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
[2026-02-03 14:51:48] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:51:48] [INFO] delete_template.php start {"raw_input":{"id":22},"query":{"debug":"true"}}
[2026-02-03 14:51:48] [INFO] delete_template.php parsed ids {"ids":[22]}
[2026-02-03 14:51:48] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:51:48] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:51:56] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:51:56] [INFO] delete_template.php start {"raw_input":{"id":19},"query":{"debug":"true"}}
[2026-02-03 14:51:56] [INFO] delete_template.php parsed ids {"ids":[19]}
[2026-02-03 14:51:56] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:51:56] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:51:59] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:51:59] [INFO] delete_template.php start {"raw_input":{"id":23},"query":{"debug":"true"}}
[2026-02-03 14:51:59] [INFO] delete_template.php parsed ids {"ids":[23]}
[2026-02-03 14:51:59] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:51:59] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:52:02] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:52:02] [INFO] delete_template.php start {"raw_input":{"id":24},"query":{"debug":"true"}}
[2026-02-03 14:52:02] [INFO] delete_template.php parsed ids {"ids":[24]}
[2026-02-03 14:52:02] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:52:02] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:52:06] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:52:06] [INFO] delete_template.php start {"raw_input":{"id":25},"query":{"debug":"true"}}
[2026-02-03 14:52:06] [INFO] delete_template.php parsed ids {"ids":[25]}
[2026-02-03 14:52:06] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:52:06] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:52:09] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:52:09] [INFO] delete_template.php start {"raw_input":{"id":20},"query":{"debug":"true"}}
[2026-02-03 14:52:09] [INFO] delete_template.php parsed ids {"ids":[20]}
[2026-02-03 14:52:09] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:52:09] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:52:11] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 14:52:11] [INFO] delete_template.php start {"raw_input":{"id":21},"query":{"debug":"true"}}
[2026-02-03 14:52:11] [INFO] delete_template.php parsed ids {"ids":[21]}
[2026-02-03 14:52:11] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 14:52:11] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 14:52:15] [INFO] Plantillas sincronizadas desde Facebook: 7 nuevas, 0 actualizadas, 0 sin cambios
[2026-02-03 14:59:06] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
[2026-02-03 15:09:36] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
[2026-02-03 15:09:55] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
[2026-02-03 15:10:09] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 15:10:09] [INFO] delete_template.php start {"raw_input":{"id":29},"query":{"debug":"true"}}
[2026-02-03 15:10:09] [INFO] delete_template.php parsed ids {"ids":[29]}
[2026-02-03 15:10:09] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 15:10:09] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 15:10:12] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 15:10:12] [INFO] delete_template.php start {"raw_input":{"id":26},"query":{"debug":"true"}}
[2026-02-03 15:10:12] [INFO] delete_template.php parsed ids {"ids":[26]}
[2026-02-03 15:10:12] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 15:10:12] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 15:10:16] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 15:10:16] [INFO] delete_template.php start {"raw_input":{"id":30},"query":{"debug":"true"}}
[2026-02-03 15:10:16] [INFO] delete_template.php parsed ids {"ids":[30]}
[2026-02-03 15:10:16] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 15:10:16] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 15:10:20] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 15:10:20] [INFO] delete_template.php start {"raw_input":{"id":31},"query":{"debug":"true"}}
[2026-02-03 15:10:20] [INFO] delete_template.php parsed ids {"ids":[31]}
[2026-02-03 15:10:20] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 15:10:20] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 15:10:23] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 15:10:23] [INFO] delete_template.php start {"raw_input":{"id":32},"query":{"debug":"true"}}
[2026-02-03 15:10:23] [INFO] delete_template.php parsed ids {"ids":[32]}
[2026-02-03 15:10:23] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 15:10:23] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 15:10:27] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 15:10:27] [INFO] delete_template.php start {"raw_input":{"id":27},"query":{"debug":"true"}}
[2026-02-03 15:10:27] [INFO] delete_template.php parsed ids {"ids":[27]}
[2026-02-03 15:10:27] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 15:10:27] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 15:10:31] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 15:10:31] [INFO] delete_template.php start {"raw_input":{"id":28},"query":{"debug":"true"}}
[2026-02-03 15:10:31] [INFO] delete_template.php parsed ids {"ids":[28]}
[2026-02-03 15:10:31] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 15:10:31] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 15:10:33] [INFO] Plantillas sincronizadas desde Facebook: 7 nuevas, 0 actualizadas, 0 sin cambios
[2026-02-03 15:27:49] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
[2026-02-03 15:27:51] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
[2026-02-03 15:29:55] [DEBUG] Page access {"uri":"\/api\/delete_template.php?debug=true","ip":"140.82.113.6","user_agent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/144.0."}
[2026-02-03 15:29:55] [INFO] delete_template.php start {"raw_input":{"id":33},"query":{"debug":"true"}}
[2026-02-03 15:29:55] [INFO] delete_template.php parsed ids {"ids":[33]}
[2026-02-03 15:29:55] [DEBUG] Class loaded {"class":"Database","file":"\/var\/www\/html\/classes\/Database.php"}
[2026-02-03 15:29:55] [ERROR] Class not found {"class":"Pdo\\Mysql"}
[2026-02-03 15:29:59] [INFO] Plantillas sincronizadas desde Facebook: 1 nuevas, 6 actualizadas, 0 sin cambios
[2026-02-03 15:34:25] [INFO] Plantillas sincronizadas desde Facebook: 0 nuevas, 7 actualizadas, 0 sin cambios
[2026-02-03 16:19:48] [INFO] Mensaje programado creado: ID=7, Usuario=3277, Fecha=2026-02-03 16:20
[2026-02-03 20:47:36] [INFO] Mensaje programado creado: ID=8, Usuario=3250, Fecha=2026-02-03 20:48
[2026-02-03 20:54:33] [INFO] Mensaje programado creado: ID=9, Usuario=3250, Fecha=2026-02-03 20:55
+103
View File
@@ -0,0 +1,103 @@
<?php
/**
* Migración - Agregar columnas para edición y eliminación de mensajes
* Fecha: 28 de enero de 2026
*/
require_once 'config/config.php';
echo "<h2>Migración: Columnas para editar/eliminar mensajes</h2>\n";
echo "<pre>\n";
try {
$db = Database::getInstance();
// Verificar si las columnas ya existen
$columns = [];
$result = $db->fetchAll("SHOW COLUMNS FROM conversations");
foreach ($result as $row) {
$columns[] = $row['Field'];
}
$migrationsNeeded = [];
// Columna edited_at
if (!in_array('edited_at', $columns)) {
$migrationsNeeded[] = [
'column' => 'edited_at',
'sql' => "ALTER TABLE conversations ADD COLUMN edited_at DATETIME NULL DEFAULT NULL COMMENT 'Fecha de última edición'"
];
} else {
echo "✅ Columna 'edited_at' ya existe\n";
}
// Columna deleted_at (para soft delete)
if (!in_array('deleted_at', $columns)) {
$migrationsNeeded[] = [
'column' => 'deleted_at',
'sql' => "ALTER TABLE conversations ADD COLUMN deleted_at DATETIME NULL DEFAULT NULL COMMENT 'Fecha de eliminación (soft delete)'"
];
} else {
echo "✅ Columna 'deleted_at' ya existe\n";
}
// Columna original_content (para guardar contenido original antes de eliminar)
if (!in_array('original_content', $columns)) {
$migrationsNeeded[] = [
'column' => 'original_content',
'sql' => "ALTER TABLE conversations ADD COLUMN original_content TEXT NULL DEFAULT NULL COMMENT 'Contenido original (antes de eliminar)'"
];
} else {
echo "✅ Columna 'original_content' ya existe\n";
}
// Ejecutar migraciones pendientes
if (count($migrationsNeeded) > 0) {
echo "\n📦 Ejecutando " . count($migrationsNeeded) . " migración(es)...\n\n";
foreach ($migrationsNeeded as $migration) {
echo "➡️ Agregando columna '{$migration['column']}'...\n";
try {
$db->execute($migration['sql']);
echo " ✅ Columna '{$migration['column']}' agregada correctamente\n";
} catch (Exception $e) {
echo " ❌ Error: " . $e->getMessage() . "\n";
}
}
// Agregar índice para optimizar búsquedas de mensajes no eliminados
echo "\n➡️ Verificando índices...\n";
try {
$indexes = $db->fetchAll("SHOW INDEX FROM conversations WHERE Key_name = 'idx_deleted_at'");
if (empty($indexes)) {
$db->execute("ALTER TABLE conversations ADD INDEX idx_deleted_at (deleted_at)");
echo " ✅ Índice 'idx_deleted_at' creado\n";
} else {
echo " ✅ Índice 'idx_deleted_at' ya existe\n";
}
} catch (Exception $e) {
echo " ⚠️ Índice no pudo crearse: " . $e->getMessage() . "\n";
}
echo "\n✅ Migración completada!\n";
} else {
echo "\n✅ No hay migraciones pendientes. La base de datos está actualizada.\n";
}
// Mostrar estructura actual de la tabla
echo "\n📋 Estructura actual de la tabla 'conversations':\n";
echo str_repeat('-', 60) . "\n";
$result = $db->fetchAll("SHOW COLUMNS FROM conversations");
foreach ($result as $row) {
$null = $row['Null'] === 'YES' ? 'NULL' : 'NOT NULL';
$default = $row['Default'] !== null ? "DEFAULT '{$row['Default']}'" : '';
echo sprintf(" %-25s %-20s %s %s\n", $row['Field'], $row['Type'], $null, $default);
}
} catch (Exception $e) {
echo "❌ Error fatal: " . $e->getMessage() . "\n";
echo "Stack trace: " . $e->getTraceAsString() . "\n";
}
echo "</pre>\n";
echo "<p><a href='conversations.php'>← Volver a conversaciones</a></p>\n";
+479
View File
@@ -0,0 +1,479 @@
<?php
session_start();
require_once 'config/config.php';
// Verificar autenticación
if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) {
header('Location: login.php');
exit;
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📅 Mensajes Programados - WhatsApp Bot</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
}
.card {
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
border: none;
}
.scheduled-message-card {
margin-bottom: 15px;
transition: all 0.3s ease;
}
.scheduled-message-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
}
.status-badge {
font-size: 0.85rem;
padding: 5px 12px;
}
.status-pending { background: #ffc107; color: #000; }
.status-sent { background: #28a745; color: #fff; }
.status-failed { background: #dc3545; color: #fff; }
.status-cancelled { background: #6c757d; color: #fff; }
.message-preview {
background: #f8f9fa;
padding: 10px;
border-radius: 8px;
margin-top: 10px;
font-size: 0.9rem;
max-height: 100px;
overflow: hidden;
}
.filter-section {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="text-white">
<i class="fas fa-calendar-alt"></i> Mensajes Programados
</h1>
<a href="index.php" class="btn btn-light">
<i class="fas fa-arrow-left"></i> Volver
</a>
</div>
<!-- Filtros -->
<div class="filter-section">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Cliente</label>
<select class="form-select" id="filter-user">
<option value="">Todos los clientes</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Estado</label>
<select class="form-select" id="filter-status">
<option value="">Todos</option>
<option value="pending" selected>Pendientes</option>
<option value="sent">Enviados</option>
<option value="failed">Fallidos</option>
<option value="cancelled">Cancelados</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Desde</label>
<input type="date" class="form-control" id="filter-date-from">
</div>
<div class="col-md-3">
<label class="form-label">Hasta</label>
<input type="date" class="form-control" id="filter-date-to">
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<button class="btn btn-primary w-100" onclick="loadScheduledMessages()">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
<!-- Lista de mensajes -->
<div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-list"></i> Mensajes Programados
</h5>
<span class="badge bg-light text-dark" id="total-count">0 mensajes</span>
</div>
<div class="card-body" id="messages-container">
<div class="text-center py-5">
<i class="fas fa-spinner fa-spin fa-3x text-muted"></i>
<p class="mt-3 text-muted">Cargando mensajes...</p>
</div>
</div>
</div>
</div>
<!-- Modal para editar mensaje -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">
<i class="fas fa-edit"></i> Editar Mensaje Programado
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-message-id">
<div class="mb-3">
<label class="form-label">Fecha</label>
<input type="date" class="form-control" id="edit-date">
</div>
<div class="mb-3">
<label class="form-label">Hora</label>
<input type="time" class="form-control" id="edit-time">
</div>
<div id="edit-parameters-container"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancelar
</button>
<button type="button" class="btn btn-primary" onclick="saveEdit()">
<i class="fas fa-save"></i> Guardar Cambios
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentMessages = [];
// Cargar usuarios para el filtro
async function loadUsers() {
try {
const response = await fetch('api/get_users.php');
const data = await response.json();
if (data.success) {
const select = document.getElementById('filter-user');
data.data.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = `${user.name} (${user.phone_number})`;
select.appendChild(option);
});
}
} catch (error) {
console.error('Error loading users:', error);
}
}
// Cargar mensajes programados
async function loadScheduledMessages() {
const container = document.getElementById('messages-container');
container.innerHTML = '<div class="text-center py-5"><i class="fas fa-spinner fa-spin fa-3x text-muted"></i></div>';
try {
const userId = document.getElementById('filter-user').value;
const status = document.getElementById('filter-status').value;
const dateFrom = document.getElementById('filter-date-from').value;
const dateTo = document.getElementById('filter-date-to').value;
let url = 'api/get_scheduled_messages.php?limit=100';
if (userId) url += `&user_id=${userId}`;
if (status) url += `&status=${status}`;
if (dateFrom) url += `&date_from=${dateFrom}`;
if (dateTo) url += `&date_to=${dateTo}`;
console.log('Fetching:', url);
const response = await fetch(url);
console.log('Response status:', response.status);
const data = await response.json();
console.log('Response data:', data);
if (data.success) {
currentMessages = data.data;
renderMessages(data.data);
document.getElementById('total-count').textContent = `${data.total} mensaje${data.total !== 1 ? 's' : ''}`;
} else {
container.innerHTML = `<div class="alert alert-danger">Error: ${data.error || 'Error desconocido'}</div>`;
}
} catch (error) {
console.error('Error completo:', error);
container.innerHTML = `<div class="alert alert-danger">Error de conexión: ${error.message}</div>`;
}
}
// Renderizar mensajes
function renderMessages(messages) {
const container = document.getElementById('messages-container');
if (messages.length === 0) {
container.innerHTML = `
<div class="text-center py-5">
<i class="fas fa-calendar-times fa-3x text-muted"></i>
<p class="mt-3 text-muted">No hay mensajes programados</p>
</div>
`;
return;
}
let html = '<div class="row">';
messages.forEach(msg => {
const statusClass = `status-${msg.status}`;
const date = new Date(msg.scheduled_date + ' ' + msg.scheduled_time);
const dateStr = date.toLocaleDateString('es-ES', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
const timeStr = date.toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
});
let preview = '';
if (msg.message_type === 'template') {
preview = `<strong>Plantilla:</strong> ${msg.template_display_name || msg.template_name}`;
if (msg.template_body) {
let bodyPreview = msg.template_body;
if (msg.template_parameters && Array.isArray(msg.template_parameters)) {
msg.template_parameters.forEach((param, index) => {
bodyPreview = bodyPreview.replace(`{{${index + 1}}}`, param);
});
}
preview += `<div class="message-preview">${bodyPreview}</div>`;
}
} else {
preview = `<div class="message-preview">${msg.message_content}</div>`;
}
html += `
<div class="col-md-6">
<div class="card scheduled-message-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="mb-0">
<i class="fas fa-user"></i> ${msg.user_name}
<small class="text-muted">(${msg.phone_number})</small>
</h6>
<span class="badge ${statusClass}">${getStatusText(msg.status)}</span>
</div>
<div class="mb-2">
<i class="fas fa-calendar"></i> ${dateStr}
<br>
<i class="fas fa-clock"></i> ${timeStr}
</div>
${preview}
${msg.error_message ? `<div class="alert alert-danger mt-2 mb-0"><small>${msg.error_message}</small></div>` : ''}
<div class="mt-3 d-flex gap-2">
${msg.status === 'pending' ? `
<button class="btn btn-sm btn-primary" onclick="editMessage(${msg.id})">
<i class="fas fa-edit"></i> Editar
</button>
<button class="btn btn-sm btn-warning" onclick="cancelMessage(${msg.id})">
<i class="fas fa-ban"></i> Cancelar
</button>
` : ''}
<button class="btn btn-sm btn-danger" onclick="deleteMessage(${msg.id})">
<i class="fas fa-trash"></i> Eliminar
</button>
</div>
</div>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
function getStatusText(status) {
const texts = {
'pending': 'Pendiente',
'sent': 'Enviado',
'failed': 'Fallido',
'cancelled': 'Cancelado'
};
return texts[status] || status;
}
// Editar mensaje
function editMessage(id) {
const msg = currentMessages.find(m => m.id == id);
if (!msg) return;
document.getElementById('edit-message-id').value = msg.id;
document.getElementById('edit-date').value = msg.scheduled_date;
document.getElementById('edit-time').value = msg.scheduled_time;
// Si tiene parámetros de plantilla, mostrar campos para editarlos
const paramsContainer = document.getElementById('edit-parameters-container');
paramsContainer.innerHTML = '';
if (msg.template_parameters && Array.isArray(msg.template_parameters)) {
paramsContainer.innerHTML = '<label class="form-label">Parámetros de la Plantilla</label>';
msg.template_parameters.forEach((param, index) => {
paramsContainer.innerHTML += `
<div class="mb-2">
<input type="text"
class="form-control form-control-sm"
id="edit-param-${index}"
value="${param}"
placeholder="Variable ${index + 1}">
</div>
`;
});
}
const modal = new bootstrap.Modal(document.getElementById('editModal'));
modal.show();
}
// Guardar cambios
async function saveEdit() {
const id = document.getElementById('edit-message-id').value;
const date = document.getElementById('edit-date').value;
const time = document.getElementById('edit-time').value;
// Recopilar parámetros editados
const msg = currentMessages.find(m => m.id == id);
let parameters = null;
if (msg && msg.template_parameters && Array.isArray(msg.template_parameters)) {
parameters = [];
msg.template_parameters.forEach((_, index) => {
const input = document.getElementById(`edit-param-${index}`);
if (input) {
parameters.push(input.value);
}
});
}
try {
const payload = {
id: parseInt(id),
scheduled_date: date,
scheduled_time: time
};
if (parameters) {
payload.template_parameters = parameters;
}
const response = await fetch('api/update_scheduled_message.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.success) {
alert('Mensaje actualizado exitosamente');
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
loadScheduledMessages();
} else {
alert('Error: ' + data.error);
}
} catch (error) {
console.error('Error:', error);
alert('Error al guardar cambios');
}
}
// Cancelar mensaje
async function cancelMessage(id) {
if (!confirm('¿Cancelar este mensaje programado?')) return;
try {
const response = await fetch('api/update_scheduled_message.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status: 'cancelled' })
});
const data = await response.json();
if (data.success) {
loadScheduledMessages();
} else {
alert('Error: ' + data.error);
}
} catch (error) {
console.error('Error:', error);
alert('Error al cancelar mensaje');
}
}
// Eliminar mensaje
async function deleteMessage(id) {
if (!confirm('¿Eliminar este mensaje programado? Esta acción no se puede deshacer.')) return;
try {
const response = await fetch('api/delete_scheduled_message.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const data = await response.json();
if (data.success) {
loadScheduledMessages();
} else {
alert('Error: ' + data.error);
}
} catch (error) {
console.error('Error:', error);
alert('Error al eliminar mensaje');
}
}
// Inicializar
document.addEventListener('DOMContentLoaded', () => {
loadUsers();
loadScheduledMessages();
// Establecer fecha de hoy como mínimo
const today = new Date().toISOString().split('T')[0];
document.getElementById('filter-date-from').value = today;
});
</script>
</body>
</html>
+190
View File
@@ -0,0 +1,190 @@
#!/bin/bash
# ============================================
# 🔍 Script de Verificación de Cache
# ============================================
echo "═══════════════════════════════════════════"
echo "🔍 VERIFICANDO CONFIGURACIÓN ANTI-CACHÉ"
echo "═══════════════════════════════════════════"
echo ""
# Colores
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# URL a verificar (cambiar según tu instalación)
URL="http://localhost:8080/conversations.php"
# Si se pasa un argumento, usarlo como URL
if [ ! -z "$1" ]; then
URL="$1"
fi
echo "📍 URL a verificar: $URL"
echo ""
# Verificar headers HTTP
echo "🔍 Verificando headers HTTP..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
HEADERS=$(curl -I -s "$URL")
# Función para verificar header
check_header() {
local header_name="$1"
local expected_value="$2"
if echo "$HEADERS" | grep -qi "$header_name"; then
local actual_value=$(echo "$HEADERS" | grep -i "$header_name" | cut -d: -f2- | tr -d '\r\n' | sed 's/^[[:space:]]*//')
echo -e "${GREEN}$header_name:${NC} $actual_value"
if [ ! -z "$expected_value" ]; then
if echo "$actual_value" | grep -qi "$expected_value"; then
echo -e " ${GREEN}→ Contiene '$expected_value' ✓${NC}"
else
echo -e " ${YELLOW}⚠️ No contiene '$expected_value' esperado${NC}"
fi
fi
return 0
else
echo -e "${RED}$header_name: NO ENCONTRADO${NC}"
return 1
fi
}
echo ""
echo "1️⃣ Cache-Control"
check_header "Cache-Control" "no-store"
echo ""
echo "2️⃣ Pragma"
check_header "Pragma" "no-cache"
echo ""
echo "3️⃣ Expires"
check_header "Expires" "1970"
echo ""
echo "4️⃣ Content-Type"
check_header "Content-Type" "text/html"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Verificar contenido HTML
echo "🔍 Verificando meta tags en HTML..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
CONTENT=$(curl -s "$URL")
check_meta() {
local meta_name="$1"
local search_pattern="$2"
if echo "$CONTENT" | grep -qi "$search_pattern"; then
echo -e "${GREEN}✅ Meta tag '$meta_name' encontrado${NC}"
return 0
else
echo -e "${RED}❌ Meta tag '$meta_name' NO encontrado${NC}"
return 1
fi
}
echo ""
check_meta "Cache-Control" 'meta.*http-equiv.*Cache-Control.*no-cache'
check_meta "Pragma" 'meta.*http-equiv.*Pragma.*no-cache'
check_meta "Expires" 'meta.*http-equiv.*Expires.*0'
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Verificar cache busting en scripts
echo "🔍 Verificando cache busting en scripts..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if echo "$CONTENT" | grep -qE 'chat-common\.js\?v=[0-9]+\.[0-9]+'; then
VERSION=$(echo "$CONTENT" | grep -oE 'chat-common\.js\?v=[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo -e "${GREEN}✅ Cache busting encontrado:${NC} $VERSION"
# Extraer solo el número de versión
VERSION_NUMBER=$(echo "$VERSION" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
echo -e " ${GREEN}→ Versión actual:${NC} $VERSION_NUMBER"
else
echo -e "${RED}❌ Cache busting NO encontrado o formato incorrecto${NC}"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Verificar versión en consola
if echo "$CONTENT" | grep -q "WHATSAPP BOT v1.2.1 - ANTI-CACHÉ ACTIVADO"; then
echo -e "${GREEN}✅ Versión 1.2.1 con anti-caché confirmada${NC}"
else
echo -e "${YELLOW}⚠️ Versión no detectada o diferente${NC}"
fi
echo ""
echo "═══════════════════════════════════════════"
echo "📊 RESUMEN"
echo "═══════════════════════════════════════════"
# Contar checks exitosos y fallidos
TOTAL_CHECKS=7
PASSED=0
FAILED=0
if echo "$HEADERS" | grep -qi "Cache-Control.*no-store"; then ((PASSED++)); else ((FAILED++)); fi
if echo "$HEADERS" | grep -qi "Pragma.*no-cache"; then ((PASSED++)); else ((FAILED++)); fi
if echo "$HEADERS" | grep -qi "Expires.*1970"; then ((PASSED++)); else ((FAILED++)); fi
if echo "$CONTENT" | grep -qi 'meta.*Cache-Control.*no-cache'; then ((PASSED++)); else ((FAILED++)); fi
if echo "$CONTENT" | grep -qi 'meta.*Pragma.*no-cache'; then ((PASSED++)); else ((FAILED++)); fi
if echo "$CONTENT" | grep -qi 'meta.*Expires.*0'; then ((PASSED++)); else ((FAILED++)); fi
if echo "$CONTENT" | grep -qE 'chat-common\.js\?v=[0-9]+'; then ((PASSED++)); else ((FAILED++)); fi
echo ""
echo -e "${GREEN}✅ Checks exitosos: $PASSED/$TOTAL_CHECKS${NC}"
echo -e "${RED}❌ Checks fallidos: $FAILED/$TOTAL_CHECKS${NC}"
echo ""
if [ $FAILED -eq 0 ]; then
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
echo -e "${GREEN}🎉 ¡TODO ESTÁ CONFIGURADO CORRECTAMENTE!${NC}"
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
echo ""
echo "✅ La página está configurada para NO cachearse"
echo "✅ Los scripts tienen cache busting activo"
echo "✅ Los usuarios verán siempre la versión más reciente"
echo ""
echo "💡 Si aún ves problemas, pide al usuario hacer:"
echo " - Ctrl+Shift+R (hard reload)"
echo " - Limpiar caché del navegador"
exit 0
else
echo -e "${YELLOW}═══════════════════════════════════════════${NC}"
echo -e "${YELLOW}⚠️ HAY PROBLEMAS DE CONFIGURACIÓN${NC}"
echo -e "${YELLOW}═══════════════════════════════════════════${NC}"
echo ""
echo "🔧 Posibles soluciones:"
echo ""
echo "1. Verifica que mod_headers esté habilitado en Apache:"
echo " sudo a2enmod headers"
echo " sudo systemctl restart apache2"
echo ""
echo "2. Verifica que .htaccess se esté leyendo:"
echo " - Asegúrate que AllowOverride All esté configurado"
echo ""
echo "3. Verifica que conversations.php tenga los headers PHP:"
echo " - Busca 'header(\"Cache-Control' al inicio del archivo"
echo ""
echo "4. Si usas Nginx en lugar de Apache:"
echo " - Los headers deben configurarse en el server block"
echo ""
exit 1
fi
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env php
<?php
/**
* Script para enviar mensajes programados
* Se ejecuta cada minuto desde cron o worker
*
* Uso:
* php send_scheduled_messages.php
*/
require_once __DIR__ . '/config/config.php';
// Configurar logs
$logFile = __DIR__ . '/logs/scheduled_messages.log';
function writeScheduledLog($message) {
global $logFile;
$timestamp = date('Y-m-d H:i:s');
file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
}
writeScheduledLog("=== Iniciando envío de mensajes programados ===");
try {
$db = Database::getInstance();
$whatsappService = new WhatsAppService();
// Obtener mensajes programados para enviar
// Buscar mensajes cuya fecha/hora ya pasó y están pendientes
$currentDateTime = date('Y-m-d H:i:s');
$currentDate = date('Y-m-d');
$currentTime = date('H:i:00'); // Redondear a minuto exacto
$sql = "SELECT sm.*, u.phone_number, u.name as user_name
FROM scheduled_messages sm
LEFT JOIN users u ON sm.user_id = u.id
WHERE sm.status = 'pending'
AND (
(sm.scheduled_date < ? AND sm.scheduled_time <= ?)
OR (sm.scheduled_date = ? AND sm.scheduled_time <= ?)
)
LIMIT 50";
$messages = $db->fetchAll($sql, [$currentDate, '23:59:59', $currentDate, $currentTime]);
writeScheduledLog("Mensajes encontrados para enviar: " . count($messages));
if (empty($messages)) {
writeScheduledLog("No hay mensajes programados para enviar");
exit(0);
}
$sentCount = 0;
$errorCount = 0;
foreach ($messages as $msg) {
try {
writeScheduledLog("Procesando mensaje ID {$msg['id']} para usuario {$msg['user_name']} ({$msg['phone_number']})");
// Verificar que el usuario tenga teléfono
if (empty($msg['phone_number'])) {
throw new Exception("Usuario sin número de teléfono");
}
$response = null;
if ($msg['message_type'] === 'template') {
// Enviar plantilla
$templateName = $msg['template_name'];
$language = $msg['template_language'] ?? 'es';
$parameters = [];
if (!empty($msg['template_parameters'])) {
$parameters = json_decode($msg['template_parameters'], true);
if (!is_array($parameters)) {
$parameters = [];
}
}
writeScheduledLog("Enviando plantilla: {$templateName} con " . count($parameters) . " parámetros");
$response = $whatsappService->sendTemplateMessage(
$msg['phone_number'],
$templateName,
$language,
$parameters
);
} else if ($msg['message_type'] === 'text') {
// Enviar mensaje de texto
$content = $msg['message_content'];
writeScheduledLog("Enviando mensaje de texto");
$response = $whatsappService->sendTextMessage(
$msg['phone_number'],
$content
);
} else {
throw new Exception("Tipo de mensaje no soportado: " . $msg['message_type']);
}
// Verificar respuesta
if ($response && (isset($response['messages']) || isset($response['success']))) {
// Marcar como enviado
$db->execute(
"UPDATE scheduled_messages SET status = 'sent', sent_at = NOW(), updated_at = NOW() WHERE id = ?",
[$msg['id']]
);
writeScheduledLog("✅ Mensaje ID {$msg['id']} enviado exitosamente");
$sentCount++;
} else {
throw new Exception("Respuesta inválida de WhatsApp API: " . json_encode($response));
}
} catch (Exception $e) {
$errorMessage = $e->getMessage();
writeScheduledLog("❌ Error enviando mensaje ID {$msg['id']}: {$errorMessage}");
// Marcar como fallido
$db->execute(
"UPDATE scheduled_messages SET status = 'failed', error_message = ?, updated_at = NOW() WHERE id = ?",
[$errorMessage, $msg['id']]
);
$errorCount++;
}
// Pequeña pausa entre envíos para no saturar API
usleep(250000); // 250ms
}
writeScheduledLog("=== Resumen ===");
writeScheduledLog("Enviados: {$sentCount}");
writeScheduledLog("Errores: {$errorCount}");
writeScheduledLog("=== Fin ===");
} catch (Exception $e) {
writeScheduledLog("ERROR FATAL: " . $e->getMessage());
writeScheduledLog($e->getTraceAsString());
exit(1);
}
exit(0);
+56 -7
View File
@@ -851,8 +851,9 @@ class BotService {
break;
case 'message':
// Enviar mensaje de respuesta
$this->whatsappService->sendTextMessage($phoneNumber, $option['action_value']);
// Enviar mensaje de respuesta (procesando saltos de línea)
$message = $this->processLineBreaks($option['action_value']);
$this->whatsappService->sendTextMessage($phoneNumber, $message);
$this->exitMenu($phoneNumber);
break;
@@ -864,6 +865,7 @@ class BotService {
case 'request_documents':
// Solicitar al usuario que envíe documentación y activar estado awaiting_documents
$message = $option['action_value'] ?: "Por favor, envíe los documentos o fotos requeridas.";
$message = $this->processLineBreaks($message);
$this->whatsappService->sendTextMessage($phoneNumber, $message);
try {
$stateSvc = new ConversationStateService();
@@ -891,6 +893,7 @@ class BotService {
case 'end':
// Finalizar conversación (comportamiento por defecto)
$message = $option['action_value'] ?: "Gracias por usar nuestro servicio. ¡Hasta pronto!";
$message = $this->processLineBreaks($message);
$this->whatsappService->sendTextMessage($phoneNumber, $message);
$this->exitMenu($phoneNumber);
// Pausar el bot para esta conversación por 3 minutos (antes eran 4 horas)
@@ -899,6 +902,20 @@ class BotService {
}
}
/**
* Procesar texto para respetar saltos de línea
* Convierte \n escapados a saltos reales
*/
private function processLineBreaks($text) {
if (empty($text)) return $text;
// Convertir \n escapados (cadena literal) a saltos de línea reales
$text = str_replace('\\n', "\n", $text);
$text = str_replace('\n', "\n", $text);
return $text;
}
/**
* Procesar llamada API externa
*/
@@ -934,7 +951,8 @@ class BotService {
);
if ($autoResponse) {
$this->whatsappService->sendTextMessage($phoneNumber, $autoResponse['response_text']);
$message = $this->processLineBreaks($autoResponse['response_text']);
$this->whatsappService->sendTextMessage($phoneNumber, $message);
// Si la keyword es 'menu', mostrar el menú
if ($keyword === 'menu' || $keyword === 'menú') {
@@ -1036,6 +1054,23 @@ class BotService {
$this->db->update('users', ['advisor_requested' => 1, 'bot_paused_until' => $pausedUntil, 'bot_enabled' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Le hemos transferido a un asesor; le responderemos en breve.");
// Enviar automáticamente el menú de horario_asesor para que el cliente sepa cuándo esperar
try {
$horarioMenu = $this->db->fetch(
"SELECT id FROM menus WHERE name = :name AND is_active = 1",
['name' => 'horario_asesor']
);
if ($horarioMenu && !empty($horarioMenu['id'])) {
$this->showMenu($phoneNumber, $horarioMenu['id']);
error_log("[BotService] Sent horario_asesor menu to {$phoneNumber} after advisor request");
} else {
error_log("[BotService] horario_asesor menu not found when requesting advisor for {$phoneNumber}");
}
} catch (Exception $e) {
error_log('Failed to send horario_asesor menu on advisor request: ' . $e->getMessage());
}
if (function_exists('writeLog')) writeLog('INFO', "Advisor solicited for $phoneNumber until $pausedUntil");
} catch (Exception $e) {
error_log('requestAdvisor failed: ' . $e->getMessage());
@@ -1152,15 +1187,29 @@ class BotService {
if (function_exists('writeLog')) writeLog('INFO', "Operator {$operatorId} finished attending for {$phoneNumber}, survey={$template}");
// Ensure consistent behavior: release hold / clear flags when finishing attend
// Do NOT send the main menu nor notify the user that the conversation is resumed.
try {
// Release hold silently to avoid duplicate notification after finishing attend
// Send notification that conversation has been resumed, but do NOT auto-send the menu.
$this->releaseHold($phoneNumber, true);
// Intentionally not calling showMainMenu or sending a 'conversation resumed' message here.
$this->releaseHold($phoneNumber, false);
} catch (Exception $e) {
error_log('finishAttendConversation: releaseHold failed: ' . $e->getMessage());
}
// Enviar automáticamente el menú de horario_asesor después de finalizar
try {
$horarioMenu = $this->db->fetch(
"SELECT id FROM menus WHERE name = :name AND is_active = 1",
['name' => 'horario_asesor']
);
if ($horarioMenu && !empty($horarioMenu['id'])) {
$this->showMenu($phoneNumber, $horarioMenu['id']);
error_log("[BotService] Sent horario_asesor menu to {$phoneNumber} after finishing attend");
} else {
error_log("[BotService] horario_asesor menu not found for {$phoneNumber}");
}
} catch (Exception $e) {
error_log('Failed to send horario_asesor menu: ' . $e->getMessage());
}
} catch (Exception $e) {
error_log('finishAttendConversation failed: ' . $e->getMessage());
throw $e;
-352
View File
@@ -1,352 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test SSE - WhatsApp Bot</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 20px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #25d366;
margin-bottom: 20px;
}
.controls {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
}
button {
padding: 10px 20px;
margin: 5px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn-connect {
background: #25d366;
color: white;
}
.btn-connect:hover {
background: #128c7e;
}
.btn-disconnect {
background: #dc3545;
color: white;
}
.btn-disconnect:hover {
background: #c82333;
}
.btn-clear {
background: #6c757d;
color: white;
}
.status {
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-weight: bold;
}
.status.connected {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.disconnected {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.connecting {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
.events {
margin-top: 20px;
}
.event {
padding: 12px;
margin: 8px 0;
border-left: 4px solid;
border-radius: 4px;
background: #f8f9fa;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.event-connected {
border-color: #28a745;
background: #d4edda;
}
.event-new_message {
border-color: #007bff;
background: #cfe2ff;
}
.event-new_conversation {
border-color: #17a2b8;
background: #d1ecf1;
}
.event-notification {
border-color: #ffc107;
background: #fff3cd;
}
.event-heartbeat {
border-color: #6c757d;
background: #e2e3e5;
opacity: 0.7;
}
.event-error {
border-color: #dc3545;
background: #f8d7da;
}
.timestamp {
color: #6c757d;
font-size: 11px;
margin-right: 10px;
}
.event-type {
font-weight: bold;
color: #495057;
margin-right: 10px;
}
.event-data {
margin-top: 8px;
padding: 8px;
background: white;
border-radius: 3px;
white-space: pre-wrap;
word-break: break-all;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat {
background: #e9ecef;
padding: 15px;
border-radius: 6px;
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #495057;
}
.stat-label {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔌 Test SSE - WhatsApp Bot</h1>
<div class="controls">
<button class="btn-connect" onclick="connect()">▶️ Conectar SSE</button>
<button class="btn-disconnect" onclick="disconnect()">⏹️ Desconectar</button>
<button class="btn-clear" onclick="clearEvents()">🗑️ Limpiar eventos</button>
</div>
<div class="status disconnected" id="status">
⚪ Desconectado
</div>
<div class="stats">
<div class="stat">
<div class="stat-value" id="stat-messages">0</div>
<div class="stat-label">Mensajes</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-conversations">0</div>
<div class="stat-label">Conversaciones</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-notifications">0</div>
<div class="stat-label">Notificaciones</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-heartbeats">0</div>
<div class="stat-label">Heartbeats</div>
</div>
</div>
<div class="events">
<h3>📋 Eventos Recibidos:</h3>
<div id="events"></div>
</div>
</div>
<script>
let eventSource = null;
let stats = {
messages: 0,
conversations: 0,
notifications: 0,
heartbeats: 0
};
function updateStatus(text, className) {
const statusEl = document.getElementById('status');
statusEl.textContent = text;
statusEl.className = 'status ' + className;
}
function updateStats() {
document.getElementById('stat-messages').textContent = stats.messages;
document.getElementById('stat-conversations').textContent = stats.conversations;
document.getElementById('stat-notifications').textContent = stats.notifications;
document.getElementById('stat-heartbeats').textContent = stats.heartbeats;
}
function addEvent(type, data) {
const eventsDiv = document.getElementById('events');
const eventDiv = document.createElement('div');
eventDiv.className = 'event event-' + type;
const now = new Date();
const timestamp = now.toLocaleTimeString() + '.' + now.getMilliseconds();
eventDiv.innerHTML = `
<div>
<span class="timestamp">${timestamp}</span>
<span class="event-type">${type}</span>
</div>
<div class="event-data">${JSON.stringify(data, null, 2)}</div>
`;
eventsDiv.insertBefore(eventDiv, eventsDiv.firstChild);
// Limitar a 50 eventos
while (eventsDiv.children.length > 50) {
eventsDiv.removeChild(eventsDiv.lastChild);
}
}
function connect() {
if (eventSource) {
console.log('Ya hay una conexión activa');
return;
}
updateStatus('🔄 Conectando...', 'connecting');
const baseUrl = window.location.origin;
const sseUrl = `${baseUrl}/api/sse_events.php?token=demo_token&t=${Date.now()}`;
console.log('Conectando a:', sseUrl);
eventSource = new EventSource(sseUrl);
eventSource.addEventListener('connected', (e) => {
console.log('✅ Conectado:', e.data);
const data = JSON.parse(e.data);
updateStatus('🟢 Conectado (' + data.mode + ')', 'connected');
addEvent('connected', data);
});
eventSource.addEventListener('new_message', (e) => {
console.log('📨 Nuevo mensaje:', e.data);
const data = JSON.parse(e.data);
stats.messages++;
updateStats();
addEvent('new_message', data);
// Reproducir sonido
const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBTGH0fPTgjMGHm7A7+OZUQ0NVKzo8bllHAU+jdfyvmwhBTGH0fPTgjMGHm7A7+OZUQ0NVKzo8bllHAU+jdfyv2whBTGH0fPTgjMGHm7A7+OZUQ0NVKzo8bllHAU+jdfy');
audio.play().catch(e => console.log('No se pudo reproducir sonido'));
});
eventSource.addEventListener('new_conversation', (e) => {
console.log('💬 Nueva conversación:', e.data);
const data = JSON.parse(e.data);
stats.conversations++;
updateStats();
addEvent('new_conversation', data);
});
eventSource.addEventListener('notification', (e) => {
console.log('🔔 Notificación:', e.data);
const data = JSON.parse(e.data);
stats.notifications++;
updateStats();
addEvent('notification', data);
});
eventSource.addEventListener('heartbeat', (e) => {
const data = JSON.parse(e.data);
stats.heartbeats++;
updateStats();
addEvent('heartbeat', data);
});
eventSource.addEventListener('error', (e) => {
console.error('❌ Error SSE:', e);
if (e.data) {
try {
const data = JSON.parse(e.data);
addEvent('error', data);
} catch (err) {
addEvent('error', { message: 'Error desconocido' });
}
}
});
eventSource.onerror = (error) => {
console.error('❌ Error en conexión SSE:', error);
const state = eventSource.readyState;
if (state === EventSource.CONNECTING) {
updateStatus('🔄 Reconectando...', 'connecting');
} else if (state === EventSource.CLOSED) {
updateStatus('🔴 Desconectado', 'disconnected');
addEvent('error', { message: 'Conexión cerrada', state: state });
eventSource = null;
}
};
}
function disconnect() {
if (eventSource) {
eventSource.close();
eventSource = null;
updateStatus('🔴 Desconectado', 'disconnected');
console.log('Desconectado');
}
}
function clearEvents() {
document.getElementById('events').innerHTML = '';
stats = {
messages: 0,
conversations: 0,
notifications: 0,
heartbeats: 0
};
updateStats();
}
// Auto-conectar al cargar
window.addEventListener('load', () => {
console.log('Página cargada, conectando automáticamente...');
setTimeout(connect, 500);
});
</script>
</body>
</html>
-292
View File
@@ -1,292 +0,0 @@
<?php
/**
* Script de testing para la arquitectura mejorada
* Verifica que todos los componentes estén funcionando correctamente
*/
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/config/config.php';
use WhatsApp\Queue\RedisQueue;
use WhatsApp\Queue\ConversationState;
echo "🧪 Testing Arquitectura Mejorada\n";
echo "================================\n\n";
$results = [
'passed' => 0,
'failed' => 0,
'warnings' => 0
];
function test($name, $callback) {
global $results;
try {
echo "Testing: $name... ";
$result = $callback();
if ($result === true) {
echo "✅ PASS\n";
$results['passed']++;
} elseif ($result === null) {
echo "⚠️ WARNING\n";
$results['warnings']++;
} else {
echo "❌ FAIL: $result\n";
$results['failed']++;
}
} catch (Exception $e) {
echo "❌ ERROR: " . $e->getMessage() . "\n";
$results['failed']++;
}
}
// 1. Test Redis Connection
test("Redis connection", function() {
try {
$redis = new Predis\Client([
'scheme' => getenv('REDIS_SCHEME') ?: 'tcp',
'host' => getenv('REDIS_HOST') ?: '127.0.0.1',
'port' => getenv('REDIS_PORT') ?: 6379,
]);
$ping = $redis->ping();
return ($ping === 'PONG' || $ping === true);
} catch (Exception $e) {
return "No se pudo conectar a Redis: " . $e->getMessage();
}
});
// 2. Test RedisQueue - Push
test("RedisQueue::push", function() {
$queue = new RedisQueue();
$result = $queue->push('test_queue', ['message' => 'test'], 1);
return $result;
});
// 3. Test RedisQueue - Pop
test("RedisQueue::pop", function() {
$queue = new RedisQueue();
$message = $queue->pop('test_queue', 1);
if (!$message) {
return "No se pudo obtener mensaje de la cola";
}
if ($message['data']['message'] !== 'test') {
return "Mensaje recibido no coincide con el enviado";
}
return true;
});
// 4. Test RedisQueue - Stats
test("RedisQueue::getStats", function() {
$queue = new RedisQueue();
$stats = $queue->getStats();
if (!is_array($stats)) {
return "Stats no devuelve un array";
}
return true;
});
// 5. Test ConversationState - Set
test("ConversationState::setState", function() {
$state = new ConversationState();
$result = $state->setState('test_user_123', 'testing', ['key' => 'value'], 300);
return $result;
});
// 6. Test ConversationState - Get
test("ConversationState::getState", function() {
$state = new ConversationState();
$result = $state->getState('test_user_123');
if (!$result) {
return "No se pudo obtener estado";
}
if ($result['state'] !== 'testing') {
return "Estado no coincide";
}
if ($result['context']['key'] !== 'value') {
return "Contexto no coincide";
}
return true;
});
// 7. Test ConversationState - Update Context
test("ConversationState::updateContext", function() {
$state = new ConversationState();
$result = $state->updateContext('test_user_123', ['new_key' => 'new_value']);
if (!$result) {
return "No se pudo actualizar contexto";
}
$current = $state->getState('test_user_123');
if (!isset($current['context']['new_key'])) {
return "Contexto no se actualizó correctamente";
}
return true;
});
// 8. Test ConversationState - Clear
test("ConversationState::clearState", function() {
$state = new ConversationState();
$state->clearState('test_user_123');
$result = $state->getState('test_user_123');
return $result === null;
});
// 9. Test Logger
test("LoggerFactory", function() {
if (!class_exists('LoggerFactory')) {
return "LoggerFactory no existe";
}
$logger = LoggerFactory::getLogger('test');
$logger->info("Test message");
// Verificar que el archivo de log existe
$logPath = __DIR__ . '/logs/test.log';
if (!file_exists($logPath)) {
return "Archivo de log no fue creado";
}
// Limpiar
@unlink($logPath);
return true;
});
// 10. Test Database Connection
test("Database connection", function() {
try {
$db = Database::getInstance();
$result = $db->query("SELECT 1 as test")->fetch();
return $result && $result['test'] == 1;
} catch (Exception $e) {
return "Error de conexión: " . $e->getMessage();
}
});
// 11. Test WhatsApp Config
test("WhatsApp configuration", function() {
if (!function_exists('getWhatsAppConfigFromDB')) {
return "Función getWhatsAppConfigFromDB no existe";
}
$config = getWhatsAppConfigFromDB();
if (empty($config['token'])) {
return null; // Warning, no error crítico
}
return true;
});
// 12. Test file permissions
test("Log directory permissions", function() {
$logDir = __DIR__ . '/logs';
if (!is_dir($logDir)) {
return "Directorio logs no existe";
}
if (!is_writable($logDir)) {
return "Directorio logs no tiene permisos de escritura";
}
return true;
});
// 13. Test uploads directory
test("Uploads directory permissions", function() {
$uploadsDir = __DIR__ . '/uploads';
if (!is_dir($uploadsDir)) {
return null; // Warning
}
if (!is_writable($uploadsDir)) {
return "Directorio uploads no tiene permisos de escritura";
}
return true;
});
// 14. Test Composer autoload
test("Composer autoload", function() {
if (!file_exists(__DIR__ . '/vendor/autoload.php')) {
return "vendor/autoload.php no existe. Ejecuta: composer install";
}
// Verificar clases clave
if (!class_exists('Predis\Client')) {
return "Predis no está instalado";
}
if (!class_exists('Monolog\Logger')) {
return "Monolog no está instalado";
}
return true;
});
// 15. Test .env file
test(".env configuration", function() {
if (!file_exists(__DIR__ . '/.env')) {
return "Archivo .env no existe. Copia .env.example";
}
$requiredVars = ['DB_HOST', 'DB_NAME', 'REDIS_HOST'];
foreach ($requiredVars as $var) {
if (!getenv($var)) {
return "Variable $var no está configurada en .env";
}
}
return true;
});
// Limpiar test data
echo "\n🧹 Limpiando datos de test...\n";
try {
$queue = new RedisQueue();
$queue->flush('test_queue');
$state = new ConversationState();
$state->clearState('test_user_123');
echo "✅ Limpieza completada\n";
} catch (Exception $e) {
echo "⚠️ Error en limpieza: " . $e->getMessage() . "\n";
}
// Resultados finales
echo "\n";
echo "================================\n";
echo "📊 RESULTADOS\n";
echo "================================\n";
echo "✅ Pasados: " . $results['passed'] . "\n";
echo "❌ Fallidos: " . $results['failed'] . "\n";
echo "⚠️ Warnings: " . $results['warnings'] . "\n";
echo "================================\n";
if ($results['failed'] > 0) {
echo "\n❌ Algunos tests fallaron. Revisa la configuración.\n";
exit(1);
} elseif ($results['warnings'] > 0) {
echo "\n⚠️ Tests completados con warnings. Algunas funciones pueden no estar completamente configuradas.\n";
exit(0);
} else {
echo "\n✅ Todos los tests pasaron correctamente!\n";
echo "\n🚀 Tu arquitectura está lista para usarse.\n";
exit(0);
}
-66
View File
@@ -1,66 +0,0 @@
#!/bin/bash
# Script para probar envío de archivos multimedia
# Verifica que se guarden correctamente en la BD
echo "🧪 Prueba de Envío de Archivos Multimedia"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Verificar estructura de BD
echo "1️⃣ Verificando columnas de la tabla conversations..."
docker exec whatsapp-dev-app php -r "
require 'config/config.php';
\$db = Database::getInstance();
\$cols = \$db->fetchAll('DESCRIBE conversations');
\$important = ['whatsapp_media_id', 'media_url', 'local_file', 'local_thumb', 'filename', 'mime_type'];
echo 'Columnas importantes:' . PHP_EOL;
foreach (\$cols as \$col) {
if (in_array(\$col['Field'], \$important)) {
echo ' ✅ ' . \$col['Field'] . ' (' . \$col['Type'] . ')' . PHP_EOL;
}
}
" || { echo "❌ Error verificando BD"; exit 1; }
echo ""
echo "2️⃣ Últimos 3 mensajes multimedia enviados:"
docker exec whatsapp-dev-app php -r "
require 'config/config.php';
\$db = Database::getInstance();
\$messages = \$db->fetchAll(
'SELECT id, user_id, message_type, content, media_url, whatsapp_media_id,
local_file, filename, created_at
FROM conversations
WHERE message_type IN (\"image\", \"video\", \"audio\", \"document\")
AND direction = \"outgoing\"
ORDER BY created_at DESC
LIMIT 3'
);
if (empty(\$messages)) {
echo ' ️ No hay mensajes multimedia en la BD aún' . PHP_EOL;
} else {
foreach (\$messages as \$msg) {
echo PHP_EOL;
echo ' 📎 ID: ' . \$msg['id'] . PHP_EOL;
echo ' Tipo: ' . \$msg['message_type'] . PHP_EOL;
echo ' Contenido: ' . substr(\$msg['content'], 0, 50) . PHP_EOL;
echo ' Media ID WA: ' . (\$msg['whatsapp_media_id'] ?: 'null') . PHP_EOL;
echo ' Archivo Local: ' . (\$msg['local_file'] ?: 'null') . PHP_EOL;
echo ' Fecha: ' . \$msg['created_at'] . PHP_EOL;
}
}
"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Pruebas completadas"
echo ""
echo "📝 Cómo probar:"
echo " 1. Abre http://localhost:8080/conversations.php"
echo " 2. Selecciona una conversación"
echo " 3. Haz click en el botón adjuntar (📎)"
echo " 4. Selecciona una imagen o documento"
echo " 5. Envía el archivo"
echo " 6. Ejecuta este script de nuevo para ver si se guardó"
echo ""
-121
View File
@@ -1,121 +0,0 @@
# 🧪 Prueba de Mensajes Nuevos en Tiempo Real
## ✅ Cambios Implementados:
### 1. **Estilo Visual para Mensajes No Leídos**
- Fondo amarillo claro (#fffbea) con borde sutil
- Animación de highlight al aparecer
- Se quita automáticamente después de 3 segundos
### 2. **Detección Mejorada de Scroll**
- Logs detallados en consola para debugging
- Cambio de `initial=false` a `initial=true` cuando estás abajo (esto fuerza una recarga completa)
- Marca el último mensaje como no leído temporalmente
### 3. **Flujo de Funcionamiento:**
#### **Cuando estás AL FINAL del chat:**
1. SSE detecta nuevo mensaje entrante
2. Verifica que estás dentro de 150px del final
3. Marca flag `_nextMessageUnread = true`
4. Llama `loadMessages(userId, true, true)` para recargar
5. El último mensaje se renderiza con clase `unread`
6. Después de 3 segundos, quita el highlight automáticamente
#### **Cuando estás LEYENDO ARRIBA:**
1. SSE detecta nuevo mensaje entrante
2. Verifica que NO estás cerca del final
3. Muestra botón "Nuevo mensaje" o "Nuevos mensajes (N)"
4. Guarda mensaje en staging
5. Cuando haces click, baja y muestra todos los mensajes
## 🔍 Cómo Probar:
### Paso 1: Abrir Navegador y Consola
```bash
# Abrir http://localhost:8080/conversations.php
# Presionar F12 para abrir Developer Tools
# Ver pestaña "Console"
```
### Paso 2: Abrir una Conversación
- Hacer click en cualquier conversación de la lista
- Verificar en consola que ves: `✅ SSE conectado (🔐 autenticado)`
### Paso 3: Enviar Mensaje de Prueba desde WhatsApp
**IMPORTANTE:** Envía el mensaje desde WhatsApp a tu número de bot
### Paso 4: Observar Logs en Consola
Deberías ver algo como:
```
📨 SSE new_message recibido: {...}
📨 Datos parseados del mensaje: {...}
👤 User ID del mensaje: 123 | Conversación actual: 123
♻️ Mensaje para conversación ACTIVA, procesando...
📍 Posición scroll: {scrollHeight: 1234, scrollTop: 1000, clientHeight: 600, distanceFromBottom: 34, isAtBottom: true}
✅ Usuario AL FINAL, recargando mensajes...
```
### Paso 5: Verificar Resultado
- El mensaje debe aparecer automáticamente (sin refrescar)
- Debe tener fondo amarillo claro durante 3 segundos
- Después el fondo vuelve a blanco
### Paso 6: Probar Scroll Arriba
1. Hacer scroll hacia arriba (leer mensajes viejos)
2. Enviar otro mensaje desde WhatsApp
3. Debe aparecer botón "Nuevo mensaje" en la parte inferior
4. Al hacer click, baja y muestra el mensaje
## 🐛 Debugging:
### Si no aparece el mensaje cuando estás abajo:
1. **Verificar SSE está conectado:**
```bash
# En otra terminal:
./test_sse_debug.sh
```
2. **Verificar logs en consola:**
- Buscar "SSE new_message recibido"
- Buscar "Usuario AL FINAL"
- Buscar errores en rojo
3. **Verificar que el mensaje se guardó en BD:**
```bash
docker exec -it whatsapp-dev-app php -r "
require 'config/config.php';
\$db = Database::getInstance();
\$msgs = \$db->fetchAll('SELECT * FROM conversations ORDER BY created_at DESC LIMIT 5');
print_r(\$msgs);
"
```
4. **Verificar el SSE está enviando eventos:**
- Abrir http://localhost:8080/api/sse_events.php?token=demo_token directamente
- Deberías ver stream de eventos (connected, heartbeat cada 30s)
### Si el highlight no aparece:
1. **Verificar flag se está seteando:**
- Buscar en consola: "Usuario AL FINAL, recargando mensajes..."
- Abrir Inspector (Elementos) y buscar la clase `unread` en el mensaje
2. **Verificar CSS está cargado:**
- Buscar en Inspector `.message.unread .message-bubble`
- Debe tener `background: #fffbea`
## 📊 Métricas Esperadas:
- **Tiempo de aparición:** 3-5 segundos después de enviar mensaje
- **Duración del highlight:** 3 segundos
- **Intervalo de polling SSE:** Cada 3 segundos revisa BD
- **Heartbeat:** Cada 30 segundos
## ✨ Próximas Mejoras (Opcional):
1. Contador de mensajes no leídos en la lista de conversaciones
2. Marcar conversación como leída cuando abres
3. Notificación de escritura ("Usuario está escribiendo...")
4. Confirmación de entrega y lectura más precisa
-1
View File
@@ -1 +0,0 @@
<?php session_start(); $_SESSION["user_id"] = 1; ?>
-133
View File
@@ -1,133 +0,0 @@
#!/bin/bash
# Script de prueba para el sistema SSE
# Fecha: 27 de enero de 2026
echo "🧪 Testing SSE Real-time System"
echo "================================"
echo ""
# Colores para output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Base URL (cambiar según tu entorno)
BASE_URL="http://localhost"
echo "📝 Configuración:"
echo " Base URL: $BASE_URL"
echo ""
# Test 1: Verificar que sse_events.php es accesible
echo "1️⃣ Probando conexión SSE..."
timeout 3 curl -N -s "$BASE_URL/api/sse_events.php" 2>&1 | head -5 &
PID=$!
sleep 2
if kill -0 $PID 2>/dev/null; then
echo -e " ${GREEN}${NC} SSE endpoint responde"
kill $PID 2>/dev/null
else
echo -e " ${RED}${NC} SSE endpoint no responde"
fi
echo ""
# Test 2: Verificar que push_event.php es accesible
echo "2️⃣ Probando push interno..."
RESPONSE=$(curl -s -X POST "$BASE_URL/api/push_event.php" \
-H "Content-Type: application/json" \
-H "X-Push-Token: internal_push_secret_2026" \
-d '{
"event_type": "test",
"data": {"message": "Test from script"},
"target_user_id": "global"
}')
if echo "$RESPONSE" | grep -q "success"; then
echo -e " ${GREEN}${NC} Push endpoint funciona"
echo " Response: $RESPONSE"
else
echo -e " ${RED}${NC} Push endpoint falló"
echo " Response: $RESPONSE"
fi
echo ""
# Test 3: Verificar permisos en directorio uploads
echo "3️⃣ Verificando permisos de escritura..."
UPLOADS_DIR="../uploads"
if [ -w "$UPLOADS_DIR" ]; then
echo -e " ${GREEN}${NC} Directorio uploads tiene permisos de escritura"
else
echo -e " ${RED}${NC} Directorio uploads NO tiene permisos de escritura"
echo " Ejecuta: chmod 755 $UPLOADS_DIR"
fi
echo ""
# Test 4: Simular evento de mensaje nuevo
echo "4️⃣ Simulando mensaje nuevo..."
PUSH_RESPONSE=$(curl -s -X POST "$BASE_URL/api/push_event.php" \
-H "Content-Type: application/json" \
-H "X-Push-Token: internal_push_secret_2026" \
-d '{
"event_type": "new_message",
"data": {
"user_id": 999,
"phone_number": "573001234567",
"name": "Usuario Prueba",
"message": "Mensaje de prueba desde script",
"message_type": "text",
"timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"
},
"target_user_id": "global"
}')
if echo "$PUSH_RESPONSE" | grep -q "success"; then
EVENT_ID=$(echo "$PUSH_RESPONSE" | grep -o '"event_id":"[^"]*"' | cut -d'"' -f4)
echo -e " ${GREEN}${NC} Evento enviado exitosamente"
echo " Event ID: $EVENT_ID"
else
echo -e " ${RED}${NC} Fallo al enviar evento"
fi
echo ""
# Test 5: Verificar que el evento se guardó
echo "5️⃣ Verificando archivo de eventos..."
EVENT_FILE="../uploads/events_global.json"
if [ -f "$EVENT_FILE" ]; then
echo -e " ${GREEN}${NC} Archivo de eventos existe"
echo " Últimos eventos guardados:"
cat "$EVENT_FILE" | python3 -m json.tool 2>/dev/null | tail -20 || cat "$EVENT_FILE"
else
echo -e " ${YELLOW}${NC} Archivo de eventos no encontrado"
echo " Esto es normal si es la primera ejecución"
fi
echo ""
# Test 6: Probar conexión SSE y recepción de evento
echo "6️⃣ Probando recepción de evento en SSE (5 segundos)..."
timeout 5 curl -N -s "$BASE_URL/api/sse_events.php" 2>&1 | while IFS= read -r line; do
if [[ "$line" == event:* ]]; then
echo -e " ${GREEN}${NC} Evento recibido: $line"
elif [[ "$line" == data:* ]]; then
echo " Data: $line"
fi
done
echo ""
# Resumen
echo "================================"
echo "✅ Pruebas completadas"
echo ""
echo "📌 Próximos pasos:"
echo " 1. Abre conversations.php en el navegador"
echo " 2. Abre DevTools → Console"
echo " 3. Deberías ver: '✅ SSE conectado'"
echo " 4. Envía un mensaje de prueba desde WhatsApp"
echo " 5. El mensaje debería aparecer en < 1 segundo"
echo ""
echo "🐛 Para debugging:"
echo " - Ver logs: tail -f /var/log/apache2/error.log"
echo " - Ver eventos: cat ../uploads/events_global.json"
echo " - Conectar manualmente: curl -N $BASE_URL/api/sse_events.php"
echo ""
-115
View File
@@ -1,115 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Diagnóstico Completo de SSE
* Prueba la conexión SSE y muestra todos los eventos recibidos
*/
echo "═══════════════════════════════════════════════════════\n";
echo " 🔍 DIAGNÓSTICO COMPLETO SSE\n";
echo "═══════════════════════════════════════════════════════\n\n";
// Configuración
$baseUrl = 'http://localhost:8000';
$token = 'demo_token';
$sseUrl = "{$baseUrl}/api/sse_events.php?token={$token}";
echo "📍 URL SSE: {$sseUrl}\n\n";
// Test 1: Verificar que el endpoint responde
echo "═══ Test 1: Verificar Headers ═══\n";
$ch = curl_init($sseUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_NOBODY, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP Code: {$httpCode}\n";
if (strpos($response, 'Content-Type: text/event-stream') !== false) {
echo "✅ Headers SSE correctos\n";
} else {
echo "❌ Headers SSE incorrectos\n";
echo "Headers recibidos:\n{$response}\n";
}
echo "\n═══ Test 2: Conectar y Escuchar Eventos ═══\n";
echo "Esperando eventos (Ctrl+C para cancelar)...\n\n";
// Test 2: Conectar y leer stream
$ch = curl_init($sseUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) {
static $buffer = '';
static $eventCount = 0;
$buffer .= $data;
// Procesar líneas completas
while (($pos = strpos($buffer, "\n")) !== false) {
$line = substr($buffer, 0, $pos);
$buffer = substr($buffer, $pos + 1);
// Parsear línea SSE
if (empty(trim($line))) {
continue;
}
if (strpos($line, 'event:') === 0) {
$eventType = trim(substr($line, 6));
$eventCount++;
echo "[{$eventCount}] 📨 Evento: {$eventType}\n";
} elseif (strpos($line, 'data:') === 0) {
$eventData = trim(substr($line, 5));
$decoded = json_decode($eventData, true);
if ($decoded) {
echo " 📦 Data: " . json_encode($decoded, JSON_PRETTY_PRINT) . "\n";
} else {
echo " 📦 Data: {$eventData}\n";
}
} elseif (strpos($line, 'id:') === 0) {
$eventId = trim(substr($line, 3));
echo " 🆔 ID: {$eventId}\n";
} elseif (strpos($line, 'retry:') === 0) {
$retry = trim(substr($line, 6));
echo " 🔄 Retry: {$retry}ms\n";
} else {
echo " {$line}\n";
}
echo "\n";
}
return strlen($data);
});
echo "Conectando...\n";
$startTime = microtime(true);
$result = curl_exec($ch);
if ($result === false) {
$error = curl_error($ch);
$errno = curl_errno($ch);
echo "\n❌ Error de conexión:\n";
echo " Código: {$errno}\n";
echo " Mensaje: {$error}\n";
} else {
$elapsed = microtime(true) - $startTime;
echo "\n✅ Conexión cerrada después de {$elapsed}s\n";
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
echo "HTTP Code final: {$httpCode}\n";
curl_close($ch);
echo "\n═══════════════════════════════════════════════════════\n";
echo " ✅ Diagnóstico completado\n";
echo "═══════════════════════════════════════════════════════\n";
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
# Script para probar conexión SSE y ver eventos en tiempo real
# Uso: ./test_sse_debug.sh
echo "🔍 Probando conexión SSE..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Conectando a: http://localhost:8080/api/sse_events.php?token=demo_token"
echo ""
echo "📡 Eventos recibidos (presiona Ctrl+C para salir):"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Conectar al SSE y mostrar eventos con timestamp
curl -N -H "Accept: text/event-stream" \
"http://localhost:8080/api/sse_events.php?token=demo_token&t=$(date +%s)" 2>&1 | \
while IFS= read -r line; do
timestamp=$(date '+%H:%M:%S')
if [[ "$line" == event:* ]]; then
echo -e "\n\033[1;36m[$timestamp]\033[0m \033[1;33m${line}\033[0m"
elif [[ "$line" == data:* ]]; then
echo -e "\033[0;32m${line}\033[0m"
elif [[ "$line" == id:* ]]; then
echo -e "\033[0;90m${line}\033[0m"
else
[[ -n "$line" ]] && echo "$line"
fi
done
-30
View File
@@ -1,30 +0,0 @@
<?php
require_once 'config/config.php';
require_once 'classes/Database.php';
$db = Database::getInstance();
$result = $db->getConnection()->query('DESCRIBE media_files');
echo "📋 Tabla media_files creada exitosamente\n\n";
echo "Estructura de la tabla:\n";
echo str_repeat('-', 90) . "\n";
printf("%-20s %-30s %-10s %-10s %-15s\n", 'Campo', 'Tipo', 'Null', 'Key', 'Extra');
echo str_repeat('-', 90) . "\n";
while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
printf("%-20s %-30s %-10s %-10s %-15s\n",
$row['Field'],
$row['Type'],
$row['Null'],
$row['Key'],
$row['Extra']
);
}
echo str_repeat('-', 90) . "\n";
echo "\n✅ Sistema multimedia listo para usar\n";
echo "\n📝 Puedes ahora:\n";
echo " 1. Abrir conversations.php\n";
echo " 2. Seleccionar una conversación\n";
echo " 3. Hacer clic en el botón 📎 para adjuntar archivos\n";
echo " 4. Enviar imágenes, videos, audios o documentos\n\n";
-79
View File
@@ -1,79 +0,0 @@
#!/bin/bash
# Script para verificar la corrección del error de autenticación SSE
# Fecha: 27 de enero de 2026
echo "🔧 Verificando corrección de SSE..."
echo "===================================="
echo ""
# Colores
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Probar conexión SSE sin token
echo "1️⃣ Probando SSE sin token (modo global)..."
RESPONSE=$(timeout 2 curl -s -N "http://localhost/api/sse_events.php" 2>&1 | head -3)
if echo "$RESPONSE" | grep -q "event: connected"; then
echo -e " ${GREEN}${NC} SSE conecta en modo global"
echo "$RESPONSE" | grep -A 1 "event: connected"
else
echo -e " ${YELLOW}${NC} SSE puede requerir configuración adicional"
echo " Response: $RESPONSE"
fi
echo ""
# Probar conexión SSE con token
echo "2️⃣ Probando SSE con token..."
RESPONSE=$(timeout 2 curl -s -N "http://localhost/api/sse_events.php?token=demo_token" 2>&1 | head -3)
if echo "$RESPONSE" | grep -q "event: connected"; then
echo -e " ${GREEN}${NC} SSE conecta con token"
echo "$RESPONSE" | grep -A 1 "event: connected"
else
echo -e " ${RED}${NC} Error al conectar con token"
echo " Response: $RESPONSE"
fi
echo ""
# Verificar que el cliente use el token correcto
echo "3️⃣ Verificando código cliente..."
if grep -q "token=demo_token" ../conversations.php 2>/dev/null; then
echo -e " ${GREEN}${NC} Cliente configurado para usar token"
else
echo -e " ${RED}${NC} Cliente no configurado correctamente"
fi
echo ""
# Simular evento y verificar recepción
echo "4️⃣ Simulando evento de prueba..."
PUSH_RESPONSE=$(curl -s -X POST "http://localhost/api/push_event.php" \
-H "Content-Type: application/json" \
-H "X-Push-Token: internal_push_secret_2026" \
-d '{
"event_type": "test_connection",
"data": {"message": "Prueba de conexión corregida"},
"target_user_id": "global"
}')
if echo "$PUSH_RESPONSE" | grep -q "success"; then
echo -e " ${GREEN}${NC} Evento enviado correctamente"
else
echo -e " ${RED}${NC} Error al enviar evento"
fi
echo ""
echo "===================================="
echo "✅ Verificación completa"
echo ""
echo "📌 Próximos pasos:"
echo " 1. Recarga conversations.php en tu navegador"
echo " 2. Abre DevTools → Console"
echo " 3. Deberías ver: '✅ SSE conectado (🌐 global)' o '✅ SSE conectado (🔐 autenticado)'"
echo " 4. Ya NO deberías ver el error 'Usuario no autenticado'"
echo ""
echo "🎉 El error ha sido corregido!"
echo ""