up
This commit is contained in:
@@ -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()
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user