Files
whatsapp/api/sse_events.php
T
2026-01-29 11:40:04 -05:00

252 lines
9.9 KiB
PHP

<?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)');
}
// IMPORTANTE: Enviar evento connected ANTES de hacer cualquier otra cosa
error_log("SSE: Enviando evento connected (userId: {$userId})");
sendSSEEvent('connected', [
'timestamp' => time(),
'user_id' => $userId,
'mode' => $authenticated ? 'authenticated' : 'global',
'status' => 'ready'
]);
// Ahora sí, conectar a BD
$db = Database::getInstance();
// Obtener último timestamp conocido por el cliente
$lastEventId = $_SERVER['HTTP_LAST_EVENT_ID'] ?? null;
$lastCheck = time();
$connectionStart = time();
$maxConnectionTime = 1800; // 30 minutos máximo (aumentado para evitar desconexiones frecuentes)
// 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) {
// Mantener track de mensajes ya enviados para evitar duplicados
static $lastMessageId = 0;
static $lastConversationCheck = null;
if ($lastConversationCheck === null) {
// Primera vez: obtener el ID más alto y timestamp actual de la BD, sin enviar nada
$maxMsg = $db->fetch("SELECT MAX(id) as max_id FROM conversations");
$lastMessageId = $maxMsg['max_id'] ?? 0;
// Usar el timestamp de la BD para evitar problemas de zona horaria
$dbTime = $db->fetch("SELECT NOW() as now");
$lastConversationCheck = $dbTime['now'] ?? date('Y-m-d H:i:s');
} else {
// 1. Verificar mensajes NUEVOS (solo los que tienen ID mayor al último visto)
$recentMessages = $db->fetchAll(
"SELECT c.*, u.phone_number, u.name as user_name
FROM conversations c
INNER JOIN users u ON c.user_id = u.id
WHERE c.id > ?
AND c.direction = 'incoming'
ORDER BY c.id ASC
LIMIT 10",
[$lastMessageId]
);
foreach ($recentMessages as $msg) {
sendSSEEvent('new_message', [
'message_id' => $msg['id'],
'user_id' => $msg['user_id'],
'phone_number' => $msg['phone_number'],
'user_name' => $msg['user_name'],
'content' => $msg['message_content'],
'message_type' => $msg['message_type'] ?? 'text',
'direction' => $msg['direction'],
'created_at' => $msg['created_at'],
'timestamp' => time()
]);
// Actualizar último ID visto
if ($msg['id'] > $lastMessageId) {
$lastMessageId = $msg['id'];
}
}
// 2. 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 > ?
GROUP BY c.user_id
ORDER BY last_message_time DESC
LIMIT 5",
[$lastConversationCheck]
);
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']
]);
// Actualizar timestamp
if ($conv['last_message_time'] > $lastConversationCheck) {
$lastConversationCheck = $conv['last_message_time'];
}
}
} // Fin del else (primera inicialización vs verificaciones posteriores)
// Verificar notificaciones NUEVAS (solo las creadas desde la última verificación)
// NO enviar notificaciones guardadas - solo las que llegan en tiempo real
try {
static $lastNotificationCheck = null;
if ($lastNotificationCheck === null) {
// Primera vez: usar el timestamp de la BD, NO enviar nada
$dbTimeNotif = $db->fetch("SELECT NOW() as now");
$lastNotificationCheck = $dbTimeNotif['now'] ?? date('Y-m-d H:i:s');
} else {
// Verificaciones posteriores: solo notificaciones NUEVAS
$notifications = $db->fetchAll(
"SELECT id, user_id, type, message, data, is_read, created_at
FROM notifications
WHERE created_at > ?
ORDER BY created_at ASC
LIMIT 10",
[$lastNotificationCheck]
);
if ($notifications && count($notifications) > 0) {
foreach ($notifications as $notification) {
sendSSEEvent('notification', $notification);
// Actualizar timestamp para evitar duplicados
if ($notification['created_at'] > $lastNotificationCheck) {
$lastNotificationCheck = $notification['created_at'];
}
}
}
}
} 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()
]);
}