get_user_messages.php and get_recent_messages.php now exclude canal='turnero' so the main bot chat only shows bot messages even for users who also have turnero conversations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
259 lines
11 KiB
PHP
259 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* API - Obtener mensajes de un usuario específico
|
|
* Fecha: 4 de enero de 2026
|
|
*/
|
|
|
|
require_once '../config/config.php';
|
|
|
|
// Verificar autenticación
|
|
requireAuthentication();
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
|
header('Pragma: no-cache');
|
|
header('Expires: 0');
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: GET');
|
|
header('Access-Control-Allow-Headers: Content-Type');
|
|
|
|
try {
|
|
$userId = intval($_GET['user_id'] ?? 0);
|
|
|
|
if (!$userId) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => '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 AND (c.canal IS NULL OR c.canal = 'bot')";
|
|
|
|
// 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 AND (canal IS NULL OR canal = 'bot')
|
|
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']);
|
|
}
|
|
}
|
|
?>
|