252 lines
9.9 KiB
PHP
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()
|
|
]);
|
|
}
|