up
This commit is contained in:
@@ -58,13 +58,17 @@ try {
|
||||
message_type,
|
||||
content,
|
||||
media_url,
|
||||
local_file,
|
||||
local_thumb,
|
||||
filename,
|
||||
mime_type,
|
||||
status,
|
||||
created_at
|
||||
FROM conversations
|
||||
WHERE user_id = ?
|
||||
AND (
|
||||
message_type = 'text'
|
||||
OR (message_type IN ('image', 'audio', 'video', 'document') AND media_url IS NOT NULL AND media_url != '')
|
||||
OR (message_type IN ('image', 'audio', 'video', 'document', 'sticker') AND (media_url IS NOT NULL AND media_url != '' OR local_file IS NOT NULL AND local_file != ''))
|
||||
)
|
||||
ORDER BY created_at ASC",
|
||||
[$user_id]
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* API - Obtener estadísticas de Rate Limiting de Facebook
|
||||
* Fecha: 6 de febrero de 2026
|
||||
*/
|
||||
|
||||
// Configurar manejo de errores
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
// Cargar config primero (ya incluye el autoloader)
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
// Verificar que la clase exista antes de usarla
|
||||
if (!class_exists('RateLimitMonitor')) {
|
||||
$rateLimitMonitorPath = __DIR__ . '/../services/RateLimitMonitor.php';
|
||||
if (file_exists($rateLimitMonitorPath)) {
|
||||
require_once $rateLimitMonitorPath;
|
||||
} else {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'RateLimitMonitor service not found',
|
||||
'path_checked' => $rateLimitMonitorPath
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Requerir autenticación
|
||||
requireAuthentication();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
try {
|
||||
$monitor = new RateLimitMonitor();
|
||||
|
||||
$action = $_GET['action'] ?? 'current';
|
||||
|
||||
switch ($action) {
|
||||
case 'current':
|
||||
// Obtener estadísticas actuales
|
||||
$stats = $monitor->getCurrentStats();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $stats,
|
||||
'timestamp' => time(),
|
||||
'datetime' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'history':
|
||||
// Obtener historial
|
||||
$type = $_GET['type'] ?? 'app';
|
||||
$limit = min(100, intval($_GET['limit'] ?? 20));
|
||||
|
||||
$history = $monitor->getUsageHistory($type, $limit);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $history,
|
||||
'type' => $type,
|
||||
'count' => count($history)
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'cleanup':
|
||||
// Ejecutar limpieza (solo admin)
|
||||
$monitor->cleanup();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Limpieza completada'
|
||||
]);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Acción no válida. Use: current, history, cleanup'
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
@@ -93,6 +93,7 @@ header('Content-Type: application/json; charset=utf-8');
|
||||
$mediaId = $_GET['id'] ?? $_POST['id'] ?? null;
|
||||
$providedUrl = $_GET['url'] ?? $_POST['url'] ?? null;
|
||||
$download = isset($_GET['download']) && $_GET['download'] === '1';
|
||||
$debug = isset($_GET['debug']) && $_GET['debug'] === '1';
|
||||
|
||||
if (!$mediaId && !$providedUrl) {
|
||||
http_response_code(400);
|
||||
@@ -239,6 +240,112 @@ if ($providedUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
// === MODO DEBUG: muestra diagnóstico en lugar de redirigir/descargar ===
|
||||
if ($debug) {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
$tokenRaw = getConfigFromDB('whatsapp_token', '');
|
||||
$tokenPreview = $tokenRaw ? substr($tokenRaw, 0, 15) . '...' . substr($tokenRaw, -10) : '(vacío)';
|
||||
$apiUrlConfig = rtrim(getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'), '/');
|
||||
|
||||
echo '<html><head><title>Media URL Debug</title>';
|
||||
echo '<style>body{font-family:monospace;padding:20px;background:#1e1e1e;color:#d4d4d4}';
|
||||
echo '.ok{color:#4ec9b0}.err{color:#f44747}.warn{color:#dcdcaa}';
|
||||
echo 'pre{background:#2d2d2d;padding:15px;border-radius:5px;overflow-x:auto;white-space:pre-wrap}';
|
||||
echo 'h2{color:#569cd6}h3{color:#9cdcfe}</style></head><body>';
|
||||
echo '<h2>🔍 Diagnóstico media-url.php</h2>';
|
||||
echo '<h3>Parámetros de entrada</h3><pre>';
|
||||
echo "Media ID: " . htmlspecialchars($mediaId ?? '(no proporcionado)') . "\n";
|
||||
echo "URL proporcionada: " . htmlspecialchars($providedUrl ?? '(no proporcionada)') . "\n";
|
||||
echo "download: " . ($download ? '1' : '0') . "\n";
|
||||
echo '</pre>';
|
||||
|
||||
echo '<h3>Configuración</h3><pre>';
|
||||
echo "Token: " . htmlspecialchars($tokenPreview) . "\n";
|
||||
echo "API URL: " . htmlspecialchars($apiUrlConfig) . "\n";
|
||||
|
||||
if (empty($tokenRaw)) {
|
||||
echo '<span class="err">⛔ ERROR: Token de WhatsApp vacío o no configurado</span>' . "\n";
|
||||
echo 'Solución: Configurar en /configurar_whatsapp.php o en system_config (key: whatsapp_token)' . "\n";
|
||||
} else {
|
||||
echo '<span class="ok">✅ Token presente</span>' . "\n";
|
||||
}
|
||||
echo '</pre>';
|
||||
|
||||
echo '<h3>Resultado Graph API</h3><pre>';
|
||||
if ($mediaUrl) {
|
||||
echo '<span class="ok">✅ URL resuelta:</span> ' . htmlspecialchars($mediaUrl) . "\n";
|
||||
echo "Resuelto desde Graph: " . ($resolvedFromGraph ? 'Sí' : 'No') . "\n";
|
||||
|
||||
// Validar que la URL sea accesible desde el servidor
|
||||
echo "\n<span class=\"warn\">Verificando accesibilidad de la URL...</span>\n";
|
||||
$testCh = curl_init();
|
||||
$testHeaders = ['User-Agent: WhatsAppMediaProxy/1.0', 'Accept: */*'];
|
||||
if ($tokenRaw) $testHeaders[] = 'Authorization: Bearer ' . $tokenRaw;
|
||||
curl_setopt_array($testCh, [
|
||||
CURLOPT_URL => $mediaUrl,
|
||||
CURLOPT_NOBODY => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => $testHeaders,
|
||||
]);
|
||||
curl_exec($testCh);
|
||||
$testInfo = curl_getinfo($testCh);
|
||||
$testErr = curl_error($testCh);
|
||||
curl_close($testCh);
|
||||
|
||||
echo "HTTP Status: " . ($testInfo['http_code'] ?? 'N/A') . "\n";
|
||||
echo "Content-Type: " . ($testInfo['content_type'] ?? 'N/A') . "\n";
|
||||
echo "Content-Length: " . ($testInfo['download_content_length'] ?? 'N/A') . " bytes\n";
|
||||
|
||||
if ($testErr) {
|
||||
echo '<span class="err">⛔ Error cURL: ' . htmlspecialchars($testErr) . '</span>' . "\n";
|
||||
}
|
||||
|
||||
if (($testInfo['http_code'] ?? 0) >= 400) {
|
||||
echo '<span class="err">⛔ La URL del media NO es accesible (HTTP ' . $testInfo['http_code'] . ')</span>' . "\n";
|
||||
if (($testInfo['http_code'] ?? 0) == 404) {
|
||||
echo '<span class="warn">⚠️ El media probablemente expiró. Los archivos multimedia de WhatsApp expiran después de ~30 días.</span>' . "\n";
|
||||
} elseif (($testInfo['http_code'] ?? 0) == 401 || ($testInfo['http_code'] ?? 0) == 403) {
|
||||
echo '<span class="warn">⚠️ Error de autenticación. Verificar que el token sea válido y tenga permisos.</span>' . "\n";
|
||||
}
|
||||
} else {
|
||||
echo '<span class="ok">✅ URL accesible correctamente</span>' . "\n";
|
||||
}
|
||||
} else {
|
||||
echo '<span class="err">⛔ No se pudo resolver la URL del media</span>' . "\n";
|
||||
if (!empty($decoded)) {
|
||||
echo "Respuesta de Graph API:\n";
|
||||
echo htmlspecialchars(json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) . "\n";
|
||||
|
||||
// Diagnóstico específico de errores comunes
|
||||
$errCode = $decoded['error']['code'] ?? null;
|
||||
$errSubcode = $decoded['error']['error_subcode'] ?? null;
|
||||
$errMsg = $decoded['error']['message'] ?? '';
|
||||
|
||||
if ($errCode == 190) {
|
||||
echo '<span class="err">⛔ Token expirado o inválido (code 190)</span>' . "\n";
|
||||
echo '<span class="warn">Solución: Renovar el token en /configurar_whatsapp.php</span>' . "\n";
|
||||
} elseif ($errCode == 100 && $errSubcode == 33) {
|
||||
echo '<span class="err">⛔ Media ID no encontrado o expirado</span>' . "\n";
|
||||
echo '<span class="warn">Los archivos multimedia de WhatsApp Business API expiran aproximadamente 30 días después de ser recibidos.</span>' . "\n";
|
||||
} elseif ($errCode == 100) {
|
||||
echo '<span class="err">⛔ Parámetro inválido (code 100)</span>' . "\n";
|
||||
echo '<span class="warn">El media ID puede ser incorrecto: ' . htmlspecialchars($mediaId ?? '') . '</span>' . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
echo '</pre>';
|
||||
|
||||
echo '<h3>Respuesta raw de Graph API ($decoded)</h3><pre>';
|
||||
echo htmlspecialchars(json_encode($decoded ?? null, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
echo '</pre>';
|
||||
|
||||
echo '<p style="margin-top:20px;color:#888">Generado: ' . date('Y-m-d H:i:s T') . '</p>';
|
||||
echo '</body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Soporte para archivos locales (local=uploads/media/...)
|
||||
$local = $_GET['local'] ?? null;
|
||||
if ($local) {
|
||||
|
||||
@@ -414,3 +414,15 @@
|
||||
[2026-02-03 20:31:47] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||
[2026-02-03 20:31:48] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770169008&hash=ARmSF2NK4GqmW53yaJd3NCLYub_AGbau7EHf5WOOETVxbw
|
||||
[2026-02-03 20:31:48] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770169008&hash=ARlAZuDk9UsmFsMvn4qiAOOAk45RwETVe04h9vkJdDruXg
|
||||
[2026-02-20 09:02:34] Request: GET /api/version/media-url.php?id=1307493841213652&debug=1 GET:{"id":"1307493841213652","debug":"1"} POST:[]
|
||||
[2026-02-20 09:02:34] Graph API request to https://graph.facebook.com/v22.0/1307493841213652
|
||||
[2026-02-20 09:03:08] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||
[2026-02-20 09:03:08] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||
[2026-02-20 09:03:10] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||
[2026-02-20 09:03:10] Graph API fetch failed for mid 2371342336638440 - http: 400 - err: - resp: {"error":{"message":"Unsupported get request. Object with ID '2371342336638440' does not exist, cannot be loaded due to missing permissions, or does not support this operation. Please read the Graph API documentation at https:\/\/developers.facebook.com\/docs\/graph-api","type":"GraphMethodException","code":100,"error_subcode":33,"fbtrace_id":"AdhQHtrZK9AxINPUaZhSBxG"}}
|
||||
[2026-02-20 09:03:10] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA
|
||||
[2026-02-20 09:05:33] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
|
||||
[2026-02-20 09:05:33] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
|
||||
[2026-02-20 09:05:37] Request: GET /api/version/media-url.php?url=https%3A%2F%2Flookaside.fbsbx.com%2Fwhatsapp_business%2Fattachments%2F%3Fmid%3D2371342336638440%26source%3Dwebhook%26ext%3D1769575796%26hash%3DARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA GET:{"url":"https:\/\/lookaside.fbsbx.com\/whatsapp_business\/attachments\/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA"} POST:[]
|
||||
[2026-02-20 09:05:37] Graph API fetch failed for mid 2371342336638440 - http: 400 - err: - resp: {"error":{"message":"Unsupported get request. Object with ID '2371342336638440' does not exist, cannot be loaded due to missing permissions, or does not support this operation. Please read the Graph API documentation at https:\/\/developers.facebook.com\/docs\/graph-api","type":"GraphMethodException","code":100,"error_subcode":33,"fbtrace_id":"Ayy1xE4nFVa4ae3yKVwTCJo"}}
|
||||
[2026-02-20 09:05:37] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=webhook&ext=1769575796&hash=ARndVKdw1p0CCx3_DpTblR2aOVkb_W5mbTqGFPSOK77cjA
|
||||
|
||||
@@ -10,3 +10,11 @@
|
||||
[21-Jan-2026 21:55:38 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 522
|
||||
[21-Jan-2026 21:59:30 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 203
|
||||
[21-Jan-2026 21:59:30 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 522
|
||||
[20-Feb-2026 09:02:34 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 220
|
||||
[20-Feb-2026 09:02:35 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 295
|
||||
[20-Feb-2026 09:03:09 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 220
|
||||
[20-Feb-2026 09:03:10 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 159
|
||||
[20-Feb-2026 09:03:10 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 645
|
||||
[20-Feb-2026 09:05:33 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 220
|
||||
[20-Feb-2026 09:05:37 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 159
|
||||
[20-Feb-2026 09:05:37 America/Bogota] PHP Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in /Users/lizandro/Documents/GitHub/whatsapp/api/version/media-url.php on line 645
|
||||
|
||||
+55
-1
@@ -209,12 +209,54 @@ class WhatsAppWebhook {
|
||||
$mimeType = $message['document']['mime_type'] ?? null;
|
||||
$filename = $message['document']['filename'] ?? null;
|
||||
|
||||
// Do NOT download documents automatically; keep media id so browser will redirect and download
|
||||
}
|
||||
|
||||
// Log debug (temporal) para verificar cómo llegan media_url/filename
|
||||
error_log(sprintf('[webhook] messageId=%s type=%s mediaUrl=%s filename=%s mime=%s', $messageId, $messageType, $mediaUrl ?? 'NULL', $filename ?? 'NULL', $mimeType ?? 'NULL'));
|
||||
|
||||
// ── Descargar media localmente de inmediato ──
|
||||
// Si hay un media ID (numérico) o URL de Graph, descargarlo y guardarlo en uploads/media/
|
||||
$localFile = null;
|
||||
$localThumb = null;
|
||||
if (!empty($mediaUrl) && in_array($messageType, ['image', 'audio', 'video', 'document', 'sticker'])) {
|
||||
try {
|
||||
$mediaService = new MediaService();
|
||||
$subdir = date('Y/m'); // Organizar por año/mes
|
||||
|
||||
// Determinar si es un media ID (numérico) o una URL directa
|
||||
if (preg_match('/^\d+$/', $mediaUrl)) {
|
||||
// Es un media ID de WhatsApp → resolver vía Graph API y descargar
|
||||
$stored = $mediaService->fetchAndStoreFromGraph($mediaUrl, $subdir);
|
||||
} else {
|
||||
// Es una URL directa → descargar directamente
|
||||
$stored = $mediaService->fetchAndStoreFromUrl($mediaUrl, $subdir);
|
||||
}
|
||||
|
||||
if (!empty($stored['local_file'])) {
|
||||
$localFile = $stored['local_file'];
|
||||
error_log("[webhook] Media descargado localmente: {$localFile}");
|
||||
}
|
||||
if (!empty($stored['local_thumb'])) {
|
||||
$localThumb = $stored['local_thumb'];
|
||||
}
|
||||
} catch (Exception $mediaEx) {
|
||||
error_log("[webhook] Error descargando media (se encolará para retry): " . $mediaEx->getMessage());
|
||||
// Encolar en media_queue para reintento posterior
|
||||
try {
|
||||
$this->db->insert('media_queue', [
|
||||
'media_id' => preg_match('/^\d+$/', $mediaUrl) ? $mediaUrl : '',
|
||||
'media_url' => $mediaUrl,
|
||||
'subdir' => date('Y/m'),
|
||||
'status' => 'pending',
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
error_log("[webhook] Media encolado en media_queue para retry");
|
||||
} catch (Exception $qEx) {
|
||||
error_log("[webhook] Error encolando media: " . $qEx->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar mensaje en base de datos
|
||||
$saveData = [
|
||||
'user_id' => $user['id'],
|
||||
@@ -291,6 +333,18 @@ class WhatsAppWebhook {
|
||||
// Guardar mensaje y conservar el id de la conversación para posibles encolamientos
|
||||
$conversationId = $this->saveMessage($saveData);
|
||||
|
||||
// Si el media se encoló (no se pudo descargar), actualizar media_queue con el conversation_id
|
||||
if (!empty($mediaUrl) && empty($localFile) && in_array($messageType, ['image', 'audio', 'video', 'document', 'sticker'])) {
|
||||
try {
|
||||
$this->db->query(
|
||||
"UPDATE media_queue SET conversation_id = :cid WHERE media_id = :mid AND conversation_id IS NULL ORDER BY id DESC LIMIT 1",
|
||||
['cid' => $conversationId, 'mid' => (preg_match('/^\d+$/', $mediaUrl) ? $mediaUrl : '')]
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
error_log("[webhook] Error actualizando media_queue con conversation_id: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Crear notificación para UI (nuevo mensaje entrante)
|
||||
try {
|
||||
$this->db->insert('notifications', [
|
||||
|
||||
@@ -197,6 +197,107 @@ try {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rate Limiting Monitor -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5><i class="fas fa-tachometer-alt"></i> Monitoreo de Rate Limiting - Facebook API</h5>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="refreshRateLimitStats()">
|
||||
<i class="fas fa-sync-alt"></i> Actualizar
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Status General -->
|
||||
<div class="alert" id="rate-limit-status" role="alert">
|
||||
<i class="fas fa-spinner fa-spin"></i> Cargando estadísticas...
|
||||
</div>
|
||||
|
||||
<!-- Métricas de Facebook -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="text-muted mb-2">Call Count</h6>
|
||||
<div class="progress mb-2" style="height: 25px;">
|
||||
<div id="fb-call-count-bar" class="progress-bar" role="progressbar" style="width: 0%">0%</div>
|
||||
</div>
|
||||
<small class="text-muted">Llamadas a la API</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="text-muted mb-2">Total Time</h6>
|
||||
<div class="progress mb-2" style="height: 25px;">
|
||||
<div id="fb-total-time-bar" class="progress-bar" role="progressbar" style="width: 0%">0%</div>
|
||||
</div>
|
||||
<small class="text-muted">Tiempo de procesamiento</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="text-muted mb-2">Total CPU Time</h6>
|
||||
<div class="progress mb-2" style="height: 25px;">
|
||||
<div id="fb-cpu-time-bar" class="progress-bar" role="progressbar" style="width: 0%">0%</div>
|
||||
</div>
|
||||
<small class="text-muted">Tiempo de CPU</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Límites Locales -->
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h6 class="mb-3"><i class="fas fa-shield-alt"></i> Límites Locales (Redis)</h6>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6>Por Segundo</h6>
|
||||
<h4 id="local-limit-second" class="mb-0">-</h4>
|
||||
<small class="text-muted">Límite: <span id="local-limit-second-max">80</span>/s</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6>Por Hora</h6>
|
||||
<h4 id="local-limit-hour" class="mb-0">-</h4>
|
||||
<small class="text-muted">Límite: <span id="local-limit-hour-max">1000</span>/h</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6>Por Día</h6>
|
||||
<h4 id="local-limit-day" class="mb-0">-</h4>
|
||||
<small class="text-muted">Límite: <span id="local-limit-day-max">10000</span>/d</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertas Recientes -->
|
||||
<div class="row mt-4" id="rate-limit-alerts-section" style="display:none;">
|
||||
<div class="col-12">
|
||||
<h6 class="mb-3"><i class="fas fa-exclamation-triangle"></i> Alertas Recientes</h6>
|
||||
<div id="rate-limit-alerts" class="list-group">
|
||||
<!-- Alertas dinámicas -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conversations Tab -->
|
||||
@@ -984,6 +1085,182 @@ try {
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="./assets/js/app_simple.js?v=20"></script>
|
||||
|
||||
<!-- Rate Limiting Monitor Script -->
|
||||
<script>
|
||||
// Función para actualizar estadísticas de Rate Limiting
|
||||
async function refreshRateLimitStats() {
|
||||
try {
|
||||
const response = await fetch('api/get_rate_limit_stats.php?action=current');
|
||||
|
||||
// Verificar que la respuesta sea válida
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
const text = await response.text();
|
||||
console.error('Respuesta no-JSON recibida:', text.substring(0, 200));
|
||||
throw new Error('El servidor no devolvió JSON válido');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Error obteniendo estadísticas');
|
||||
}
|
||||
|
||||
const stats = result.data;
|
||||
updateRateLimitUI(stats);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error cargando rate limit stats:', error);
|
||||
const statusEl = document.getElementById('rate-limit-status');
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML =
|
||||
'<i class="fas fa-exclamation-triangle"></i> Error cargando estadísticas: ' + error.message;
|
||||
statusEl.className = 'alert alert-warning';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateRateLimitUI(stats) {
|
||||
// Verificar que los elementos existan
|
||||
const statusEl = document.getElementById('rate-limit-status');
|
||||
const callCountBar = document.getElementById('fb-call-count-bar');
|
||||
|
||||
if (!statusEl || !callCountBar) {
|
||||
console.log('Elementos del rate limit monitor no encontrados en el DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
// Manejar caso donde Redis no está disponible
|
||||
if (stats.status === 'unavailable' || stats.error) {
|
||||
statusEl.className = 'alert alert-info';
|
||||
statusEl.innerHTML = '<i class="fas fa-info-circle"></i> Rate Limiting Monitor: Redis no disponible. Las estadísticas de Facebook se mostrarán cuando se realice la próxima llamada a la API.';
|
||||
|
||||
// Mostrar mensaje en las barras de progreso
|
||||
const metricsContainer = callCountBar.closest('.row');
|
||||
if (metricsContainer) {
|
||||
metricsContainer.innerHTML = '<div class="col-12"><div class="alert alert-info text-center"><i class="fas fa-info-circle"></i> Los datos de Facebook API se capturarán automáticamente en la próxima llamada. Redis: ' + (stats.error || 'No disponible') + '</div></div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Actualizar status general
|
||||
const statusEl = document.getElementById('rate-limit-status');
|
||||
let statusClass = 'alert-success';
|
||||
let statusIcon = 'fa-check-circle';
|
||||
let statusText = 'Sistema operando normalmente';
|
||||
|
||||
if (stats.status === 'critical') {
|
||||
statusClass = 'alert-danger';
|
||||
statusIcon = 'fa-exclamation-triangle';
|
||||
statusText = '¡ALERTA! Rate limit cerca del límite crítico';
|
||||
} else if (stats.status === 'warning') {
|
||||
statusClass = 'alert-warning';
|
||||
statusIcon = 'fa-exclamation-circle';
|
||||
statusText = 'Advertencia: Acercándose al límite';
|
||||
}
|
||||
|
||||
statusEl.className = `alert ${statusClass}`;
|
||||
statusEl.innerHTML = `<i class="fas ${statusIcon}"></i> ${statusText}`;
|
||||
|
||||
// Actualizar métricas de Facebook
|
||||
if (stats.app_usage) {
|
||||
updateProgressBar('fb-call-count-bar', stats.app_usage.call_count || 0);
|
||||
updateProgressBar('fb-total-time-bar', stats.app_usage.total_time || 0);
|
||||
updateProgressBar('fb-cpu-time-bar', stats.app_usage.total_cputime || 0);
|
||||
} else {
|
||||
// Sin datos de Facebook aún
|
||||
const callCountBar = document.getElementById('fb-call-count-bar');
|
||||
if (callCountBar) {
|
||||
const metricsRow = callCountBar.closest('.row');
|
||||
if (metricsRow) {
|
||||
metricsRow.innerHTML =
|
||||
'<div class="col-12"><div class="alert alert-info text-center"><i class="fas fa-info-circle"></i> Los datos de Facebook API se capturarán automáticamente en la próxima llamada.</div></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar límites locales
|
||||
if (stats.local_limits) {
|
||||
const limitSecondEl = document.getElementById('local-limit-second-max');
|
||||
const limitHourEl = document.getElementById('local-limit-hour-max');
|
||||
const limitDayEl = document.getElementById('local-limit-day-max');
|
||||
|
||||
if (limitSecondEl) limitSecondEl.textContent = stats.local_limits.per_second;
|
||||
if (limitHourEl) limitHourEl.textContent = stats.local_limits.per_hour;
|
||||
if (limitDayEl) limitDayEl.textContent = stats.local_limits.per_day;
|
||||
}
|
||||
|
||||
// Mostrar alertas si existen
|
||||
const alertsSection = document.getElementById('rate-limit-alerts-section');
|
||||
const alertsContainer = document.getElementById('rate-limit-alerts');
|
||||
|
||||
if (stats.alerts && stats.alerts.length > 0 && alertsSection && alertsContainer) {
|
||||
alertsSection.style.display = 'block';
|
||||
alertsContainer.innerHTML = '';
|
||||
|
||||
stats.alerts.slice(0, 5).forEach(alert => {
|
||||
const alertClass = alert.severity === 'critical' ? 'list-group-item-danger' : 'list-group-item-warning';
|
||||
const alertIcon = alert.severity === 'critical' ? 'fa-exclamation-triangle' : 'fa-exclamation-circle';
|
||||
|
||||
const alertEl = document.createElement('div');
|
||||
alertEl.className = `list-group-item ${alertClass}`;
|
||||
alertEl.innerHTML = `
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1"><i class="fas ${alertIcon}"></i> Alerta de ${alert.level}</h6>
|
||||
<small>${alert.datetime}</small>
|
||||
</div>
|
||||
<p class="mb-1">Uso al ${alert.usage}% - Endpoint: ${alert.data.endpoint || 'N/A'}</p>
|
||||
`;
|
||||
alertsContainer.appendChild(alertEl);
|
||||
});
|
||||
} else if (alertsSection) {
|
||||
alertsSection.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgressBar(id, percentage) {
|
||||
const bar = document.getElementById(id);
|
||||
if (!bar) return;
|
||||
|
||||
percentage = Math.min(100, Math.max(0, percentage));
|
||||
|
||||
bar.style.width = percentage + '%';
|
||||
bar.textContent = percentage.toFixed(1) + '%';
|
||||
|
||||
// Cambiar color según el porcentaje
|
||||
bar.className = 'progress-bar';
|
||||
if (percentage >= 90) {
|
||||
bar.classList.add('bg-danger');
|
||||
} else if (percentage >= 75) {
|
||||
bar.classList.add('bg-warning');
|
||||
} else if (percentage >= 50) {
|
||||
bar.classList.add('bg-info');
|
||||
} else {
|
||||
bar.classList.add('bg-success');
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar estadísticas al cargar la página
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Cargar inmediatamente
|
||||
refreshRateLimitStats();
|
||||
|
||||
// Actualizar cada 30 segundos
|
||||
setInterval(refreshRateLimitStats, 30000);
|
||||
});
|
||||
|
||||
// Actualizar también cuando se cambia a la pestaña de dashboard
|
||||
document.querySelectorAll('.nav-link[data-tab="dashboard"]').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
setTimeout(refreshRateLimitStats, 500);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Función de diagnóstico mejorada
|
||||
function runDiagnostic() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
$db = Database::getInstance();
|
||||
$rows = $db->fetchAll("SELECT id, message_type, media_url, filename, message_id, created_at FROM conversations WHERE (message_type IN ('image','video','audio')) AND (local_file IS NULL OR local_file = '') AND (media_url IS NOT NULL AND media_url != '') ORDER BY created_at DESC LIMIT 200");
|
||||
$rows = $db->fetchAll("SELECT id, message_type, media_url, filename, message_id, created_at FROM conversations WHERE (message_type IN ('image','video','audio','document','sticker')) AND (local_file IS NULL OR local_file = '') AND (media_url IS NOT NULL AND media_url != '') ORDER BY created_at DESC LIMIT 200");
|
||||
$inserted = 0;
|
||||
foreach ($rows as $r) {
|
||||
$media = $r['media_url'];
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* Servicio de Monitoreo de Rate Limiting de Facebook
|
||||
* Captura y analiza headers X-App-Usage y X-Business-Use-Case-Usage
|
||||
* Fecha: 6 de febrero de 2026
|
||||
*/
|
||||
|
||||
use Predis\Client as RedisClient;
|
||||
|
||||
class RateLimitMonitor {
|
||||
private $redis;
|
||||
private $enabled;
|
||||
|
||||
// Umbrales de alerta
|
||||
const WARNING_THRESHOLD = 75; // Advertencia al 75%
|
||||
const CRITICAL_THRESHOLD = 90; // Crítico al 90%
|
||||
|
||||
public function __construct() {
|
||||
try {
|
||||
// Usar el mismo patrón que RedisQueue
|
||||
$this->redis = new RedisClient([
|
||||
'scheme' => getenv('REDIS_SCHEME') ?: 'tcp',
|
||||
'host' => getenv('REDIS_HOST') ?: '127.0.0.1',
|
||||
'port' => getenv('REDIS_PORT') ?: 6379,
|
||||
'password' => getenv('REDIS_PASSWORD') ?: null,
|
||||
'database' => getenv('REDIS_DB') ?: 0,
|
||||
]);
|
||||
|
||||
// Verificar conexión
|
||||
$this->redis->ping();
|
||||
$this->enabled = true;
|
||||
} catch (Exception $e) {
|
||||
error_log("RateLimitMonitor: Redis no disponible - " . $e->getMessage());
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captura y almacena información de rate limit desde headers HTTP
|
||||
* @param array $responseHeaders - Headers de la respuesta de Facebook API
|
||||
* @param string $endpoint - Endpoint llamado (para tracking)
|
||||
*/
|
||||
public function captureFromHeaders(array $responseHeaders, string $endpoint = 'unknown'): void {
|
||||
if (!$this->enabled) return;
|
||||
|
||||
try {
|
||||
$headerText = is_array($responseHeaders) ? implode("\n", $responseHeaders) : $responseHeaders;
|
||||
|
||||
// Buscar X-App-Usage
|
||||
if (preg_match('/x-app-usage:\s*({[^}]+})/i', $headerText, $matches)) {
|
||||
$usage = json_decode($matches[1], true);
|
||||
if ($usage) {
|
||||
$this->recordAppUsage($usage, $endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar X-Business-Use-Case-Usage
|
||||
if (preg_match('/x-business-use-case-usage:\s*({.+})/i', $headerText, $matches)) {
|
||||
$usage = json_decode($matches[1], true);
|
||||
if ($usage) {
|
||||
$this->recordBusinessUsage($usage, $endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("RateLimitMonitor: Error capturando headers - " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra uso a nivel de App (X-App-Usage)
|
||||
*/
|
||||
private function recordAppUsage(array $usage, string $endpoint): void {
|
||||
$timestamp = time();
|
||||
$key = "fb:ratelimit:app:current";
|
||||
$historyKey = "fb:ratelimit:app:history";
|
||||
|
||||
$data = [
|
||||
'call_count' => $usage['call_count'] ?? 0,
|
||||
'total_time' => $usage['total_time'] ?? 0,
|
||||
'total_cputime' => $usage['total_cputime'] ?? 0,
|
||||
'endpoint' => $endpoint,
|
||||
'timestamp' => $timestamp,
|
||||
'datetime' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// Guardar estado actual
|
||||
$this->redis->setex($key, 3600, json_encode($data));
|
||||
|
||||
// Agregar a historial (últimas 100 muestras)
|
||||
$this->redis->lpush($historyKey, json_encode($data));
|
||||
$this->redis->ltrim($historyKey, 0, 99);
|
||||
|
||||
// Verificar umbrales y generar alertas
|
||||
$this->checkThresholds('app', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra uso a nivel de Business (X-Business-Use-Case-Usage)
|
||||
*/
|
||||
private function recordBusinessUsage(array $usage, string $endpoint): void {
|
||||
$timestamp = time();
|
||||
|
||||
foreach ($usage as $businessId => $metrics) {
|
||||
if (!is_array($metrics)) continue;
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
if (!is_array($metric)) continue;
|
||||
|
||||
$type = $metric['type'] ?? 'unknown';
|
||||
$key = "fb:ratelimit:business:{$businessId}:{$type}";
|
||||
$historyKey = "fb:ratelimit:business:{$businessId}:{$type}:history";
|
||||
|
||||
$data = [
|
||||
'business_id' => $businessId,
|
||||
'type' => $type,
|
||||
'call_count' => $metric['call_count'] ?? 0,
|
||||
'total_time' => $metric['total_time'] ?? 0,
|
||||
'total_cputime' => $metric['total_cputime'] ?? 0,
|
||||
'estimated_time_to_regain_access' => $metric['estimated_time_to_regain_access'] ?? 0,
|
||||
'ads_api_access_tier' => $metric['ads_api_access_tier'] ?? null,
|
||||
'endpoint' => $endpoint,
|
||||
'timestamp' => $timestamp,
|
||||
'datetime' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// Guardar estado actual
|
||||
$this->redis->setex($key, 3600, json_encode($data));
|
||||
|
||||
// Agregar a historial
|
||||
$this->redis->lpush($historyKey, json_encode($data));
|
||||
$this->redis->ltrim($historyKey, 0, 99);
|
||||
|
||||
// Verificar umbrales
|
||||
$this->checkThresholds('business', $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica umbrales y genera alertas si es necesario
|
||||
*/
|
||||
private function checkThresholds(string $level, array $data): void {
|
||||
$callCount = $data['call_count'] ?? 0;
|
||||
$totalTime = $data['total_time'] ?? 0;
|
||||
$totalCputime = $data['total_cputime'] ?? 0;
|
||||
|
||||
$maxUsage = max($callCount, $totalTime, $totalCputime);
|
||||
|
||||
if ($maxUsage >= self::CRITICAL_THRESHOLD) {
|
||||
$this->triggerAlert('critical', $level, $data, $maxUsage);
|
||||
} elseif ($maxUsage >= self::WARNING_THRESHOLD) {
|
||||
$this->triggerAlert('warning', $level, $data, $maxUsage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispara una alerta de rate limit
|
||||
*/
|
||||
private function triggerAlert(string $severity, string $level, array $data, float $usage): void {
|
||||
$alertKey = "fb:ratelimit:alerts";
|
||||
|
||||
$alert = [
|
||||
'severity' => $severity,
|
||||
'level' => $level,
|
||||
'usage' => $usage,
|
||||
'data' => $data,
|
||||
'timestamp' => time(),
|
||||
'datetime' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// Guardar alerta
|
||||
$this->redis->lpush($alertKey, json_encode($alert));
|
||||
$this->redis->ltrim($alertKey, 0, 49); // Mantener últimas 50 alertas
|
||||
$this->redis->expire($alertKey, 86400 * 7); // 7 días
|
||||
|
||||
// Log
|
||||
error_log("R
|
||||
'error' => 'Redis no disponible',
|
||||
'app_usage' => null,
|
||||
'business_usage' => [],
|
||||
'local_limits' => $this->getLocalLimits(),
|
||||
'alerts' => [],
|
||||
'status' => 'unavailable'
|
||||
rt - {$level} usage at {$usage}% for endpoint: " . ($data['endpoint'] ?? 'unknown'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas actuales de rate limit
|
||||
*/
|
||||
public function getCurrentStats(): array {
|
||||
if (!$this->enabled) {
|
||||
return ['error' => 'Redis no disponible'];
|
||||
}
|
||||
|
||||
try {
|
||||
$stats = [
|
||||
'app_usage' => null,
|
||||
'business_usage' => [],
|
||||
'local_limits' => $this->getLocalLimits(),
|
||||
'alerts' => [],
|
||||
'status' => 'healthy'
|
||||
];
|
||||
|
||||
// Obtener uso a nivel de app
|
||||
$appData = $this->redis->get("fb:ratelimit:app:current");
|
||||
if ($appData) {
|
||||
$stats['app_usage'] = json_decode($appData, true);
|
||||
}
|
||||
|
||||
// Obtener uso a nivel de business
|
||||
$businessKeys = $this->redis->keys("fb:ratelimit:business:*:current") ?: [];
|
||||
foreach ($businessKeys as $key) {
|
||||
$data = $this->redis->get($key);
|
||||
if ($data) {
|
||||
$decoded = json_decode($data, true);
|
||||
$stats['business_usage'][] = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// Obtener alertas recientes
|
||||
$alerts = $this->redis->lrange("fb:ratelimit:alerts", 0, 9);
|
||||
foreach ($alerts as $alert) {
|
||||
$stats['alerts'][] = json_decode($alert, true);
|
||||
}
|
||||
|
||||
// Determinar estado general
|
||||
$maxUsage = 0;
|
||||
if ($stats['app_usage']) {
|
||||
$maxUsage = max(
|
||||
$stats['app_usage']['call_count'] ?? 0,
|
||||
$stats['app_usage']['total_time'] ?? 0,
|
||||
$stats['app_usage']['total_cputime'] ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
if ($maxUsage >= self::CRITICAL_THRESHOLD) {
|
||||
$stats['status'] = 'critical';
|
||||
} elseif ($maxUsage >= self::WARNING_THRESHOLD) {
|
||||
$stats['status'] = 'warning';
|
||||
}
|
||||
|
||||
return $stats;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("RateLimitMonitor: Error obteniendo stats - " . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene límites locales (del sistema)
|
||||
*/
|
||||
private function getLocalLimits(): array {
|
||||
try {
|
||||
// Intentar obtener de WhatsAppServiceWithRateLimit si existe
|
||||
if (class_exists('WhatsAppServiceWithRateLimit')) {
|
||||
$reflection = new ReflectionClass('WhatsAppServiceWithRateLimit');
|
||||
|
||||
return [
|
||||
'per_second' => $reflection->getConstant('RATE_LIMIT_PER_SECOND') ?: 80,
|
||||
'per_hour' => $reflection->getConstant('RATE_LIMIT_PER_HOUR') ?: 1000,
|
||||
'per_day' => $reflection->getConstant('RATE_LIMIT_PER_DAY') ?: 10000,
|
||||
];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Fallback a valores por defecto
|
||||
}
|
||||
|
||||
return [
|
||||
'per_second' => 80,
|
||||
'per_hour' => 1000,
|
||||
'per_day' => 10000,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene historial de uso
|
||||
*/
|
||||
public function getUsageHistory(string $type = 'app', int $limit = 20): array {
|
||||
if (!$this->enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$key = "fb:ratelimit:{$type}:history";
|
||||
$items = $this->redis->lrange($key, 0, $limit - 1);
|
||||
|
||||
$history = [];
|
||||
foreach ($items as $item) {
|
||||
$decoded = json_decode($item, true);
|
||||
if ($decoded) {
|
||||
$history[] = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return $history;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("RateLimitMonitor: Error obteniendo historial - " . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia datos antiguos (mantenimiento)
|
||||
*/
|
||||
public function cleanup(): void {
|
||||
if (!$this->enabled) return;
|
||||
|
||||
try {
|
||||
// Limpiar alertas antiguas
|
||||
$this->redis->expire("fb:ratelimit:alerts", 86400 * 7);
|
||||
|
||||
// Limpiar historiales antiguos
|
||||
$historyKeys = $this->redis->keys("fb:ratelimit:*:history") ?: [];
|
||||
foreach ($historyKeys as $key) {
|
||||
$this->redis->ltrim($key, 0, 99);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("RateLimitMonitor: Error en cleanup - " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ class WhatsAppService
|
||||
private $phoneNumberId;
|
||||
private $apiUrl;
|
||||
private $db;
|
||||
private $rateLimitMonitor;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -30,6 +31,25 @@ class WhatsAppService
|
||||
$this->phoneNumberId = $config['phone_number_id']; // Usar phone_number_id de BD
|
||||
$this->apiUrl = $config['api_url'] ?: 'https://graph.facebook.com/v22.0/'; // Usar api_url de BD
|
||||
$this->db = Database::getInstance();
|
||||
|
||||
// Inicializar monitor de rate limit
|
||||
try {
|
||||
// Cargar autoloader si no está cargado
|
||||
if (!class_exists('Predis\Client')) {
|
||||
$autoloadPath = __DIR__ . '/../vendor/autoload.php';
|
||||
if (file_exists($autoloadPath)) {
|
||||
require_once $autoloadPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (file_exists(__DIR__ . '/RateLimitMonitor.php')) {
|
||||
require_once __DIR__ . '/RateLimitMonitor.php';
|
||||
$this->rateLimitMonitor = new RateLimitMonitor();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("WhatsAppService: No se pudo inicializar RateLimitMonitor - " . $e->getMessage());
|
||||
$this->rateLimitMonitor = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -561,7 +581,8 @@ class WhatsAppService
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 3
|
||||
CURLOPT_MAXREDIRS => 3,
|
||||
CURLOPT_HEADER => true, // Capturar headers de respuesta
|
||||
]);
|
||||
|
||||
if ($method === 'POST' && $data) {
|
||||
@@ -569,13 +590,27 @@ class WhatsAppService
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$fullResponse = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
// Separar headers y body
|
||||
$headerText = substr($fullResponse, 0, $headerSize);
|
||||
$response = substr($fullResponse, $headerSize);
|
||||
|
||||
// curl_close is deprecated in PHP 8.5+ (has no effect). Avoid calling it on newer versions.
|
||||
if (version_compare(PHP_VERSION, '8.5.0', '<')) {
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
// Capturar rate limit info de los headers
|
||||
if ($this->rateLimitMonitor && $headerText) {
|
||||
$endpoint = parse_url($url, PHP_URL_PATH);
|
||||
// Parsear headers string a array
|
||||
$headersArray = array_filter(explode("\r\n", $headerText));
|
||||
$this->rateLimitMonitor->captureFromHeaders($headersArray, $endpoint);
|
||||
}
|
||||
|
||||
if ($error) {
|
||||
error_log("cURL Error: " . $error);
|
||||
|
||||
Reference in New Issue
Block a user