diff --git a/api/get_conversation_detail.php b/api/get_conversation_detail.php index 3eb0d27..f35cf58 100644 --- a/api/get_conversation_detail.php +++ b/api/get_conversation_detail.php @@ -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] diff --git a/api/get_rate_limit_stats.php b/api/get_rate_limit_stats.php new file mode 100644 index 0000000..8d0e276 --- /dev/null +++ b/api/get_rate_limit_stats.php @@ -0,0 +1,94 @@ + 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() + ]); +} diff --git a/api/version/media-url.php b/api/version/media-url.php index ca8f377..4d77220 100644 --- a/api/version/media-url.php +++ b/api/version/media-url.php @@ -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 'Media URL Debug'; + echo ''; + echo '

🔍 Diagnóstico media-url.php

'; + echo '

Parámetros de entrada

';
+    echo "Media ID: " . htmlspecialchars($mediaId ?? '(no proporcionado)') . "\n";
+    echo "URL proporcionada: " . htmlspecialchars($providedUrl ?? '(no proporcionada)') . "\n";
+    echo "download: " . ($download ? '1' : '0') . "\n";
+    echo '
'; + + echo '

Configuración

';
+    echo "Token: " . htmlspecialchars($tokenPreview) . "\n";
+    echo "API URL: " . htmlspecialchars($apiUrlConfig) . "\n";
+    
+    if (empty($tokenRaw)) {
+        echo '⛔ ERROR: Token de WhatsApp vacío o no configurado' . "\n";
+        echo 'Solución: Configurar en /configurar_whatsapp.php o en system_config (key: whatsapp_token)' . "\n";
+    } else {
+        echo '✅ Token presente' . "\n";
+    }
+    echo '
'; + + echo '

Resultado Graph API

';
+    if ($mediaUrl) {
+        echo '✅ URL resuelta: ' . htmlspecialchars($mediaUrl) . "\n";
+        echo "Resuelto desde Graph: " . ($resolvedFromGraph ? 'Sí' : 'No') . "\n";
+        
+        // Validar que la URL sea accesible desde el servidor
+        echo "\nVerificando accesibilidad de la URL...\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 '⛔ Error cURL: ' . htmlspecialchars($testErr) . '' . "\n";
+        }
+        
+        if (($testInfo['http_code'] ?? 0) >= 400) {
+            echo '⛔ La URL del media NO es accesible (HTTP ' . $testInfo['http_code'] . ')' . "\n";
+            if (($testInfo['http_code'] ?? 0) == 404) {
+                echo '⚠️ El media probablemente expiró. Los archivos multimedia de WhatsApp expiran después de ~30 días.' . "\n";
+            } elseif (($testInfo['http_code'] ?? 0) == 401 || ($testInfo['http_code'] ?? 0) == 403) {
+                echo '⚠️ Error de autenticación. Verificar que el token sea válido y tenga permisos.' . "\n";
+            }
+        } else {
+            echo '✅ URL accesible correctamente' . "\n";
+        }
+    } else {
+        echo '⛔ No se pudo resolver la URL del media' . "\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 '⛔ Token expirado o inválido (code 190)' . "\n";
+                echo 'Solución: Renovar el token en /configurar_whatsapp.php' . "\n";
+            } elseif ($errCode == 100 && $errSubcode == 33) {
+                echo '⛔ Media ID no encontrado o expirado' . "\n";
+                echo 'Los archivos multimedia de WhatsApp Business API expiran aproximadamente 30 días después de ser recibidos.' . "\n";
+            } elseif ($errCode == 100) {
+                echo '⛔ Parámetro inválido (code 100)' . "\n";
+                echo 'El media ID puede ser incorrecto: ' . htmlspecialchars($mediaId ?? '') . '' . "\n";
+            }
+        }
+    }
+    echo '
'; + + echo '

Respuesta raw de Graph API ($decoded)

';
+    echo htmlspecialchars(json_encode($decoded ?? null, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
+    echo '
'; + + echo '

Generado: ' . date('Y-m-d H:i:s T') . '

'; + echo ''; + exit; +} + // Soporte para archivos locales (local=uploads/media/...) $local = $_GET['local'] ?? null; if ($local) { diff --git a/api/version/media-url_debug.log b/api/version/media-url_debug.log index 1e19b5e..0bd91a7 100644 --- a/api/version/media-url_debug.log +++ b/api/version/media-url_debug.log @@ -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 diff --git a/api/version/media-url_error.log b/api/version/media-url_error.log index 6ed5eb5..56e5020 100644 --- a/api/version/media-url_error.log +++ b/api/version/media-url_error.log @@ -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 diff --git a/api/webhook.php b/api/webhook.php index f01740d..fddf376 100644 --- a/api/webhook.php +++ b/api/webhook.php @@ -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', [ diff --git a/index.php b/index.php index f8a42a2..a7b934d 100644 --- a/index.php +++ b/index.php @@ -197,6 +197,107 @@ try { + + +
+
+
+
+
Monitoreo de Rate Limiting - Facebook API
+ +
+
+ + + + +
+
+
+
+
Call Count
+
+
0%
+
+ Llamadas a la API +
+
+
+
+
+
+
Total Time
+
+
0%
+
+ Tiempo de procesamiento +
+
+
+
+
+
+
Total CPU Time
+
+
0%
+
+ Tiempo de CPU +
+
+
+
+ + +
+
+
Límites Locales (Redis)
+
+
+
+
+
Por Segundo
+

-

+ Límite: 80/s +
+
+
+
+
+
+
Por Hora
+

-

+ Límite: 1000/h +
+
+
+
+
+
+
Por Día
+

-

+ Límite: 10000/d +
+
+
+
+ + + +
+
+
+
@@ -984,6 +1085,182 @@ try { + + +