'user_id es requerido']); exit; } // Paginación: limit y before (timestamp) - cargamos N mensajes anteriores a 'before' $limit = isset($_GET['limit']) ? intval($_GET['limit']) : 50; $limit = max(1, min(200, $limit)); $before = !empty($_GET['before']) ? $_GET['before'] : null; // expect timestamp string // We also support a 'since' parameter to fetch only messages strictly newer than a timestamp $since = !empty($_GET['since']) ? $_GET['since'] : null; // Validar formato de 'before' si se proporcionó (evitar SQL/parse errors por input malformado) if (!is_null($before)) { // Aceptar formato 'YYYY-MM-DD HH:MM:SS' (espacio entre fecha y hora) if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $before)) { http_response_code(400); echo json_encode(['success' => false, 'error' => 'before timestamp inválido']); exit; } } // Validar 'since' si se pasó if (!is_null($since)) { if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $since)) { http_response_code(400); echo json_encode(['success' => false, 'error' => 'since timestamp inválido']); exit; } } $db = Database::getInstance(); // Construir consulta: by default paginamos hacia atrás (before) y ordenamos DESC // Pero cuando se solicita 'since' queremos sólo mensajes más nuevos => orden ASC (para retornar cronológicamente) $sql = "SELECT c.id as id, c.user_id as user_id, c.content as content, c.message_id as message_id, c.media_url, c.local_file, c.filename, c.mime_type, u.phone_number as user_phone, c.direction as direction, c.message_type as message_type, c.status as status, c.created_at as created_at, COALESCE(c.is_read, 0) as is_read, c.reply_to_message_id as reply_to_message_id, c.reaction_emoji as reaction_emoji, c.reaction_to_message_id as reaction_to_message_id FROM conversations c LEFT JOIN users u ON c.user_id = u.id WHERE c.user_id = :user_id"; // Soporte para paginación estable: before (timestamp) y before_id (id del mensaje más antiguo del bloque) $beforeId = isset($_GET['before_id']) ? intval($_GET['before_id']) : null; $params = ['user_id' => $userId]; if (!is_null($since)) { // If 'since' is provided, return only messages strictly newer than 'since' ordered ascending $sql .= " AND c.created_at > :since"; $params['since'] = $since; $sql .= " ORDER BY c.created_at ASC, c.id ASC LIMIT " . $limit; } else { if ($before) { $sql .= " AND (c.created_at < :before_lt"; $params['before_lt'] = $before; if ($beforeId) { // Incluir mensajes con mismo timestamp pero id menor (paginación estable) // Usamos placeholder distinto para evitar duplicar el mismo nombre en la query $sql .= " OR (c.created_at = :before_eq AND c.id < :before_id)"; $params['before_eq'] = $before; $params['before_id'] = $beforeId; } $sql .= ")"; } $sql .= " ORDER BY c.created_at DESC, c.id DESC LIMIT " . $limit; } // Log params/consulta para diagnóstico si algo falla (no sensible) error_log('get_user_messages.php - params: ' . json_encode(['user_id' => $userId, 'limit' => $limit, 'before' => $before])); error_log('get_user_messages.php - executing SQL: ' . $sql . ' params: ' . json_encode($params)); $conversations = $db->fetchAll($sql, $params); // Si no hay mensajes, intentar fallback si se pidió paginación con 'before' if (empty($conversations) && $before) { error_log('get_user_messages.php - initial query returned empty, attempting fallback with inclusive <= for before param'); try { $sqlFallback = str_replace('c.created_at < :before', 'c.created_at <= :before', $sql); $conversationsFallback = $db->fetchAll($sqlFallback, $params); if (!empty($conversationsFallback)) { error_log('get_user_messages.php - fallback query returned ' . count($conversationsFallback) . ' rows'); $conversations = $conversationsFallback; } } catch (Exception $e) { error_log('get_user_messages.php - fallback query failed: ' . $e->getMessage()); } } // Si todavía no hay mensajes, devolver array vacío if (empty($conversations)) { echo json_encode(['success' => true, 'data' => [], 'has_more' => false], JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); exit; } // Si se solicitó 'since' ya devolvimos en orden ASC (cronológico). Si no, revertimos $sinceProvided = !is_null($since); if (!$sinceProvided) { $conversations = array_reverse($conversations); } // Recalcular usando el resultado final para decidir paginación $finalCount = is_array($conversations) ? count($conversations) : 0; $hasMore = ($finalCount > 0 && $finalCount == $limit) ? true : false; // Si no hay mensajes en conversations, intentar con conversaciones (compatibilidad antigua) if (empty($conversations)) { $conversations = $db->fetchAll( "SELECT id, user_id, message_text as content, direction, message_type, media_url, local_file, filename, mime_type, status, created_at FROM conversations WHERE user_id = :user_id ORDER BY created_at ASC", ['user_id' => $userId] ); } // Formatear fechas y limpiar datos $conversations = array_map(function($msg) { $external = (isset($msg['media_url']) && $msg['media_url']) ? (preg_match('#^https?://#i', $msg['media_url']) ? $msg['media_url'] : ("api/get_media.php?id=" . urlencode($msg['media_url']))) : null; $contentStr = $msg['content'] ?? ''; if (!$external && $contentStr) { $j = json_decode($contentStr, true); if (is_array($j)) { foreach (['image','document','audio','video'] as $k) { if (isset($j[$k]) && is_array($j[$k])) { if (!empty($j[$k]['link'])) { $external = $j[$k]['link']; break; } if (!empty($j[$k]['url'])) { $external = $j[$k]['url']; break; } if (!empty($j[$k]['id'])) { $external = "api/get_media.php?id=" . urlencode($j[$k]['id']); break; } } } if (!$external && !empty($j['link'])) $external = $j['link']; } } return [ 'id' => intval($msg['id']), 'user_id' => intval($msg['user_id']), 'content' => $msg['content'] ?? '', 'message_id' => $msg['message_id'] ?? null, 'media_url' => null, 'local_file' => $msg['local_file'] ?? null, 'media_url_external' => $external, 'filename' => $msg['filename'] ?? null, 'mime_type' => $msg['mime_type'] ?? null, 'user_phone' => $msg['user_phone'] ?? null, 'direction' => $msg['direction'] ?? 'incoming', 'message_type' => $msg['message_type'] ?? 'text', 'status' => $msg['status'] ?? 'sent', 'created_at' => $msg['created_at'], 'reply_to_message_id' => $msg['reply_to_message_id'] ?? null, 'reaction_emoji' => $msg['reaction_emoji'] ?? null, 'reaction_to_message_id' => $msg['reaction_to_message_id'] ?? null ]; }, $conversations); // earliest message timestamp (para paginación hacia atrás) $earliest = $conversations[0]['created_at'] ?? null; $earliestId = $conversations[0]['id'] ?? null; // Si usamos fallback y el earliest coincide o es >= al 'before' solicitado, // decrementamos 1 segundo para evitar que la próxima petición repita el mismo bloque $fallbackUsed = (isset($conversationsFallback) && !empty($conversationsFallback)); if ($fallbackUsed && $earliest && $before) { $earliest_ts = strtotime($earliest); $before_ts = strtotime($before); if ($earliest_ts !== false && $before_ts !== false && $earliest_ts >= $before_ts) { // restar 1 segundo $earliest = date('Y-m-d H:i:s', $earliest_ts - 1); error_log('get_user_messages.php - adjusted earliest to avoid duplicate pagination: ' . $earliest); } } // Recalculate final_count for debug $finalCount = is_array($conversations) ? count($conversations) : 0; // Añadir debug information cuando se solicita (solo con ?debug=1) $debugInfo = null; if (!empty($_GET['debug']) && $_GET['debug'] == '1') { $debugInfo = [ 'original_sql' => substr($sql, 0, 2000), 'params' => $params, 'final_count' => $finalCount, 'fallback_used' => $fallbackUsed, 'fallback_count' => $fallbackUsed ? count($conversationsFallback) : 0 ]; } $out = ['success' => true, 'data' => $conversations, 'has_more' => $hasMore, 'earliest' => $earliest, 'earliest_id' => $earliestId]; if ($debugInfo) $out['debug'] = $debugInfo; echo json_encode($out, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); } catch (Exception $e) { // Log detallado con stack trace para diagnóstico error_log("Error in get_user_messages.php: " . $e->getMessage() . "\n" . $e->getTraceAsString()); http_response_code(500); // Responder con detalle sólo si se pasa ?debug=1 (NO activar en producción) if (!empty($_GET['debug']) && $_GET['debug'] == '1') { echo json_encode([ 'error' => 'Error interno del servidor', 'details' => $e->getMessage(), 'trace' => explode("\n", $e->getTraceAsString()) ]); } else { echo json_encode(['error' => 'Error interno del servidor']); } } ?>