This commit is contained in:
lizandrogd
2026-01-20 22:13:02 -05:00
parent f0a713e1da
commit 648a6c0291
19 changed files with 762 additions and 33 deletions
+80
View File
@@ -0,0 +1,80 @@
<?php
/**
* API - Obtener payload crudo de un webhook
* Fecha: 20 de enero de 2026
*/
require_once '../config/config_enhanced.php';
// Suprimir errores para obtener JSON limpio
error_reporting(E_ERROR | E_PARSE);
// Modo debug: desactivar autenticación si existe el parámetro debug
$debugMode = isset($_GET['debug']) && $_GET['debug'] === 'true';
if (!$debugMode) {
requireAuthentication();
}
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['error' => 'Método no permitido. Use GET.']);
exit;
}
try {
$db = Database::getInstance();
$id = intval($_GET['id'] ?? 0);
if (!$id) {
http_response_code(400);
echo json_encode(['error' => 'ID de webhook requerido']);
exit;
}
$row = $db->fetch(
'SELECT id, request_body, response_body, status_code, ip_address, created_at FROM webhook_logs WHERE id = ? LIMIT 1',
[$id]
);
if (!$row) {
http_response_code(404);
echo json_encode(['error' => 'Registro no encontrado']);
exit;
}
// Intentar parsear JSON bonito
$pretty = null;
$decoded = json_decode($row['request_body'], true);
if ($decoded !== null) {
$pretty = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
echo json_encode([
'success' => true,
'data' => [
'id' => $row['id'],
'request_body' => $row['request_body'],
'request_body_pretty' => $pretty,
'response_body' => $row['response_body'],
'status_code' => $row['status_code'],
'ip_address' => $row['ip_address'],
'created_at' => $row['created_at']
]
]);
} catch (Exception $e) {
error_log("Error in get_webhook_log.php: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Error interno del servidor']);
}
+101
View File
@@ -0,0 +1,101 @@
<?php
/**
* API - Obtener registros de webhooks
* Fecha: 20 de enero de 2026
*/
require_once '../config/config_enhanced.php';
// Suprimir errores para obtener JSON limpio
error_reporting(E_ERROR | E_PARSE);
// Modo debug: desactivar autenticación si existe el parámetro debug
$debugMode = isset($_GET['debug']) && $_GET['debug'] === 'true';
if (!$debugMode) {
requireAuthentication();
}
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['error' => 'Método no permitido. Use GET.']);
exit;
}
try {
$db = Database::getInstance();
$limit = max(1, min(200, intval($_GET['limit'] ?? 50)));
$page = max(1, intval($_GET['page'] ?? 1));
$offset = ($page - 1) * $limit;
$sinceId = isset($_GET['since_id']) ? intval($_GET['since_id']) : null;
$search = trim($_GET['search'] ?? '');
$filter = trim($_GET['filter'] ?? ''); // e.g., HAS_MESSAGES
$where = [];
$params = [];
if ($sinceId) {
$where[] = 'id > ?';
$params[] = $sinceId;
}
if ($search !== '') {
$where[] = '(request_body LIKE ?)';
$params[] = "%{$search}%";
}
if ($filter === 'HAS_MESSAGES') {
$where[] = 'request_body LIKE ?';
$params[] = '%"messages"%';
}
$whereClause = empty($where) ? '' : 'WHERE ' . implode(' AND ', $where);
if ($sinceId) {
// Return newer logs only in ascending order for incremental updates
$rows = $db->fetchAll(
"SELECT id, SUBSTRING(request_body, 1, 1000) as snippet, status_code, ip_address, created_at FROM webhook_logs {$whereClause} ORDER BY id ASC LIMIT ?",
array_merge($params, [$limit])
);
echo json_encode(['success' => true, 'data' => $rows]);
exit;
}
$rows = $db->fetchAll(
"SELECT id, SUBSTRING(request_body, 1, 500) as snippet, status_code, ip_address, created_at FROM webhook_logs {$whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?",
array_merge($params, [$limit, $offset])
);
$total = $db->fetch(
"SELECT COUNT(*) as cnt FROM webhook_logs {$whereClause}",
$params
);
$totalCount = intval($total['cnt'] ?? 0);
echo json_encode([
'success' => true,
'data' => $rows,
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $totalCount,
'pages' => $totalCount > 0 ? ceil($totalCount / $limit) : 0
]
]);
} catch (Exception $e) {
error_log("Error in get_webhook_logs.php: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Error interno del servidor']);
}
+50 -11
View File
@@ -4,7 +4,7 @@
* Fecha: 13 de noviembre de 2025
*/
require_once '../config/config.php';
require_once __DIR__ . '/../config/config_enhanced.php';
// Headers para API
header('Content-Type: application/json; charset=utf-8');
@@ -146,8 +146,12 @@ class WhatsAppWebhook {
'status' => 'received'
]);
// Procesar con bot
$this->botService->processMessage($user, $messageText, $messageType);
// Procesar con bot (protección contra excepciones externas)
try {
$this->botService->processMessage($user, $messageText, $messageType);
} catch (Exception $e) {
error_log("Bot processing failed: " . $e->getMessage());
}
}
// Procesar estados de mensajes (entregado, leído, etc.)
@@ -207,15 +211,50 @@ class WhatsAppWebhook {
]);
}
}
/**
* Procesar un payload (array) recibido manualmente (replay)
* Útil para reproducir entradas desde la UI o scripts.
*/
public function processPayload(array $data) {
error_log("processPayload START");
// Registrar como recibido (reproducción)
$this->logWebhook(json_encode($data), json_encode(['status' => 'replayed']), 200);
if (empty($data) || !isset($data['entry'])) {
error_log("processPayload: empty or missing entry");
return false;
}
foreach ($data['entry'] as $entry) {
if (isset($entry['changes'])) {
foreach ($entry['changes'] as $change) {
error_log("processPayload: processing change field=" . ($change['field'] ?? ''));
if (($change['field'] ?? '') === 'messages' && isset($change['value'])) {
try {
$this->processMessages($change['value']);
} catch (Exception $e) {
error_log("processPayload: processMessages failed: " . $e->getMessage());
}
}
}
}
}
error_log("processPayload END");
return true;
}
}
// Procesar la solicitud
try {
$webhook = new WhatsAppWebhook();
$webhook->handleRequest();
} catch (Exception $e) {
error_log("Fatal error in webhook: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Error fatal del servidor']);
// Procesar la solicitud (solo si no estamos en CLI)
if (php_sapi_name() !== 'cli') {
try {
$webhook = new WhatsAppWebhook();
$webhook->handleRequest();
} catch (Exception $e) {
error_log("Fatal error in webhook: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Error fatal del servidor']);
}
}
?>
+61
View File
@@ -0,0 +1,61 @@
<?php
/**
* API - Reproducir payload de webhook (simulación)
* Fecha: 20 de enero de 2026
*/
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/webhook.php';
error_log('webhook_replay loaded');
// Suprimir errores para obtener JSON limpio
error_reporting(E_ERROR | E_PARSE);
$debugMode = isset($_GET['debug']) && $_GET['debug'] === 'true';
if (!$debugMode) {
requireAuthentication();
}
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Método no permitido. Use POST.']);
exit;
}
try {
error_log('webhook_replay START');
$raw = file_get_contents('php://input');
error_log('webhook_replay raw length: ' . strlen($raw));
$data = json_decode($raw, true);
if (!$data) {
error_log('webhook_replay: invalid json');
http_response_code(400);
echo json_encode(['error' => 'JSON inválido o vacío']);
exit;
}
$webhook = new WhatsAppWebhook();
$ok = $webhook->processPayload($data);
if ($ok) {
echo json_encode(['success' => true, 'message' => 'Payload reproducido correctamente']);
} else {
http_response_code(400);
echo json_encode(['error' => 'El payload no contenía entradas válidas']);
}
} catch (Exception $e) {
error_log('Error in webhook_replay.php: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Error interno del servidor']);
}
+33 -11
View File
@@ -650,17 +650,39 @@ if (empty($user_id)) {
// Cargar plantillas
async function loadTemplates() {
try {
const response = await whatsappManager.apiCall('check_templates.php');
const templateSelect = document.getElementById('templateSelect');
if (response && response.templates) {
templateSelect.innerHTML = '<option value="">Seleccionar plantilla...</option>';
response.templates.forEach(template => {
templateSelect.innerHTML += `<option value="${template.name}">${template.display_name || template.name}</option>`;
});
const response = await whatsappManager.apiCall('check_templates.php');
const templateSelect = document.getElementById('templateSelect');
if (response && response.templates) {
templateSelect.innerHTML = '<option value="">Seleccionar plantilla...</option>';
response.templates.forEach(template => {
// Esperamos que la API devuelva "language_code" (fallback a en_US)
const lang = template.language_code || template.language || 'en_US';
const display = template.display_name || template.name;
const option = document.createElement('option');
option.value = template.name;
option.textContent = `${display} (${lang})`;
option.dataset.language = lang;
templateSelect.appendChild(option);
});
// Valor por defecto global para la plantilla seleccionada
window.language = 'en_US';
// Actualizar el language global cuando cambie la plantilla seleccionada
templateSelect.addEventListener('change', function() {
const sel = this.selectedOptions[0];
window.language = sel && sel.dataset && sel.dataset.language ? sel.dataset.language : 'en_US';
});
// Si hay una plantilla seleccionada por defecto, establecer language acorde
if (templateSelect.selectedOptions.length && templateSelect.selectedOptions[0].dataset.language) {
window.language = templateSelect.selectedOptions[0].dataset.language;
}
}
} catch (error) {
console.error('Error cargando plantillas:', error);
console.error('Error cargando plantillas:', error);
}
}
@@ -690,7 +712,7 @@ if (empty($user_id)) {
return;
}
await sendTemplateMessage(template, message);
await sendTemplateMessage(template,language, message);
} else {
await sendTextMessage(message);
}
@@ -739,7 +761,7 @@ if (empty($user_id)) {
}
// Enviar mensaje de plantilla
async function sendTemplateMessage(template, parameters) {
async function sendTemplateMessage(template,language, parameters) {
try {
showTyping();
+1 -1
View File
@@ -26,7 +26,7 @@ CREATE TABLE conversations (
message_type ENUM('text', 'image', 'audio', 'video', 'document', 'template') DEFAULT 'text',
content TEXT,
media_url VARCHAR(500),
status ENUM('sent', 'delivered', 'read', 'failed') DEFAULT 'sent',
status ENUM('sent', 'delivered', 'read', 'failed', 'received') DEFAULT 'sent',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
+9 -10
View File
@@ -196,9 +196,9 @@ try {
<span class="input-group-text bg-light border-0">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control border-0"
placeholder="Buscar conversaciones..."
id="search-conversations">
<input type="text" class="form-control border-0"
placeholder="Buscar conversaciones..."
id="search-conversations">
</div>
</div>
</div>
@@ -581,10 +581,9 @@ try {
</div>
<div class="mb-3">
<label class="form-label">Idioma</label>
<select class="form-control" id="template-language">
<option value="es">Español</option>
<option value="en">English</option>
<option value="pt">Português</option>
<select class="form-control" id="template-language" name="template_language">
<option value="es_ES" <?= (defined('APP_LANG') && constant('APP_LANG') === 'es_ES') ? 'selected' : '' ?>>Español</option>
<option value="en_US" <?= (defined('APP_LANG') && constant('APP_LANG') === 'en_US') ? 'selected' : '' ?>>English</option>
</select>
</div>
<div class="mb-3">
@@ -689,7 +688,7 @@ try {
// Si el mensaje es un string simple
apiMessage = data.api.message;
}
html += `<div class="mb-3">
<h6><i class="fas fa-plug"></i> Conectividad API</h6>
<div class="alert alert-${data.api.status === 'ready' ? 'success' : 'danger'}">
@@ -774,7 +773,7 @@ try {
<option value="inactive">Inactivo</option>
</select>
</div>
<hr>
<h6>📋 Opciones del Menú</h6>
<div id="menu-options-container">
@@ -810,7 +809,7 @@ try {
<div class="modal-body">
<form id="autoresponse-form">
<input type="hidden" id="autoresponse-id">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
+2
View File
@@ -0,0 +1,2 @@
[20-Jan-2026 21:21:01 America/Bogota] Query failed: SQLSTATE[01000]: Warning: 1265 Data truncated for column 'status' at row 1 SQL: INSERT INTO conversations (user_id,message_id,direction,message_type,content,media_url,status,created_at) VALUES (:user_id, :message_id, :direction, :message_type, :content, :media_url, :status, :created_at) Params: {"user_id":26,"message_id":"ABGGFlA5Fpa","direction":"incoming","message_type":"text","content":"this is a text message","media_url":null,"status":"received","created_at":"2026-01-20 21:21:01"}
[20-Jan-2026 21:22:55 America/Bogota] Query failed: SQLSTATE[01000]: Warning: 1265 Data truncated for column 'status' at row 1 SQL: INSERT INTO conversations (user_id,message_id,direction,message_type,content,media_url,status,created_at) VALUES (:user_id, :message_id, :direction, :message_type, :content, :media_url, :status, :created_at) Params: {"user_id":26,"message_id":"ABGGFlA5Fpa","direction":"incoming","message_type":"text","content":"this is a text message","media_url":null,"status":"received","created_at":"2026-01-20 21:22:55"}
+10
View File
@@ -0,0 +1,10 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../classes/Database.php';
try {
$db = Database::getInstance();
$db->query("ALTER TABLE conversations MODIFY COLUMN status ENUM('sent','delivered','read','failed','received') DEFAULT 'sent'");
echo "ALTER OK\n";
} catch (Exception $e) {
echo "ALTER ERROR: " . $e->getMessage() . "\n";
}
+45
View File
@@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../classes/Database.php';
try {
$db = Database::getInstance();
$rows = $db->fetchAll('SELECT id, request_body, created_at FROM webhook_logs ORDER BY created_at DESC LIMIT 50');
$missing = [];
foreach ($rows as $r) {
$data = json_decode($r['request_body'], true);
if (!$data) continue;
if (!empty($data['entry'])) {
foreach ($data['entry'] as $entry) {
if (!empty($entry['changes'])) {
foreach ($entry['changes'] as $change) {
$val = $change['value'] ?? [];
if (!empty($val['messages'])) {
foreach ($val['messages'] as $m) {
$mid = $m['id'] ?? null;
$from = $m['from'] ?? null;
if ($mid) {
$exists = $db->fetch("SELECT id FROM conversations WHERE message_id = ? LIMIT 1", [$mid]);
if (!$exists) {
$missing[] = ['message_id' => $mid, 'from' => $from, 'log_id' => $r['id'], 'log_time' => $r['created_at']];
}
}
}
}
}
}
}
}
}
if (empty($missing)) {
echo "All recent messages found in conversations.\n";
} else {
echo "Missing messages (not found in conversations):\n";
foreach ($missing as $m) {
echo json_encode($m) . PHP_EOL;
}
}
} catch (Exception $e) {
echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
}
+15
View File
@@ -0,0 +1,15 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../classes/Database.php';
try {
$db = Database::getInstance();
$row = $db->fetch('SELECT * FROM webhook_logs ORDER BY created_at DESC LIMIT 1');
if ($row) {
echo json_encode(['found' => true, 'row' => $row]);
} else {
echo json_encode(['found' => false]);
}
} catch (Exception $e) {
echo json_encode(['error' => $e->getMessage()]);
}
+14
View File
@@ -0,0 +1,14 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../classes/Database.php';
try {
$db = Database::getInstance();
$rows = $db->fetchAll('SELECT id, request_body, created_at FROM webhook_logs ORDER BY created_at DESC LIMIT 20');
foreach ($rows as $r) {
$rb = $r['request_body'];
$has = strpos($rb, '"messages"') !== false ? 'HAS_MESSAGES' : (strpos($rb, '"statuses"') !== false ? 'ONLY_STATUSES' : 'OTHER');
echo $r['created_at'] . ' | id:' . $r['id'] . ' | ' . $has . PHP_EOL;
}
} catch (Exception $e) {
echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
}
+18
View File
@@ -0,0 +1,18 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../classes/Database.php';
$mid = $argv[1] ?? '';
if (!$mid) { echo "Usage: php dump_webhook_payload.php <message_id>\n"; exit(1); }
try {
$db = Database::getInstance();
$row = $db->fetch('SELECT id, request_body, created_at FROM webhook_logs WHERE request_body LIKE ? ORDER BY created_at DESC LIMIT 1', ['%'.$mid.'%']);
if ($row) {
$file = __DIR__ . '/webhook_payload_'.$mid.'.json';
file_put_contents($file, $row['request_body']);
echo "Saved payload to: $file\n";
} else echo "No encontrado\n";
} catch (Exception $e) {
echo 'ERROR: '.$e->getMessage().PHP_EOL;
}
+15
View File
@@ -0,0 +1,15 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../classes/Database.php';
$mid = $argv[1] ?? '';
if (!$mid) { echo "Usage: php find_webhook_by_mid.php <message_id>\n"; exit(1); }
try {
$db = Database::getInstance();
$row = $db->fetch('SELECT id, request_body, created_at FROM webhook_logs WHERE request_body LIKE ? ORDER BY created_at DESC LIMIT 1', ['%'.$mid.'%']);
if ($row) echo $row['created_at'] . ' | id:' . $row['id'] . PHP_EOL . $row['request_body'] . PHP_EOL;
else echo "No encontrado\n";
} catch (Exception $e) {
echo 'ERROR: '.$e->getMessage().PHP_EOL;
}
+68
View File
@@ -0,0 +1,68 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../classes/Database.php';
$file = __DIR__ . '/webhook_payload_ABGGFlA5Fpa.json';
if (!file_exists($file)) { echo "Payload not found\n"; exit(1); }
$payload = json_decode(file_get_contents($file), true);
if (!$payload) { echo "Invalid JSON\n"; exit(1); }
try {
$db = Database::getInstance();
foreach ($payload['entry'] as $entry) {
foreach ($entry['changes'] as $change) {
$val = $change['value'];
if (!empty($val['messages'])) {
foreach ($val['messages'] as $message) {
$phone = $message['from'];
$mid = $message['id'];
$text = $message['text']['body'] ?? '';
// Check user
$user = $db->fetch('SELECT id FROM users WHERE phone_number = ?', [$phone]);
if (!$user) {
echo "User not found, creating: $phone\n";
$db->insert('users', ['phone_number' => $phone, 'status' => 'active', 'created_at' => date('Y-m-d H:i:s')]);
$userId = $db->lastInsertId();
echo "Created user id: $userId\n";
} else {
$userId = $user['id'];
echo "User exists id: $userId\n";
}
// Attempt insert
echo "Attempting insert message_id: $mid\n";
$msg = [
'user_id' => $userId,
'message_id' => $mid,
'direction' => 'incoming',
'message_type' => 'text',
'content' => $text,
'media_url' => null,
'status' => 'received',
'created_at' => date('Y-m-d H:i:s')
];
try {
// Manual insert using PDO to capture detailed errors
$pdo = $db->getConnection();
$keys = array_keys($msg);
$fields = implode(', ', $keys);
$placeholders = ':' . implode(', :', $keys);
$sql = "INSERT INTO conversations ({$fields}) VALUES ({$placeholders})";
$stmt = $pdo->prepare($sql);
echo "Prepared SQL\n";
$res = $stmt->execute($msg);
echo "Executed: " . ($res ? 'true' : 'false') . "\n";
$err = $stmt->errorInfo();
echo "Stmt errorInfo: " . json_encode($err) . "\n";
$lastId = $pdo->lastInsertId();
echo "Inserted conversation id (manual): $lastId\n";
} catch (Exception $e) {
echo "INSERT EXCEPTION: " . $e->getMessage() . PHP_EOL;
}
}
}
}
}
} catch (Exception $e) {
echo "EXCEPTION: " . $e->getMessage() . PHP_EOL;
}
+22
View File
@@ -0,0 +1,22 @@
<?php
echo "TEST START\n";
ini_set('display_errors', 1);
error_reporting(E_ALL);
echo "Before config load\n";
require_once __DIR__ . '/../config/config_enhanced.php';
echo "After config load\n";
require_once __DIR__ . '/../classes/Database.php';
echo "After DB load\n";
require_once __DIR__ . '/../api/webhook.php';
echo "After webhook include\n";
$json = file_get_contents(__DIR__ . '/webhook_payload_ABGGFlA5Fpa.json');
$data = json_decode($json, true);
try {
echo "Calling processPayload...\n";
$w = new WhatsAppWebhook();
$ok = $w->processPayload($data);
echo 'processPayload returned: ' . ($ok ? 'true' : 'false') . PHP_EOL;
} catch (Exception $e) {
echo 'EXCEPTION: ' . $e->getMessage() . PHP_EOL;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/../config/config_enhanced.php';
require_once __DIR__ . '/../classes/Database.php';
require_once __DIR__ . '/../api/webhook.php';
try {
echo "Starting...\n";
$db = Database::getInstance();
echo "Got DB\n";
$row = $db->fetch('SELECT id, request_body FROM webhook_logs ORDER BY created_at DESC LIMIT 1');
echo "Latest log id: " . ($row['id'] ?? 'none') . PHP_EOL;
$payload = json_decode($row['request_body'], true);
echo "Decoded payload keys: " . implode(',', array_keys($payload)) . PHP_EOL;
$w = new WhatsAppWebhook();
echo "Webhook instance created\n";
$res = $w->processPayload($payload);
echo 'ProcessPayload => ' . ($res ? 'OK' : 'FAILED') . PHP_EOL;
} catch (Exception $e) {
echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
}
+1
View File
@@ -0,0 +1 @@
{"object":"whatsapp_business_account","entry":[{"id":"0","changes":[{"field":"messages","value":{"messaging_product":"whatsapp","metadata":{"display_phone_number":"16505551111","phone_number_id":"123456123"},"contacts":[{"profile":{"name":"test user name"},"wa_id":"16315551181"}],"messages":[{"from":"16315551181","id":"ABGGFlA5Fpa","timestamp":"1504902988","type":"text","text":{"body":"this is a text message"}}]}}]}]}
+197
View File
@@ -0,0 +1,197 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Webhook Logs - Tiempo Real</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
pre.json { max-height: 60vh; overflow: auto; background: #f8f9fa; padding: 1rem; border-radius: 6px; }
.new-row { animation: highlight 1.5s ease; }
@keyframes highlight { from { background: #e6ffed } to { background: transparent } }
</style>
</head>
<body>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3><i class="fab fa-whatsapp text-success"></i> Webhook Logs (Tiempo Real)</h3>
<div>
<input id="searchInput" class="form-control d-inline-block" style="width:260px" placeholder="Buscar ID o texto...">
</div>
</div>
<div class="mb-2">
<button class="btn btn-sm btn-outline-secondary" id="refreshBtn">Actualizar</button>
<label class="ms-3">Auto-refresh: <input id="autoRefresh" type="checkbox" checked></label>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body p-2">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Timestamp</th>
<th>Snippet</th>
<th>IP</th>
<th>Acción</th>
</tr>
</thead>
<tbody id="logsTable">
<tr><td colspan="5" class="text-center py-4 text-muted">Cargando...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5>Payload (crudo)</h5>
<pre id="rawPayload" class="json">Selecciona un registro para ver el payload completo</pre>
<div class="mt-2 d-flex gap-2">
<button id="downloadBtn" class="btn btn-sm btn-outline-primary" disabled>Descargar JSON</button>
<button id="replayBtn" class="btn btn-sm btn-outline-success" disabled>Reproducir (Simular)</button>
</div>
</div>
</div>
</div>
</div>
<div class="mt-3 text-muted">Nota: Esta vista muestra los webhooks crudos como llegaron y se actualiza cada 3s (auto-refresh).</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js"></script>
<script>
const API_LIST = 'api/get_webhook_logs.php?debug=true';
const API_GET = 'api/get_webhook_log.php?debug=true';
let auto = true;
let latestId = 0;
let pollingInterval = 3000;
let lastSelected = null;
async function fetchLogs(sinceId = 0, search = '') {
const params = new URLSearchParams();
if (sinceId) params.set('since_id', sinceId);
if (search) params.set('search', search);
params.set('limit', 30);
const res = await fetch(API_LIST + '&' + params.toString());
return await res.json();
}
async function refresh(full = true) {
const search = document.getElementById('searchInput').value.trim();
try {
const data = await fetchLogs(full ? 0 : latestId, search);
if (!data.success) { console.error(data); return; }
const rows = data.data || [];
if (!full && rows.length) {
// append new rows
prependRows(rows.reverse()); // since API returns ascending when since_id used
} else {
renderFullTable(rows);
}
if (rows.length) {
latestId = Math.max(...rows.map(r => r.id), latestId);
}
} catch (e) { console.error(e); }
}
function renderFullTable(rows) {
const tbody = document.getElementById('logsTable');
if (!rows || rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No hay registros</td></tr>';
return;
}
tbody.innerHTML = rows.map(r => rowHtml(r)).join('');
}
function prependRows(rows) {
const tbody = document.getElementById('logsTable');
rows.forEach(r => {
const tr = document.createElement('tr');
tr.innerHTML = rowHtml(r);
tr.classList.add('new-row');
tbody.insertBefore(tr, tbody.firstChild);
});
}
function rowHtml(r) {
const snippet = (r.snippet || '').replace(/</g, '&lt;').substring(0, 120);
return `
<tr data-id="${r.id}" onclick="selectLog(${r.id}, this)">
<td>${r.id}</td>
<td>${r.created_at}</td>
<td><small class="text-muted">${snippet}</small></td>
<td>${r.ip_address || ''}</td>
<td><button class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation(); viewRaw(${r.id})">Ver</button></td>
</tr>
`;
}
async function viewRaw(id) {
const res = await fetch(API_GET + '&id=' + id);
const data = await res.json();
if (!data.success) { alert(data.error || 'Error'); return; }
const payload = data.data;
lastSelected = payload;
document.getElementById('rawPayload').textContent = payload.request_body_pretty || payload.request_body || '';
document.getElementById('downloadBtn').disabled = false;
document.getElementById('replayBtn').disabled = false;
}
function selectLog(id, el) {
// highlight
const rows = document.querySelectorAll('#logsTable tr');
rows.forEach(r => r.classList.remove('table-active'));
el.classList.add('table-active');
viewRaw(id);
}
function downloadSelected() {
if (!lastSelected) return;
const blob = new Blob([lastSelected.request_body || ''], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `webhook_${lastSelected.id}.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
async function replaySelected() {
if (!lastSelected) return alert('Selecciona un registro primero');
try {
const payload = lastSelected.request_body;
const resp = await fetch('api/webhook_replay.php', {method: 'POST', headers: {'Content-Type':'application/json'}, body: payload});
const data = await resp.json();
alert(data.success ? 'Reproducción ejecutada' : ('Error: ' + (data.error || '')));
} catch (e) {
alert('Error reproduciendo: ' + e.message);
}
}
// Event listeners
document.getElementById('refreshBtn').addEventListener('click', () => refresh(true));
document.getElementById('autoRefresh').addEventListener('change', (e) => { auto = e.target.checked; });
document.getElementById('downloadBtn').addEventListener('click', downloadSelected);
document.getElementById('replayBtn').addEventListener('click', replaySelected);
document.getElementById('searchInput').addEventListener('input', (e) => { setTimeout(()=>refresh(true), 350); });
// Polling loop
setInterval(() => { if (!auto) return; refresh(false); }, pollingInterval);
// Initial load
refresh(true);
</script>
</body>
</html>