up
This commit is contained in:
@@ -6,6 +6,9 @@
|
||||
|
||||
require_once '../config/config.php';
|
||||
|
||||
// Verificar autenticación
|
||||
requireAuthentication();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET');
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/**
|
||||
* API interna para empujar eventos SSE
|
||||
* Este endpoint es llamado por el webhook para notificar eventos
|
||||
* Fecha: 27 de enero de 2026
|
||||
*/
|
||||
|
||||
require_once '../config/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Solo permitir llamadas internas
|
||||
$allowedIPs = ['127.0.0.1', '::1', 'localhost'];
|
||||
$clientIP = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
|
||||
// En producción, verificar IP o usar token secreto
|
||||
$secretToken = $_SERVER['HTTP_X_PUSH_TOKEN'] ?? $_POST['push_token'] ?? '';
|
||||
$validToken = getenv('PUSH_TOKEN') ?: 'internal_push_secret_2026';
|
||||
|
||||
if (!in_array($clientIP, $allowedIPs) && $secretToken !== $validToken) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'No autorizado']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input) {
|
||||
$input = $_POST;
|
||||
}
|
||||
|
||||
$eventType = $input['event_type'] ?? 'message';
|
||||
$eventData = $input['data'] ?? [];
|
||||
$targetUserId = $input['target_user_id'] ?? 'all'; // ID del operador, no del cliente
|
||||
|
||||
// Si no se especifica usuario objetivo, broadcast a todos
|
||||
$targets = [];
|
||||
if ($targetUserId === 'all') {
|
||||
// Broadcast: usar archivo global para todos los clientes conectados
|
||||
$targets = ['global'];
|
||||
|
||||
// Opcional: También enviar a usuarios con token conocidos
|
||||
// (buscar archivos events_token_*.json en uploads)
|
||||
$uploadsDir = __DIR__ . '/../uploads/';
|
||||
if (is_dir($uploadsDir)) {
|
||||
$tokenFiles = glob($uploadsDir . 'events_token_*.json');
|
||||
foreach ($tokenFiles as $file) {
|
||||
$basename = basename($file, '.json');
|
||||
$userId = str_replace('events_', '', $basename);
|
||||
if ($userId && $userId !== 'global') {
|
||||
$targets[] = $userId;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$targets = [$targetUserId];
|
||||
}
|
||||
|
||||
// Escribir evento en archivos temporales para cada usuario objetivo
|
||||
$eventId = uniqid('evt_', true);
|
||||
$event = [
|
||||
'id' => $eventId,
|
||||
'type' => $eventType,
|
||||
'data' => $eventData,
|
||||
'timestamp' => time()
|
||||
];
|
||||
|
||||
$written = 0;
|
||||
foreach ($targets as $target) {
|
||||
$eventsFile = __DIR__ . "/../uploads/events_{$target}.json";
|
||||
|
||||
// Asegurar que el directorio existe
|
||||
$dir = dirname($eventsFile);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
// Leer eventos existentes
|
||||
$existingEvents = [];
|
||||
if (file_exists($eventsFile)) {
|
||||
$content = @file_get_contents($eventsFile);
|
||||
if ($content) {
|
||||
$existingEvents = json_decode($content, true) ?: [];
|
||||
}
|
||||
}
|
||||
|
||||
// Agregar nuevo evento
|
||||
$existingEvents[] = $event;
|
||||
|
||||
// Mantener solo últimos 50 eventos
|
||||
if (count($existingEvents) > 50) {
|
||||
$existingEvents = array_slice($existingEvents, -50);
|
||||
}
|
||||
|
||||
// Guardar
|
||||
if (@file_put_contents($eventsFile, json_encode($existingEvents))) {
|
||||
$written++;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'event_id' => $eventId,
|
||||
'targets_notified' => $written
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("push_event.php error: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
+33
-12
@@ -67,20 +67,22 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
|
||||
// Función auxiliar para enviar con media_id
|
||||
function sendMediaByIdToWhatsApp($whatsapp, $recipient, $mediaId, $mediaType, $caption, $filename) {
|
||||
return $whatsapp->sendMediaById($recipient, $mediaId, $mediaType, $caption, $filename);
|
||||
// Pasar skipAutoSave=true para evitar guardado duplicado (lo guardamos manualmente después)
|
||||
return $whatsapp->sendMediaById($recipient, $mediaId, $mediaType, $caption, $filename, true);
|
||||
}
|
||||
|
||||
// Función auxiliar para enviar con link
|
||||
function sendMediaByLinkToWhatsApp($whatsapp, $recipient, $mediaUrl, $mediaType, $caption, $filename) {
|
||||
// Pasar skipAutoSave=true para evitar guardado duplicado (lo guardamos manualmente después)
|
||||
switch ($mediaType) {
|
||||
case 'image':
|
||||
return $whatsapp->sendImageMessage($recipient, $mediaUrl, $caption);
|
||||
return $whatsapp->sendImageMessage($recipient, $mediaUrl, $caption, true);
|
||||
case 'video':
|
||||
return $whatsapp->sendVideoMessage($recipient, $mediaUrl, $caption);
|
||||
return $whatsapp->sendVideoMessage($recipient, $mediaUrl, $caption, true);
|
||||
case 'audio':
|
||||
return $whatsapp->sendAudioMessage($recipient, $mediaUrl);
|
||||
return $whatsapp->sendAudioMessage($recipient, $mediaUrl, true);
|
||||
case 'document':
|
||||
return $whatsapp->sendDocumentMessage($recipient, $mediaUrl, $filename, $caption);
|
||||
return $whatsapp->sendDocumentMessage($recipient, $mediaUrl, $filename, $caption, true);
|
||||
default:
|
||||
throw new Exception('Tipo de media no soportado: ' . $mediaType);
|
||||
}
|
||||
@@ -333,26 +335,45 @@ try {
|
||||
);
|
||||
|
||||
if ($user) {
|
||||
// Determinar content: prioridad filename > caption > descripción por defecto
|
||||
$content = '';
|
||||
if (!empty($filename)) {
|
||||
$content = $filename;
|
||||
} elseif (!empty($caption)) {
|
||||
$content = $caption;
|
||||
} else {
|
||||
// Descripción por defecto según tipo
|
||||
$defaultLabels = [
|
||||
'image' => '[Imagen]',
|
||||
'video' => '[Video]',
|
||||
'audio' => '[Audio]',
|
||||
'document' => '[Documento]'
|
||||
];
|
||||
$content = $defaultLabels[$mediaType] ?? '[Media]';
|
||||
}
|
||||
|
||||
// Datos base para insertar
|
||||
// content: solo caption o nombre de archivo
|
||||
// content: nombre de archivo o caption (nunca JSON)
|
||||
// media_url: ID de WhatsApp si está disponible, sino la URL original
|
||||
$conversationData = [
|
||||
'user_id' => $user['id'],
|
||||
'message_id' => $sentMessageId,
|
||||
'direction' => 'outgoing',
|
||||
'message_type' => $mediaType,
|
||||
'content' => $caption ?: ($filename ?: ''),
|
||||
'content' => $content,
|
||||
'media_url' => isset($uploaded_media_id) ? $uploaded_media_id : $mediaUrl,
|
||||
'whatsapp_media_id' => $uploaded_media_id ?? null,
|
||||
'status' => 'sent',
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// Intentar añadir filename solo si la columna existe
|
||||
// TODO: Ejecutar migración add_filename_to_conversations.sql
|
||||
// ALTER TABLE conversations ADD COLUMN filename VARCHAR(255) NULL AFTER media_url;
|
||||
|
||||
error_log("send_media_message.php - Guardando en BD: " . json_encode($conversationData));
|
||||
// Log detallado para debugging
|
||||
error_log("send_media_message.php - Guardando mensaje multimedia:");
|
||||
error_log(" - content: " . $content);
|
||||
error_log(" - media_url: " . ($conversationData['media_url'] ?? 'null'));
|
||||
error_log(" - whatsapp_media_id: " . ($conversationData['whatsapp_media_id'] ?? 'null'));
|
||||
error_log(" - media_type: " . $mediaType);
|
||||
smm_log("Saving to DB - content: {$content}, media_url: " . ($conversationData['media_url'] ?? 'null'));
|
||||
|
||||
try {
|
||||
$db->insert('conversations', $conversationData);
|
||||
|
||||
@@ -16,3 +16,36 @@
|
||||
[2026-01-21 14:34:33] Media ID obtained: 2656357121414699
|
||||
[2026-01-21 14:34:34] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNkEyNzVBODA0MEQ3RTI3M0I0AA=="}]}
|
||||
[2026-01-21 14:34:34] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNkEyNzVBODA0MEQ3RTI3M0I0AA=="}]}
|
||||
[2026-01-27 11:22:17] Raw input: {"recipient":"573168950803","media_url":"http://localhost:8000/uploads/media_6978e6305ed3f9.57471688.png","media_type":"image","caption":null,"filename":"image.png"}
|
||||
[2026-01-27 11:22:17] Input decoded: {"recipient":"573168950803","media_url":"http:\/\/localhost:8000\/uploads\/media_6978e6305ed3f9.57471688.png","media_type":"image","caption":null,"filename":"image.png"}
|
||||
[2026-01-27 11:22:19] Upload result: {"id":"1521015225642113"}
|
||||
[2026-01-27 11:22:19] Media ID obtained: 1521015225642113
|
||||
[2026-01-27 11:22:20] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNkM1NzU4MjA1NkFCRDc5QjI4AA=="}]}
|
||||
[2026-01-27 11:22:20] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNkM1NzU4MjA1NkFCRDc5QjI4AA=="}]}
|
||||
[2026-01-27 11:26:42] Raw input: {"recipient":"573168950803","media_url":"http://localhost:8000/uploads/media_6978e7416984c9.82102744.png","media_type":"image","caption":null,"filename":"image.png"}
|
||||
[2026-01-27 11:26:42] Input decoded: {"recipient":"573168950803","media_url":"http:\/\/localhost:8000\/uploads\/media_6978e7416984c9.82102744.png","media_type":"image","caption":null,"filename":"image.png"}
|
||||
[2026-01-27 11:26:44] Upload result: {"id":"2345203359256908"}
|
||||
[2026-01-27 11:26:44] Media ID obtained: 2345203359256908
|
||||
[2026-01-27 11:26:45] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSOUNCMzU3QTZDQjUxNjIwN0UyAA=="}]}
|
||||
[2026-01-27 11:26:45] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSOUNCMzU3QTZDQjUxNjIwN0UyAA=="}]}
|
||||
[2026-01-27 14:10:55] Raw input: {"recipient":"573168950803","media_url":"http://localhost:8000/uploads/media_69790db0a11ab5.67639229.jpg","media_type":"image","caption":null,"filename":"media-url.jpg"}
|
||||
[2026-01-27 14:10:55] Input decoded: {"recipient":"573168950803","media_url":"http:\/\/localhost:8000\/uploads\/media_69790db0a11ab5.67639229.jpg","media_type":"image","caption":null,"filename":"media-url.jpg"}
|
||||
[2026-01-27 14:10:56] Upload result: {"id":"1222009752659731"}
|
||||
[2026-01-27 14:10:56] Media ID obtained: 1222009752659731
|
||||
[2026-01-27 14:10:57] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSN0U4MjRBRDRBQTYwQzBFRkRFAA=="}]}
|
||||
[2026-01-27 14:10:57] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSN0U4MjRBRDRBQTYwQzBFRkRFAA=="}]}
|
||||
[2026-01-27 14:10:57] Saving to DB - content: media-url.jpg, media_url: 1222009752659731
|
||||
[2026-01-27 14:41:16] Raw input: {"recipient":"573168950803","media_url":"http://localhost:8000/uploads/media_697914b0af8943.61710113.jpg","media_type":"image","caption":null,"filename":"media-url.jpg"}
|
||||
[2026-01-27 14:41:16] Input decoded: {"recipient":"573168950803","media_url":"http:\/\/localhost:8000\/uploads\/media_697914b0af8943.61710113.jpg","media_type":"image","caption":null,"filename":"media-url.jpg"}
|
||||
[2026-01-27 14:41:17] Upload result: {"id":"1974106793522186"}
|
||||
[2026-01-27 14:41:17] Media ID obtained: 1974106793522186
|
||||
[2026-01-27 14:41:18] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSQjg4NTk3RDlBMjc0QzNERUFCAA=="}]}
|
||||
[2026-01-27 14:41:18] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSQjg4NTk3RDlBMjc0QzNERUFCAA=="}]}
|
||||
[2026-01-27 14:41:18] Saving to DB - content: media-url.jpg, media_url: 1974106793522186
|
||||
[2026-01-27 14:48:37] Raw input: {"recipient":"573168950803","media_url":"http://localhost:8000/uploads/media_697916524ccef8.19324180.jpg","media_type":"image","caption":null,"filename":"media-url.jpg"}
|
||||
[2026-01-27 14:48:37] Input decoded: {"recipient":"573168950803","media_url":"http:\/\/localhost:8000\/uploads\/media_697916524ccef8.19324180.jpg","media_type":"image","caption":null,"filename":"media-url.jpg"}
|
||||
[2026-01-27 14:48:38] Upload result: {"id":"3817400915062613"}
|
||||
[2026-01-27 14:48:38] Media ID obtained: 3817400915062613
|
||||
[2026-01-27 14:48:39] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSRTJDMEIwNUExMkMwRkFDNUZFAA=="}]}
|
||||
[2026-01-27 14:48:39] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSRTJDMEIwNUExMkMwRkFDNUZFAA=="}]}
|
||||
[2026-01-27 14:48:40] Saving to DB - content: media-url.jpg, media_url: 3817400915062613
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
/**
|
||||
* Server-Sent Events (SSE) - Push de eventos en tiempo real
|
||||
* Este endpoint mantiene una conexión abierta y envía eventos cuando ocurren cambios
|
||||
* Fecha: 27 de enero de 2026
|
||||
*/
|
||||
|
||||
require_once '../config/config.php';
|
||||
|
||||
// Configurar headers para SSE primero (antes de cualquier output)
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no'); // Nginx
|
||||
|
||||
// Deshabilitar buffer de salida
|
||||
if (ob_get_level()) ob_end_clean();
|
||||
@ini_set('output_buffering', 'off');
|
||||
@ini_set('zlib.output_compression', 'off');
|
||||
|
||||
// Función para enviar un evento SSE
|
||||
function sendSSEEvent($event, $data, $id = null) {
|
||||
if ($id !== null) {
|
||||
echo "id: {$id}\n";
|
||||
}
|
||||
echo "event: {$event}\n";
|
||||
echo "data: " . json_encode($data) . "\n\n";
|
||||
|
||||
if (ob_get_level()) ob_flush();
|
||||
flush();
|
||||
}
|
||||
|
||||
// Función para leer eventos pendientes de un archivo temporal
|
||||
function readPendingEvents($userId) {
|
||||
$eventsFile = __DIR__ . "/../uploads/events_{$userId}.json";
|
||||
|
||||
if (!file_exists($eventsFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = @file_get_contents($eventsFile);
|
||||
if (!$content) return [];
|
||||
|
||||
$events = json_decode($content, true);
|
||||
if (!is_array($events)) return [];
|
||||
|
||||
// Limpiar archivo después de leer
|
||||
@file_put_contents($eventsFile, json_encode([]));
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
try {
|
||||
// Verificar autenticación (sesión o token)
|
||||
$userId = null;
|
||||
$authenticated = false;
|
||||
|
||||
// Opción 1: Sesión PHP
|
||||
if (isset($_SESSION['user']['id'])) {
|
||||
$userId = $_SESSION['user']['id'];
|
||||
$authenticated = true;
|
||||
}
|
||||
|
||||
// Opción 2: Token en query string (para EventSource sin credenciales)
|
||||
$token = $_GET['token'] ?? null;
|
||||
if (!$authenticated && $token) {
|
||||
// Validar token simple (en producción usar JWT o token en BD)
|
||||
if ($token === 'demo_token' || strlen($token) > 10) {
|
||||
// Modo autenticado con token
|
||||
$userId = 'token_' . substr(md5($token), 0, 8);
|
||||
$authenticated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Si aún no está autenticado, usar modo global
|
||||
if (!$authenticated) {
|
||||
// Modo global: recibir eventos broadcast a todos
|
||||
$userId = 'global';
|
||||
error_log('SSE: Conexión en modo global (sin autenticación específica)');
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Enviar evento inicial de conexión
|
||||
error_log("SSE: Nueva conexión establecida (userId: {$userId})");
|
||||
sendSSEEvent('connected', [
|
||||
'timestamp' => time(),
|
||||
'user_id' => $userId,
|
||||
'mode' => $authenticated ? 'authenticated' : 'global'
|
||||
]);
|
||||
|
||||
// Obtener último timestamp conocido por el cliente
|
||||
$lastEventId = $_SERVER['HTTP_LAST_EVENT_ID'] ?? null;
|
||||
$lastCheck = time();
|
||||
$connectionStart = time();
|
||||
$maxConnectionTime = 300; // 5 minutos máximo
|
||||
|
||||
// Loop principal - revisar eventos cada 2 segundos
|
||||
while (true) {
|
||||
// Verificar tiempo máximo de conexión (evitar conexiones eternas)
|
||||
if (time() - $connectionStart > $maxConnectionTime) {
|
||||
error_log("SSE: Conexión alcanzó tiempo máximo ({$maxConnectionTime}s), cerrando...");
|
||||
sendSSEEvent('timeout', ['message' => 'Conexión reiniciándose', 'reconnect' => true]);
|
||||
break;
|
||||
}
|
||||
|
||||
// Verificar si la conexión sigue activa
|
||||
if (connection_aborted()) {
|
||||
error_log("SSE: Cliente desconectado (userId: {$userId})");
|
||||
break;
|
||||
}
|
||||
|
||||
// Leer eventos pendientes del archivo temporal
|
||||
$pendingEvents = readPendingEvents($userId);
|
||||
|
||||
foreach ($pendingEvents as $event) {
|
||||
sendSSEEvent(
|
||||
$event['type'] ?? 'message',
|
||||
$event['data'] ?? [],
|
||||
$event['id'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
// Revisar nuevos mensajes en BD cada 3 segundos
|
||||
if (time() - $lastCheck >= 3) {
|
||||
// Verificar si hay nuevas conversaciones desde la última revisión
|
||||
$recentConversations = $db->fetchAll(
|
||||
"SELECT DISTINCT c.user_id, u.phone_number, u.name,
|
||||
MAX(c.created_at) as last_message_time,
|
||||
COUNT(*) as message_count
|
||||
FROM conversations c
|
||||
INNER JOIN users u ON c.user_id = u.id
|
||||
WHERE c.created_at >= DATE_SUB(NOW(), INTERVAL 10 SECOND)
|
||||
GROUP BY c.user_id
|
||||
ORDER BY last_message_time DESC
|
||||
LIMIT 5"
|
||||
);
|
||||
|
||||
foreach ($recentConversations as $conv) {
|
||||
sendSSEEvent('new_conversation', [
|
||||
'user_id' => $conv['user_id'],
|
||||
'phone_number' => $conv['phone_number'],
|
||||
'name' => $conv['name'],
|
||||
'message_count' => $conv['message_count'],
|
||||
'timestamp' => $conv['last_message_time']
|
||||
]);
|
||||
}
|
||||
|
||||
// Verificar notificaciones no leídas
|
||||
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);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log('SSE: Error leyendo notificaciones: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$lastCheck = time();
|
||||
}
|
||||
|
||||
// Enviar heartbeat cada 30 segundos para mantener conexión viva
|
||||
if (time() % 30 === 0) {
|
||||
sendSSEEvent('heartbeat', ['timestamp' => time()]);
|
||||
}
|
||||
|
||||
// Esperar 2 segundos antes de la siguiente verificación
|
||||
sleep(2);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("SSE Error: " . $e->getMessage());
|
||||
error_log("SSE Error trace: " . $e->getTraceAsString());
|
||||
sendSSEEvent('error', [
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode()
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// Test simple de SSE
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
if (ob_get_level()) ob_end_clean();
|
||||
@ini_set('output_buffering', 'off');
|
||||
@ini_set('zlib.output_compression', 'off');
|
||||
|
||||
echo "event: connected\n";
|
||||
echo "data: " . json_encode(['test' => true, 'timestamp' => time()]) . "\n\n";
|
||||
|
||||
if (ob_get_level()) ob_flush();
|
||||
flush();
|
||||
|
||||
echo ": heartbeat\n\n";
|
||||
flush();
|
||||
|
||||
// Log para debug
|
||||
error_log('SSE Test: enviado evento connected');
|
||||
@@ -305,6 +305,16 @@ class WhatsAppWebhook {
|
||||
error_log('Failed to create notification: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Empujar evento SSE en tiempo real
|
||||
$this->pushSSEEvent('new_message', [
|
||||
'user_id' => $user['id'],
|
||||
'phone_number' => $user['phone_number'],
|
||||
'name' => $user['name'] ?? $from,
|
||||
'message' => substr($messageText, 0, 250),
|
||||
'message_type' => $messageType,
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
// Procesar con bot (protección contra excepciones externas)
|
||||
try {
|
||||
$this->botService->processMessage($user, $messageText, $messageType);
|
||||
@@ -398,6 +408,44 @@ class WhatsAppWebhook {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Empujar evento a través de SSE (Server-Sent Events)
|
||||
* Notifica a todos los operadores conectados sobre nuevos eventos
|
||||
*/
|
||||
private function pushSSEEvent($eventType, $data) {
|
||||
try {
|
||||
// Llamar al endpoint push_event interno
|
||||
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
|
||||
|
||||
$payload = json_encode([
|
||||
'event_type' => $eventType,
|
||||
'data' => $data,
|
||||
'target_user_id' => 'all' // Notificar a todos los operadores
|
||||
]);
|
||||
|
||||
// Hacer llamada asíncrona (fire and forget)
|
||||
$ch = curl_init($pushUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT_MS => 500, // Timeout corto
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'X-Push-Token: internal_push_secret_2026'
|
||||
]
|
||||
]);
|
||||
|
||||
// Ejecutar sin bloquear
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
} catch (Exception $e) {
|
||||
// No fallar si el push falla
|
||||
error_log('pushSSEEvent failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesar un payload (array) recibido manualmente (replay)
|
||||
* Útil para reproducir entradas desde la UI o scripts.
|
||||
|
||||
Reference in New Issue
Block a user