up
This commit is contained in:
@@ -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']);
|
||||
}
|
||||
@@ -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
@@ -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']);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"}}]}}]}]}
|
||||
@@ -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, '<').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>
|
||||
Reference in New Issue
Block a user