This commit is contained in:
Lizandro Guarnizo
2026-02-03 21:30:55 -05:00
parent 44423adb32
commit 1aae148396
89 changed files with 8581 additions and 7687 deletions
+52
View File
@@ -0,0 +1,52 @@
<?php
/**
* API - Cambiar contraseña de usuario administrador
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Leer datos del request
$input = json_decode(file_get_contents('php://input'), true);
$userId = $input['user_id'] ?? null;
$newPassword = $input['new_password'] ?? '';
if (!$userId) {
throw new Exception('ID de usuario requerido');
}
if (strlen($newPassword) < 6) {
throw new Exception('La contraseña debe tener al menos 6 caracteres');
}
// Encriptar contraseña
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
// Actualizar contraseña
$db->execute(
"UPDATE admin_users SET password_hash = ? WHERE id = ?",
[$hashedPassword, $userId]
);
echo json_encode([
'success' => true,
'message' => 'Contraseña actualizada correctamente'
]);
} catch (Exception $e) {
error_log('Error changing admin password: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* API - Crear nuevo usuario administrador
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Leer datos del request
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
$fullName = trim($input['full_name'] ?? '');
$email = trim($input['email'] ?? '');
$password = $input['password'] ?? '';
if (empty($username)) {
throw new Exception('El nombre de usuario es requerido');
}
if (strlen($password) < 6) {
throw new Exception('La contraseña debe tener al menos 6 caracteres');
}
// Verificar si el username ya existe
$existing = $db->fetch(
"SELECT id FROM admin_users WHERE username = ?",
[$username]
);
if ($existing) {
throw new Exception('El nombre de usuario ya está en uso');
}
// Encriptar contraseña
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// Insertar usuario
$db->execute(
"INSERT INTO admin_users (username, password_hash, full_name, email, is_active, created_at)
VALUES (?, ?, ?, ?, 1, NOW())",
[$username, $hashedPassword, $fullName, $email]
);
echo json_encode([
'success' => true,
'message' => 'Usuario creado correctamente'
]);
} catch (Exception $e) {
error_log('Error creating admin user: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+193
View File
@@ -0,0 +1,193 @@
<?php
/**
* API - Eliminar mensaje
* Permite eliminar un mensaje de la conversación
* Fecha: 28 de enero de 2026
*/
// Habilitar display de errores para debug
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once '../config/config.php';
// Log para debug
error_log("delete_message.php: Iniciando, session_id=" . session_id() . ", admin_logged_in=" . (isset($_SESSION['admin_logged_in']) ? $_SESSION['admin_logged_in'] : 'no definido'));
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Manejar preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'DELETE') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Método no permitido. Use POST o DELETE.']);
exit;
}
try {
$input = json_decode(file_get_contents('php://input'), true);
// También soportar query params para DELETE
if (!$input && $_SERVER['REQUEST_METHOD'] === 'DELETE') {
$input = [
'message_id' => $_GET['message_id'] ?? $_GET['id'] ?? null,
'delete_for_everyone' => $_GET['delete_for_everyone'] ?? false
];
}
if (!$input) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'JSON inválido']);
exit;
}
$messageId = $input['message_id'] ?? $input['id'] ?? null;
$deleteForEveryone = $input['delete_for_everyone'] ?? false;
$softDelete = $input['soft_delete'] ?? true; // Por defecto, soft delete
if (!$messageId) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'message_id es requerido']);
exit;
}
$db = Database::getInstance();
// Buscar el mensaje
$sql = "SELECT id, user_id, direction, message_type, content, message_id as wa_message_id,
local_file, local_thumb, media_url
FROM conversations
WHERE id = :mid1 OR message_id = :mid2
LIMIT 1";
$message = $db->fetch($sql, ['mid1' => $messageId, 'mid2' => $messageId]);
if (!$message) {
http_response_code(404);
echo json_encode(['success' => false, 'error' => 'Mensaje no encontrado']);
exit;
}
// Solo permitir eliminar mensajes salientes (outgoing) a menos que sea admin
$isAdmin = isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'];
if ($message['direction'] !== 'outgoing' && !$isAdmin) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Solo puedes eliminar tus propios mensajes']);
exit;
}
$deletedMessageId = $message['id'];
$userId = $message['user_id'];
$waMessageId = $message['wa_message_id'];
if ($softDelete) {
// Soft delete: marcar como eliminado pero mantener registro
// Verificar si existen las columnas deleted_at y original_content
$hasDeletedAt = false;
$hasOriginalContent = false;
try {
$columns = $db->fetchAll("SHOW COLUMNS FROM conversations WHERE Field IN ('deleted_at', 'original_content')");
foreach ($columns as $col) {
if ($col['Field'] === 'deleted_at') $hasDeletedAt = true;
if ($col['Field'] === 'original_content') $hasOriginalContent = true;
}
} catch (Exception $e) {
// Ignorar
}
// Construir la query según las columnas disponibles
// IMPORTANTE: Guardar original_content ANTES de modificar content
if ($hasDeletedAt && $hasOriginalContent) {
// Primero guardamos el contenido original
$db->execute("UPDATE conversations SET original_content = content WHERE id = :id AND original_content IS NULL", ['id' => $deletedMessageId]);
// Luego marcamos como eliminado
$updateSql = "UPDATE conversations SET content = '[Mensaje eliminado]', deleted_at = NOW() WHERE id = :id";
} elseif ($hasDeletedAt) {
$updateSql = "UPDATE conversations SET content = '[Mensaje eliminado]', deleted_at = NOW() WHERE id = :id";
} else {
$updateSql = "UPDATE conversations SET content = '[Mensaje eliminado]' WHERE id = :id";
}
$db->execute($updateSql, ['id' => $deletedMessageId]);
error_log("Message soft-deleted: ID={$deletedMessageId}, User={$userId}");
} else {
// Hard delete: eliminar completamente
// Primero, eliminar archivos multimedia asociados si existen
if (!empty($message['local_file'])) {
$filePath = dirname(__DIR__) . '/' . $message['local_file'];
if (file_exists($filePath)) {
@unlink($filePath);
error_log("Deleted media file: {$filePath}");
}
}
if (!empty($message['local_thumb'])) {
$thumbPath = dirname(__DIR__) . '/' . $message['local_thumb'];
if (file_exists($thumbPath)) {
@unlink($thumbPath);
error_log("Deleted thumbnail: {$thumbPath}");
}
}
// Eliminar el mensaje de la base de datos
$deleteSql = "DELETE FROM conversations WHERE id = :id";
$db->execute($deleteSql, ['id' => $deletedMessageId]);
error_log("Message hard-deleted: ID={$deletedMessageId}, User={$userId}");
}
// Intentar notificar por SSE si está disponible
try {
$ssePayload = [
'type' => 'message_deleted',
'user_id' => $userId,
'message_id' => $deletedMessageId,
'wa_message_id' => $waMessageId,
'soft_delete' => $softDelete,
'deleted_at' => date('Y-m-d H:i:s')
];
// Llamar al endpoint push_event interno
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
$ch = curl_init($pushUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'event_type' => 'message_deleted',
'data' => $ssePayload,
'target_user_id' => 'all'
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
curl_exec($ch);
curl_close($ch);
} catch (Exception $sseErr) {
error_log("delete_message: SSE notification failed: " . $sseErr->getMessage());
}
echo json_encode([
'success' => true,
'message' => $softDelete ? 'Mensaje eliminado' : 'Mensaje eliminado permanentemente',
'data' => [
'id' => $deletedMessageId,
'message_id' => $waMessageId,
'soft_delete' => $softDelete,
'deleted_at' => date('Y-m-d H:i:s')
]
]);
} catch (Exception $e) {
error_log('delete_message.php error: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Error interno del servidor', 'debug' => $e->getMessage()]);
}
+55
View File
@@ -0,0 +1,55 @@
<?php
/**
* API - Eliminar mensaje programado
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: DELETE, POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
// Intentar obtener de query params
$id = isset($_GET['id']) ? intval($_GET['id']) : null;
} else {
$id = isset($input['id']) ? intval($input['id']) : null;
}
if (!$id) {
throw new Exception('ID de mensaje programado requerido');
}
$db = Database::getInstance();
// Verificar que existe
$existing = $db->fetch("SELECT * FROM scheduled_messages WHERE id = ?", [$id]);
if (!$existing) {
throw new Exception('Mensaje programado no encontrado');
}
// Eliminar
$db->execute("DELETE FROM scheduled_messages WHERE id = ?", [$id]);
writeLog('INFO', "Mensaje programado eliminado: ID={$id}");
echo json_encode([
'success' => true,
'message' => 'Mensaje programado eliminado exitosamente'
]);
} catch (Exception $e) {
error_log("Error in delete_scheduled_message.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+160
View File
@@ -0,0 +1,160 @@
<?php
/**
* API - Editar mensaje
* Permite editar el contenido de un mensaje enviado (solo mensajes propios/outgoing)
* Fecha: 28 de enero de 2026
*/
// Habilitar display de errores para debug
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once '../config/config.php';
// Log para debug
error_log("edit_message.php: Iniciando, session_id=" . session_id() . ", admin_logged_in=" . (isset($_SESSION['admin_logged_in']) ? $_SESSION['admin_logged_in'] : 'no definido'));
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');
// Manejar preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Método no permitido. Use POST.']);
exit;
}
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'JSON inválido']);
exit;
}
$messageId = $input['message_id'] ?? $input['id'] ?? null;
$newContent = $input['content'] ?? $input['new_content'] ?? null;
if (!$messageId) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'message_id es requerido']);
exit;
}
if ($newContent === null || trim($newContent) === '') {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'El contenido no puede estar vacío']);
exit;
}
$db = Database::getInstance();
// Buscar el mensaje
$sql = "SELECT id, user_id, direction, message_type, content, message_id as wa_message_id
FROM conversations
WHERE id = :mid1 OR message_id = :mid2
LIMIT 1";
$message = $db->fetch($sql, ['mid1' => $messageId, 'mid2' => $messageId]);
if (!$message) {
http_response_code(404);
echo json_encode(['success' => false, 'error' => 'Mensaje no encontrado']);
exit;
}
// Solo permitir editar mensajes salientes (outgoing)
if ($message['direction'] !== 'outgoing') {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Solo puedes editar tus propios mensajes']);
exit;
}
// Solo permitir editar mensajes de texto
if ($message['message_type'] !== 'text' && $message['message_type'] !== null) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Solo se pueden editar mensajes de texto']);
exit;
}
// Guardar contenido original para historial (opcional)
$originalContent = $message['content'];
// Verificar si existe la columna edited_at
$hasEditedAt = false;
try {
$columns = $db->fetchAll("SHOW COLUMNS FROM conversations WHERE Field = 'edited_at'");
$hasEditedAt = !empty($columns);
} catch (Exception $e) {
// Ignorar
}
// Actualizar el mensaje en la base de datos
if ($hasEditedAt) {
$updateSql = "UPDATE conversations SET content = :content, edited_at = NOW() WHERE id = :id";
} else {
$updateSql = "UPDATE conversations SET content = :content WHERE id = :id";
}
$db->execute($updateSql, [
'content' => trim($newContent),
'id' => $message['id']
]);
// Registrar la edición en el log
error_log("Message edited: ID={$message['id']}, User={$message['user_id']}, Original='{$originalContent}', New='{$newContent}'");
// Intentar notificar por SSE si está disponible
try {
$ssePayload = [
'type' => 'message_edited',
'user_id' => $message['user_id'],
'message_id' => $message['id'],
'wa_message_id' => $message['wa_message_id'],
'new_content' => trim($newContent),
'edited_at' => date('Y-m-d H:i:s')
];
// Llamar al endpoint push_event interno
$pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
$ch = curl_init($pushUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'event_type' => 'message_edited',
'data' => $ssePayload,
'target_user_id' => 'all'
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
curl_exec($ch);
curl_close($ch);
} catch (Exception $sseErr) {
error_log("edit_message: SSE notification failed: " . $sseErr->getMessage());
}
echo json_encode([
'success' => true,
'message' => 'Mensaje editado correctamente',
'data' => [
'id' => $message['id'],
'message_id' => $message['wa_message_id'],
'new_content' => trim($newContent),
'edited_at' => date('Y-m-d H:i:s')
]
]);
} catch (Exception $e) {
error_log('edit_message.php error: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Error interno del servidor', 'debug' => $e->getMessage()]);
}
+86 -7
View File
@@ -25,6 +25,9 @@ try {
$limit = max(1, min(200, $limit)); // máximo 200 por página
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$offset = ($page - 1) * $limit;
// Parámetro de búsqueda
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
// Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar
$filter = isset($_GET['filter']) ? strtolower(trim($_GET['filter'])) : 'all';
@@ -47,7 +50,19 @@ try {
lm.local_file AS last_local_file,
lm.local_thumb AS last_local_thumb,
lm.created_at AS last_time,
IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) AS unread_count
IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) AS unread_count";
// Si hay búsqueda, agregar campo para ordenar por relevancia
if (!empty($search)) {
$sql .= ",
(CASE
WHEN u.name LIKE ? THEN 3
WHEN u.phone_number LIKE ? THEN 2
ELSE 1
END) AS relevance";
}
$sql .= "
FROM users u
LEFT JOIN conversations c ON c.user_id = u.id
LEFT JOIN (
@@ -55,7 +70,23 @@ try {
JOIN (
SELECT user_id, MAX(created_at) AS last_time FROM conversations GROUP BY user_id
) t2 ON t1.user_id = t2.user_id AND t1.created_at = t2.last_time
) lm ON lm.user_id = u.id
) lm ON lm.user_id = u.id";
// Agregar condición de búsqueda si existe
if (!empty($search)) {
$sql .= "
WHERE (
u.name LIKE ?
OR u.phone_number LIKE ?
OR EXISTS (
SELECT 1 FROM conversations c2
WHERE c2.user_id = u.id
AND c2.content LIKE ?
)
)";
}
$sql .= "
GROUP BY u.id";
// Aplicar filtro 'unread' si se solicita
@@ -63,15 +94,63 @@ try {
$sql .= " HAVING IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) > 0";
}
$sql .= "\n ORDER BY lm.created_at DESC\n LIMIT %d OFFSET %d";
// Ordenar por relevancia si hay búsqueda, sino por fecha
if (!empty($search)) {
$sql .= "\n ORDER BY relevance DESC, lm.created_at DESC";
} else {
$sql .= "\n ORDER BY lm.created_at DESC";
}
$sql .= "\n LIMIT %d OFFSET %d";
$conversations = $db->fetchAll(sprintf($sql, $limit, $offset));
// Preparar parámetros para la consulta
$params = [];
if (!empty($search)) {
$searchParam = '%' . $search . '%';
// Para el campo de relevancia
$params[] = $searchParam; // name LIKE
$params[] = $searchParam; // phone_number LIKE
// Para el WHERE
$params[] = $searchParam; // name LIKE
$params[] = $searchParam; // phone_number LIKE
$params[] = $searchParam; // content LIKE
}
$finalSql = sprintf($sql, $limit, $offset);
if (!empty($params)) {
$conversations = $db->fetchAll($finalSql, $params);
} else {
$conversations = $db->fetchAll($finalSql);
}
// Conteo total de usuarios con al menos una conversación (útil para paginar)
if ($filter === 'unread') {
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations c WHERE c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)");
if (!empty($search)) {
// Conteo con búsqueda
$searchParam = '%' . $search . '%';
$countSql = "SELECT COUNT(DISTINCT u.id) AS count
FROM users u
LEFT JOIN conversations c ON c.user_id = u.id
WHERE (
u.name LIKE ?
OR u.phone_number LIKE ?
OR EXISTS (
SELECT 1 FROM conversations c2
WHERE c2.user_id = u.id
AND c2.content LIKE ?
)
)";
if ($filter === 'unread') {
$countSql .= " AND c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)";
}
$totalRow = $db->fetch($countSql, [$searchParam, $searchParam, $searchParam]);
} else {
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations");
// Conteo normal sin búsqueda
if ($filter === 'unread') {
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations c WHERE c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0)");
} else {
$totalRow = $db->fetch("SELECT COUNT(DISTINCT user_id) AS count FROM conversations");
}
}
$total = isset($totalRow['count']) ? intval($totalRow['count']) : 0;
+113
View File
@@ -0,0 +1,113 @@
<?php
/**
* API - Obtener mensajes programados
*/
require_once '../config/config.php';
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');
requireAuthentication();
try {
$db = Database::getInstance();
// Parámetros de filtro
$userId = isset($_GET['user_id']) ? intval($_GET['user_id']) : null;
$status = $_GET['status'] ?? null;
$dateFrom = $_GET['date_from'] ?? null;
$dateTo = $_GET['date_to'] ?? null;
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : 100;
$offset = isset($_GET['offset']) ? intval($_GET['offset']) : 0;
// Construir query
$sql = "SELECT * FROM scheduled_messages_view WHERE 1=1";
$params = [];
if ($userId) {
$sql .= " AND user_id = ?";
$params[] = $userId;
}
if ($status) {
$sql .= " AND status = ?";
$params[] = $status;
}
if ($dateFrom) {
$sql .= " AND scheduled_date >= ?";
$params[] = $dateFrom;
}
if ($dateTo) {
$sql .= " AND scheduled_date <= ?";
$params[] = $dateTo;
}
// Ordenar por fecha más cercana primero
$sql .= " ORDER BY scheduled_date ASC, scheduled_time ASC";
// Paginación
$sql .= " LIMIT ? OFFSET ?";
$params[] = $limit;
$params[] = $offset;
$messages = $db->fetchAll($sql, $params);
error_log('📋 get_scheduled_messages.php - Encontrados: ' . count($messages) . ' mensajes');
error_log('🔍 SQL ejecutado: ' . $sql);
error_log('📊 Parámetros: ' . json_encode($params));
// Decodificar parámetros JSON
foreach ($messages as &$message) {
if ($message['template_parameters']) {
$message['template_parameters'] = json_decode($message['template_parameters'], true);
}
}
// Contar total
$countSql = "SELECT COUNT(*) as total FROM scheduled_messages WHERE 1=1";
$countParams = [];
if ($userId) {
$countSql .= " AND user_id = ?";
$countParams[] = $userId;
}
if ($status) {
$countSql .= " AND status = ?";
$countParams[] = $status;
}
if ($dateFrom) {
$countSql .= " AND scheduled_date >= ?";
$countParams[] = $dateFrom;
}
if ($dateTo) {
$countSql .= " AND scheduled_date <= ?";
$countParams[] = $dateTo;
}
$countResult = $db->fetch($countSql, $countParams);
$total = $countResult['total'] ?? 0;
echo json_encode([
'success' => true,
'data' => $messages,
'total' => $total,
'limit' => $limit,
'offset' => $offset
]);
} catch (Exception $e) {
error_log("Error in get_scheduled_messages.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Error obteniendo mensajes programados'
]);
}
+111
View File
@@ -0,0 +1,111 @@
<?php
/**
* API - Obtener detalles completos de una plantilla con variables
* Devuelve los parámetros que se deben llenar
*/
require_once '../config/config.php';
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');
requireAuthentication();
try {
$templateId = isset($_GET['id']) ? intval($_GET['id']) : null;
$templateName = isset($_GET['name']) ? trim($_GET['name']) : null;
$language = isset($_GET['language']) ? trim($_GET['language']) : 'es';
if (!$templateId && !$templateName) {
throw new Exception('Se requiere ID o nombre de plantilla');
}
$db = Database::getInstance();
if ($templateId) {
$template = $db->fetch(
"SELECT * FROM message_templates WHERE id = ?",
[$templateId]
);
} else {
$template = $db->fetch(
"SELECT * FROM message_templates WHERE template_name = ? AND language_code = ?",
[$templateName, $language]
);
}
if (!$template) {
throw new Exception('Plantilla no encontrada');
}
// Extraer variables del body_text o de example_parameters
$bodyText = $template['body_text'] ?? '';
$variables = [];
// Primero intentar usar las variables guardadas en example_parameters
if (!empty($template['example_parameters'])) {
$examples = json_decode($template['example_parameters'], true);
if (isset($examples['variables']) && is_array($examples['variables'])) {
// Usar las variables ya procesadas durante la sincronización
foreach ($examples['variables'] as $var) {
$varName = $var['name'] ?? $var['index'];
$variables[] = [
'index' => $var['index'],
'placeholder' => $var['placeholder'],
'label' => is_numeric($varName) ? 'Variable ' . $varName : ucfirst(str_replace('_', ' ', $varName)),
'required' => true,
'example' => $var['example'] ?? null
];
}
}
}
// Si no hay variables guardadas, extraerlas del body_text (fallback)
if (empty($variables) && preg_match_all('/\{\{([^\}]+)\}\}/', $bodyText, $matches)) {
$uniqueVars = array_unique($matches[1]);
$index = 1;
foreach ($uniqueVars as $varName) {
$varIndex = is_numeric($varName) ? intval($varName) : $index++;
$variables[] = [
'index' => $varIndex,
'placeholder' => '{{' . $varName . '}}',
'label' => is_numeric($varName) ? 'Variable ' . $varName : ucfirst(str_replace('_', ' ', $varName)),
'required' => true,
'example' => null
];
}
}
// Preparar respuesta
$response = [
'success' => true,
'template' => [
'id' => $template['id'],
'name' => $template['name'],
'template_name' => $template['template_name'],
'language_code' => $template['language_code'],
'category' => $template['category'],
'status' => $template['status'],
'body_text' => $bodyText,
'header_text' => $template['header_text'] ?? null,
'header_type' => $template['header_type'] ?? null,
'footer_text' => $template['footer_text'] ?? null,
'has_variables' => count($variables) > 0,
'variables_count' => count($variables),
'variables' => $variables
]
];
echo json_encode($response, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
error_log("Error in get_template_details.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+16 -2
View File
@@ -27,8 +27,22 @@ try {
$approvedOnly = isset($_GET['approved_only']) && $_GET['approved_only'] == '1';
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : null;
// Ajuste: some DBs may not have body_text column; omit it to avoid SQL errors
$sql = "SELECT id, name, template_name, language_code, category, status, created_at FROM message_templates";
// Incluir todos los campos necesarios para la UI
$sql = "SELECT
id,
name,
template_name,
language_code,
category,
status,
body_text,
header_text,
header_type,
footer_text,
components,
example_parameters,
created_at
FROM message_templates";
$params = [];
if ($approvedOnly) {
+41 -5
View File
@@ -24,8 +24,30 @@ header('Access-Control-Allow-Headers: Content-Type');
try {
$db = Database::getInstance();
$users = $db->fetchAll(
"SELECT
// Parámetros de paginación
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$limit = isset($_GET['limit']) ? max(1, min(100, intval($_GET['limit']))) : 20;
$offset = ($page - 1) * $limit;
// Búsqueda opcional
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$whereClause = '';
$params = [];
if (!empty($search)) {
$whereClause = "WHERE u.phone_number LIKE ? OR u.name LIKE ? OR u.email LIKE ?";
$searchParam = "%{$search}%";
$params = [$searchParam, $searchParam, $searchParam];
}
// Contar total de registros
$countQuery = "SELECT COUNT(*) as total FROM users u {$whereClause}";
$totalResult = $db->fetch($countQuery, $params);
$total = $totalResult['total'];
$totalPages = ceil($total / $limit);
// Obtener usuarios paginados
$query = "SELECT
u.id,
u.phone_number,
u.name,
@@ -38,12 +60,26 @@ try {
m.name as current_menu
FROM users u
LEFT JOIN menus m ON u.current_menu_id = m.id
ORDER BY u.created_at DESC"
);
{$whereClause}
ORDER BY u.created_at DESC
LIMIT ? OFFSET ?";
$params[] = $limit;
$params[] = $offset;
$users = $db->fetchAll($query, $params);
echo json_encode([
'success' => true,
'data' => $users
'data' => $users,
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'totalPages' => $totalPages,
'hasNext' => $page < $totalPages,
'hasPrev' => $page > 1
]
]);
} catch (Exception $e) {
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* API - Listar usuarios administradores del sistema
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Obtener todos los usuarios administradores
$users = $db->fetchAll("
SELECT
id,
username,
full_name,
email,
is_active,
last_login,
created_at
FROM admin_users
ORDER BY created_at DESC
");
echo json_encode([
'success' => true,
'data' => $users
]);
} catch (Exception $e) {
error_log('Error listing admin users: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Error al obtener usuarios: ' . $e->getMessage()
]);
}
+130
View File
@@ -0,0 +1,130 @@
<?php
/**
* API - Generar vista previa de plantilla con parámetros
* Reemplaza las variables y muestra cómo se verá el mensaje final
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Datos inválidos');
}
$templateId = $input['template_id'] ?? null;
$parameters = $input['parameters'] ?? [];
if (!$templateId) {
throw new Exception('Se requiere ID de plantilla');
}
$db = Database::getInstance();
$template = $db->fetch(
"SELECT * FROM message_templates WHERE id = ?",
[$templateId]
);
if (!$template) {
throw new Exception('Plantilla no encontrada');
}
// Obtener textos de la plantilla
$bodyText = $template['body_text'] ?? '';
$headerText = $template['header_text'] ?? null;
$footerText = $template['footer_text'] ?? null;
// Obtener variables guardadas para saber sus placeholders
$exampleParams = json_decode($template['example_parameters'] ?? '{}', true);
$savedVariables = $exampleParams['variables'] ?? [];
// Reemplazar variables en el body
$bodyPreview = $bodyText;
if (is_array($parameters)) {
// Si tenemos variables guardadas, usar sus placeholders
if (!empty($savedVariables)) {
foreach ($savedVariables as $var) {
$index = $var['index'] - 1; // Convertir a índice de array (0-based)
if (isset($parameters[$index])) {
$placeholder = $var['placeholder']; // Ejemplo: {{nombre_tema}} o {{1}}
$bodyPreview = str_replace($placeholder, $parameters[$index], $bodyPreview);
}
}
} else {
// Fallback: reemplazar variables numéricas estándar
foreach ($parameters as $index => $value) {
$placeholder = '{{' . ($index + 1) . '}}';
$bodyPreview = str_replace($placeholder, $value, $bodyPreview);
}
}
}
// Reemplazar variables en el header si existen
$headerPreview = $headerText;
if ($headerText && is_array($parameters)) {
if (!empty($savedVariables)) {
foreach ($savedVariables as $var) {
$index = $var['index'] - 1;
if (isset($parameters[$index])) {
$placeholder = $var['placeholder'];
$headerPreview = str_replace($placeholder, $parameters[$index], $headerPreview);
}
}
} else {
foreach ($parameters as $index => $value) {
$placeholder = '{{' . ($index + 1) . '}}';
$headerPreview = str_replace($placeholder, $value, $headerPreview);
}
}
}
// Verificar si quedan variables sin reemplazar (tanto numéricas como con nombres)
$hasUnreplacedVars = preg_match('/\{\{[^\}]+\}\}/', $bodyPreview);
// Generar vista previa HTML
$htmlPreview = '';
if ($headerPreview) {
$htmlPreview .= '<div class="template-header" style="font-weight: bold; margin-bottom: 8px; color: #075e54;">';
$htmlPreview .= htmlspecialchars($headerPreview);
$htmlPreview .= '</div>';
}
$htmlPreview .= '<div class="template-body" style="margin-bottom: 8px;">';
$htmlPreview .= nl2br(htmlspecialchars($bodyPreview));
$htmlPreview .= '</div>';
if ($footerText) {
$htmlPreview .= '<div class="template-footer" style="font-size: 0.85em; color: #667781; margin-top: 8px;">';
$htmlPreview .= htmlspecialchars($footerText);
$htmlPreview .= '</div>';
}
echo json_encode([
'success' => true,
'preview' => [
'body' => $bodyPreview,
'header' => $headerPreview,
'footer' => $footerText,
'html' => $htmlPreview,
'has_unreplaced_variables' => $hasUnreplacedVars,
'is_complete' => !$hasUnreplacedVars
]
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
error_log("Error in preview_template.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+150
View File
@@ -0,0 +1,150 @@
<?php
/**
* API - Crear mensaje programado / recordatorio
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$input = json_decode(file_get_contents('php://input'), true);
error_log('📥 schedule_message.php - Input recibido: ' . json_encode($input));
if (!$input) {
throw new Exception('Datos inválidos');
}
// Validar campos requeridos
$userId = isset($input['user_id']) ? intval($input['user_id']) : null;
$messageType = $input['message_type'] ?? 'template';
$scheduledDate = $input['scheduled_date'] ?? null;
$scheduledTime = $input['scheduled_time'] ?? '09:00';
if (!$userId) {
throw new Exception('ID de usuario requerido');
}
if (!$scheduledDate) {
throw new Exception('Fecha programada requerida');
}
// Validar que la fecha no sea en el pasado
$scheduledDateTime = new DateTime($scheduledDate . ' ' . $scheduledTime);
$now = new DateTime();
if ($scheduledDateTime < $now) {
throw new Exception('La fecha programada no puede ser en el pasado');
}
$db = Database::getInstance();
// Verificar que el usuario existe
$user = $db->fetch("SELECT * FROM users WHERE id = ?", [$userId]);
if (!$user) {
throw new Exception('Usuario no encontrado');
}
// Preparar datos según el tipo de mensaje
$templateId = null;
$templateName = null;
$templateLanguage = 'es';
$templateParameters = null;
$messageContent = null;
if ($messageType === 'template') {
$templateId = isset($input['template_id']) ? intval($input['template_id']) : null;
$templateName = $input['template_name'] ?? null;
$templateLanguage = $input['template_language'] ?? 'es';
$templateParameters = $input['template_parameters'] ?? null;
error_log('🔍 template_parameters recibido: ' . json_encode($templateParameters));
error_log('🔍 Es array? ' . (is_array($templateParameters) ? 'Sí' : 'No'));
if (!$templateId && !$templateName) {
throw new Exception('Se requiere ID o nombre de plantilla');
}
// Si se proporcionaron parámetros, convertir a JSON
if ($templateParameters && is_array($templateParameters)) {
$templateParameters = json_encode($templateParameters, JSON_UNESCAPED_UNICODE);
error_log('✅ Convertido a JSON: ' . $templateParameters);
} else {
error_log('⚠️ No se convirtió - es null o no es array');
}
} else if ($messageType === 'text') {
$messageContent = $input['message_content'] ?? null;
if (empty($messageContent)) {
throw new Exception('Contenido del mensaje requerido');
}
} else {
throw new Exception('Tipo de mensaje no soportado');
}
// Obtener ID del admin actual
$createdBy = $_SESSION['admin_user']['id'] ?? $_SESSION['user_id'] ?? null;
// Insertar mensaje programado
$db->execute(
"INSERT INTO scheduled_messages (
user_id,
template_id,
template_name,
template_language,
template_parameters,
message_type,
message_content,
scheduled_date,
scheduled_time,
created_by,
status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')",
[
$userId,
$templateId,
$templateName,
$templateLanguage,
$templateParameters,
$messageType,
$messageContent,
$scheduledDate,
$scheduledTime,
$createdBy
]
);
$scheduledId = $db->lastInsertId();
error_log('✅ Mensaje programado guardado con ID: ' . $scheduledId);
// Log
writeLog('INFO', "Mensaje programado creado: ID={$scheduledId}, Usuario={$userId}, Fecha={$scheduledDate} {$scheduledTime}");
echo json_encode([
'success' => true,
'message' => 'Recordatorio programado exitosamente',
'data' => [
'id' => $scheduledId,
'scheduled_date' => $scheduledDate,
'scheduled_time' => $scheduledTime,
'user_name' => $user['name'],
'phone_number' => $user['phone_number']
]
]);
} catch (Exception $e) {
error_log("Error in schedule_message.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+190 -28
View File
@@ -23,55 +23,214 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['message'])) {
if (!$input) {
http_response_code(400);
echo json_encode(['error' => 'Mensaje requerido']);
echo json_encode(['error' => 'Datos requeridos']);
exit;
}
$filter = $input['filter'] ?? 'all';
$message = $input['message'];
$selectionType = $input['selection_type'] ?? 'filter';
$messageType = $input['message_type'] ?? 'text';
$db = Database::getInstance();
$whatsappService = new WhatsAppService();
// Construir query según filtro
$whereClause = "WHERE u.status = 'active'";
// Obtener usuarios según tipo de selección
$users = [];
switch ($filter) {
case 'active':
$whereClause .= " AND EXISTS (
SELECT 1 FROM conversations c
WHERE c.user_id = u.id
AND c.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
)";
break;
case 'recent':
$whereClause .= " AND u.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)";
break;
if ($selectionType === 'specific') {
// Usuarios específicos seleccionados
if (!isset($input['user_ids']) || !is_array($input['user_ids']) || empty($input['user_ids'])) {
http_response_code(400);
echo json_encode(['error' => 'Debes seleccionar al menos un usuario']);
exit;
}
$userIds = array_map('intval', $input['user_ids']);
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
$query = "SELECT id, phone_number, name FROM users WHERE id IN ($placeholders) ORDER BY id ASC";
$users = $db->fetchAll($query, $userIds);
} else {
// Por filtro
$filter = $input['filter'] ?? 'all';
$whereClause = "1=1";
switch ($filter) {
case 'active':
$whereClause .= " AND EXISTS (
SELECT 1 FROM conversations c
WHERE c.user_id = u.id
AND c.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
)";
break;
case 'recent':
$whereClause .= " AND u.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)";
break;
case 'all':
// Incluir todos los usuarios
break;
}
$query = "SELECT id, phone_number, name FROM users u WHERE " . $whereClause . " ORDER BY id ASC";
$users = $db->fetchAll($query);
}
// Obtener usuarios según filtro
$users = $db->fetchAll(
"SELECT phone_number FROM users u " . $whereClause
);
if (empty($users)) {
http_response_code(404);
echo json_encode([
'error' => 'No se encontraron usuarios para enviar',
'selection_type' => $selectionType
]);
exit;
}
$sentCount = 0;
$errorCount = 0;
$errors = [];
foreach ($users as $user) {
try {
$response = $whatsappService->sendTextMessage($user['phone_number'], $message);
if ($response) {
$sentCount++;
$response = null;
$messageContent = '';
// Enviar según tipo de mensaje
if ($messageType === 'template') {
// Enviar plantilla
if (!isset($input['template_id'])) {
throw new Exception('ID de plantilla requerido');
}
$templateId = intval($input['template_id']);
$template = $db->fetch("SELECT * FROM message_templates WHERE id = ?", [$templateId]);
if (!$template) {
throw new Exception('Plantilla no encontrada');
}
$status = strtoupper($template['status']);
if ($status !== 'APPROVED') {
throw new Exception('La plantilla no está aprobada. Status: ' . $template['status']);
}
// Preparar variables
$templateVariables = $input['template_variables'] ?? [];
error_log('📝 Variables recibidas: ' . json_encode($templateVariables));
error_log('🔍 Tipo: ' . gettype($templateVariables));
// Detectar si son variables numéricas o con nombres
$isNumericVars = false;
$isNamedVars = false;
if (is_array($templateVariables) && !empty($templateVariables)) {
$firstKey = array_key_first($templateVariables);
if (is_int($firstKey) && isset($templateVariables[0])) {
// Array indexado numéricamente [0 => "val1", 1 => "val2"]
$isNumericVars = true;
error_log('✅ Variables tipo: Array numérico indexado');
} elseif (is_string($firstKey) && !is_numeric($firstKey)) {
// Array asociativo con nombres ["nombre_tema" => "val1", "fecha" => "val2"]
$isNamedVars = true;
error_log('✅ Variables tipo: Objeto con nombres (asociativo)');
} elseif (is_numeric($firstKey)) {
// Array con claves numéricas [1 => "val1", 2 => "val2"]
$isNumericVars = true;
error_log('✅ Variables tipo: Array con claves numéricas');
}
}
// Procesar según el tipo
$processedVariables = [];
if ($isNamedVars) {
// Variables con nombres: pasar como objeto asociativo
$processedVariables = $templateVariables;
error_log('📦 Enviando como objeto con nombres: ' . json_encode($processedVariables));
} elseif ($isNumericVars) {
// Variables numéricas: ordenar y convertir a array indexado
ksort($templateVariables, SORT_NUMERIC);
foreach ($templateVariables as $value) {
$processedVariables[] = $value;
}
error_log('📊 Enviando como array ordenado: ' . json_encode($processedVariables));
} else {
error_log('⚠️ Sin variables o formato desconocido');
}
error_log('📤 Variables procesadas: ' . json_encode($processedVariables));
// Verificar variables si la plantilla usa formato numérico
$expectedVars = preg_match_all('/\{\{(\d+)\}\}/', $template['body_text'] ?? '', $matches);
if ($expectedVars > 0 && $isNumericVars && count($processedVariables) !== $expectedVars) {
throw new Exception("La plantilla requiere {$expectedVars} variable(s) numéricas, pero se enviaron " . count($processedVariables));
}
$response = $whatsappService->sendTemplateMessage(
$user['phone_number'],
$template['template_name'] ?? $template['name'],
$template['language_code'] ?? 'es',
$processedVariables
);
// Construir contenido para BD (reemplazar variables en body_text)
$messageContent = $template['body_text'] ?? $template['name'];
if ($isNamedVars) {
// Reemplazar por nombres
foreach ($templateVariables as $key => $value) {
$messageContent = str_replace('{{' . $key . '}}', $value, $messageContent);
}
} else {
// Reemplazar por índice numérico
foreach ($processedVariables as $index => $value) {
$messageContent = str_replace('{{' . ($index + 1) . '}}', $value, $messageContent);
}
}
} else {
// Enviar texto simple
$message = trim($input['message'] ?? '');
if (empty($message)) {
throw new Exception('Mensaje vacío');
}
$response = $whatsappService->sendTextMessage($user['phone_number'], $message);
$messageContent = $message;
}
// Pequeña pausa para no saturar la API
usleep(100000); // 0.1 segundo
if ($response && isset($response['success']) && $response['success']) {
$sentCount++;
// Guardar mensaje en BD
$db->insert('conversations', [
'user_id' => $user['id'],
'message_id' => $response['message_id'] ?? null,
'content' => $messageContent,
'direction' => 'outgoing',
'message_type' => $messageType === 'template' ? 'template' : 'text',
'status' => 'sent',
'created_at' => date('Y-m-d H:i:s')
]);
} else {
$errorCount++;
$errors[] = [
'phone' => $user['phone_number'],
'error' => 'Respuesta inválida de WhatsApp'
];
}
// Pausa para no saturar la API (250ms entre mensajes)
usleep(250000);
} catch (Exception $e) {
$errorCount++;
$errors[] = [
'phone' => $user['phone_number'],
'name' => $user['name'] ?? 'Sin nombre',
'error' => $e->getMessage()
];
error_log("Error sending broadcast to {$user['phone_number']}: " . $e->getMessage());
}
}
@@ -80,7 +239,10 @@ try {
'success' => true,
'sent_count' => $sentCount,
'error_count' => $errorCount,
'total_users' => count($users)
'total_users' => count($users),
'selection_type' => $selectionType,
'message_type' => $messageType,
'errors' => $errorCount > 0 ? $errors : null
]);
} catch (Exception $e) {
+24
View File
@@ -239,3 +239,27 @@
[2026-01-29 11:39:41] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
[2026-01-29 11:39:41] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
[2026-01-29 11:39:41] Saving to DB - content: record_1769704778262.webm, media_url: 906365935662402, local_file: uploads/media_697b8d4b88a6c2.66006027.ogg, local_thumb: null
[2026-02-02 21:43:43] Raw input: {"recipient":"573168950803","media_url":"https://bot.u-s.app/uploads/media_698160df42ea78.64430762.ogg","media_type":"audio","caption":null,"filename":"record_1770086620567.webm","is_voice":true}
[2026-02-02 21:43:43] Input decoded: {"recipient":"573168950803","media_url":"https:\/\/bot.u-s.app\/uploads\/media_698160df42ea78.64430762.ogg","media_type":"audio","caption":null,"filename":"record_1770086620567.webm","is_voice":true}
[2026-02-02 21:43:43] is_voice: true
[2026-02-02 21:43:44] Upload result: {"id":"1591364868775705"}
[2026-02-02 21:43:44] Media ID obtained: 1591364868775705
[2026-02-02 21:43:45] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMDhGRTNDMUQ5NEFGQTRBQ0FDAA=="}]}
[2026-02-02 21:43:45] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMDhGRTNDMUQ5NEFGQTRBQ0FDAA=="}]}
[2026-02-02 21:43:45] Saving to DB - content: record_1770086620567.webm, media_url: 1591364868775705, local_file: uploads/media_698160df42ea78.64430762.ogg, local_thumb: null
[2026-02-03 08:52:11] Raw input: {"recipient":"573168950803","media_url":"https://bot.u-s.app/uploads/media_6981fd8bb34da0.05106104.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
[2026-02-03 08:52:11] Input decoded: {"recipient":"573168950803","media_url":"https:\/\/bot.u-s.app\/uploads\/media_6981fd8bb34da0.05106104.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
[2026-02-03 08:52:11] is_voice: false
[2026-02-03 08:52:12] Upload result: {"id":"817602961329813"}
[2026-02-03 08:52:12] Media ID obtained: 817602961329813
[2026-02-03 08:52:13] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMUVGMkUzOTNCNjBENTE4OEI1AA=="}]}
[2026-02-03 08:52:13] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSMUVGMkUzOTNCNjBENTE4OEI1AA=="}]}
[2026-02-03 08:52:13] Saving to DB - content: 20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx, media_url: 817602961329813, local_file: uploads/media_6981fd8bb34da0.05106104.xlsx, local_thumb: null
[2026-02-03 16:20:01] Raw input: {"recipient":"573022548060","media_url":"https://bot.u-s.app/uploads/media_69826681178109.48762271.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
[2026-02-03 16:20:01] Input decoded: {"recipient":"573022548060","media_url":"https:\/\/bot.u-s.app\/uploads\/media_69826681178109.48762271.xlsx","media_type":"document","caption":null,"filename":"20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx","is_voice":false}
[2026-02-03 16:20:01] is_voice: false
[2026-02-03 16:20:02] Upload result: {"id":"876532491851564"}
[2026-02-03 16:20:02] Media ID obtained: 876532491851564
[2026-02-03 16:20:02] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573022548060","wa_id":"573022548060"}],"messages":[{"id":"wamid.HBgMNTczMDIyNTQ4MDYwFQIAERgSMjAzNDlFQTVENzA2OEQ5QzkwAA=="}]}
[2026-02-03 16:20:02] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573022548060","wa_id":"573022548060"}],"messages":[{"id":"wamid.HBgMNTczMDIyNTQ4MDYwFQIAERgSMjAzNDlFQTVENzA2OEQ5QzkwAA=="}]}
[2026-02-03 16:20:02] Saving to DB - content: 20260122213820_CONSOLIDADO FERTILIZACION DESDE EL 2025-01-22 HASTA EL 2026-01-22.xlsx, media_url: 876532491851564, local_file: uploads/media_69826681178109.48762271.xlsx, local_thumb: null
+10 -3
View File
@@ -163,10 +163,17 @@ try {
echo json_encode(['error' => 'Mensaje requerido']);
exit;
}
// Procesar saltos de línea escapados (\\n o \n) -> convertir a saltos reales
$message = $input['message'];
$message = str_replace('\\n', "\n", $message); // \n escapado literal
$message = preg_replace('/(?<!\\\\)\\\\n/', "\n", $message); // \n en string
// DEBUG: Mostrar datos de envío de mensaje de texto
error_log("=== DEBUG WHATSAPP TEXT MESSAGE ===");
error_log("Recipient: " . $recipient);
error_log("Message: " . $input['message']);
error_log("Message original: " . $input['message']);
error_log("Message processed: " . $message);
error_log("Method: sendTextMessage()/sendTextReply()");
error_log("====================================");
@@ -175,9 +182,9 @@ try {
if (!empty($input['reply_to'])) {
$replyTo = $input['reply_to'];
$response = $whatsappService->sendTextReply($recipient, $replyTo, $input['message'], $operatorId ? ['operator_id' => $operatorId] : null);
$response = $whatsappService->sendTextReply($recipient, $replyTo, $message, $operatorId ? ['operator_id' => $operatorId] : null);
} else {
$response = $whatsappService->sendTextMessage($recipient, $input['message'], $operatorId ? ['operator_id' => $operatorId] : null);
$response = $whatsappService->sendTextMessage($recipient, $message, $operatorId ? ['operator_id' => $operatorId] : null);
}
break;
+266
View File
@@ -0,0 +1,266 @@
<?php
/**
* API - Sincronizar plantillas desde Facebook/WhatsApp Business API
* Obtiene las plantillas desde la API de WhatsApp y las guarda en la base de datos
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$db = Database::getInstance();
// Obtener configuración de WhatsApp
$token = getConfigFromDB('whatsapp_token', '');
$wabaId = getConfigFromDB('whatsapp_business_account_id', '');
if (empty($token)) {
throw new Exception('Token de WhatsApp no configurado');
}
if (empty($wabaId)) {
throw new Exception('Business Account ID no configurado. Configure el WABA ID en la configuración del sistema.');
}
// Hacer petición a la API de WhatsApp para obtener plantillas
$url = "https://graph.facebook.com/v21.0/{$wabaId}/message_templates";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Content-Type: application/json"
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new Exception("Error de conexión: {$error}");
}
if ($httpCode !== 200) {
$errorData = json_decode($response, true);
$errorMsg = $errorData['error']['message'] ?? 'Error desconocido';
throw new Exception("Error de API ({$httpCode}): {$errorMsg}");
}
$data = json_decode($response, true);
// LOG: Ver respuesta completa de WhatsApp
error_log('📥 Respuesta de WhatsApp API: ' . substr($response, 0, 2000));
error_log('📊 Total de plantillas recibidas: ' . count($data['data'] ?? []));
if (!isset($data['data']) || !is_array($data['data'])) {
throw new Exception('Respuesta inválida de la API de WhatsApp');
}
$templates = $data['data'];
// LOG: Ver primera plantilla como ejemplo
if (!empty($templates)) {
error_log('📋 Ejemplo de plantilla recibida: ' . json_encode($templates[0], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
$syncedCount = 0;
$updatedCount = 0;
$skippedCount = 0;
$errors = [];
foreach ($templates as $template) {
try {
$templateName = $template['name'] ?? '';
$language = $template['language'] ?? 'es';
$status = $template['status'] ?? 'pending';
$category = $template['category'] ?? 'UTILITY';
$components = $template['components'] ?? [];
if (empty($templateName)) {
$skippedCount++;
continue;
}
// Extraer información de los componentes
$bodyText = null;
$headerText = null;
$headerType = null;
$footerText = null;
$exampleParameters = [];
foreach ($components as $component) {
$type = $component['type'] ?? '';
if ($type === 'BODY') {
$bodyText = $component['text'] ?? null;
// Extraer ejemplos de parámetros del body
if (isset($component['example']['body_text'])) {
$exampleParameters['body'] = $component['example']['body_text'];
}
} elseif ($type === 'HEADER') {
$headerText = $component['text'] ?? null;
$headerType = strtolower($component['format'] ?? 'text');
// Extraer ejemplos del header
if (isset($component['example']['header_text'])) {
$exampleParameters['header'] = $component['example']['header_text'];
}
} elseif ($type === 'FOOTER') {
$footerText = $component['text'] ?? null;
}
}
// NUEVO: Extraer variables automáticamente del body_text
$variables = [];
if ($bodyText) {
// Buscar tanto variables numéricas {{1}} como con nombres {{nombre_tema}}
preg_match_all('/\{\{([^\}]+)\}\}/', $bodyText, $matches);
if (!empty($matches[1])) {
$uniqueVars = array_unique($matches[1]);
error_log("🔍 Template '{$templateName}' - Variables encontradas: " . json_encode($matches[1]));
$index = 1;
foreach ($uniqueVars as $varName) {
$example = null;
// Si la variable es numérica, usar su índice
if (is_numeric($varName)) {
$varIndex = (int)$varName;
// Los ejemplos de WhatsApp vienen como array de arrays: [["valor"]]
if (isset($exampleParameters['body'][$varIndex - 1])) {
$exampleData = $exampleParameters['body'][$varIndex - 1];
$example = is_array($exampleData) ? $exampleData[0] : $exampleData;
}
} else {
// Para variables con nombre, usar índice secuencial
$varIndex = $index;
if (isset($exampleParameters['body'][$index - 1])) {
$exampleData = $exampleParameters['body'][$index - 1];
$example = is_array($exampleData) ? $exampleData[0] : $exampleData;
}
$index++;
}
$variables[] = [
'index' => $varIndex,
'placeholder' => "{{" . $varName . "}}",
'name' => $varName,
'example' => $example
];
}
error_log("✅ Variables procesadas para '{$templateName}': " . json_encode($variables, JSON_UNESCAPED_UNICODE));
}
}
// Si encontramos variables, agregarlas a example_parameters
if (!empty($variables)) {
$exampleParameters['variables'] = $variables;
error_log("💾 example_parameters final para '{$templateName}': " . json_encode($exampleParameters, JSON_UNESCAPED_UNICODE));
}
// Preparar datos para guardar
$componentsJson = !empty($components) ? json_encode($components, JSON_UNESCAPED_UNICODE) : null;
$exampleJson = !empty($exampleParameters) ? json_encode($exampleParameters, JSON_UNESCAPED_UNICODE) : null;
// Verificar si la plantilla ya existe
$existing = $db->fetch(
"SELECT id, status FROM message_templates WHERE template_name = ? AND language_code = ?",
[$templateName, $language]
);
if ($existing) {
// Actualizar plantilla existente con nuevos campos
$db->execute(
"UPDATE message_templates SET
status = ?,
category = ?,
body_text = ?,
header_text = ?,
header_type = ?,
footer_text = ?,
components = ?,
example_parameters = ?,
updated_at = NOW()
WHERE id = ?",
[
strtolower($status),
strtolower($category),
$bodyText,
$headerText,
$headerType,
$footerText,
$componentsJson,
$exampleJson,
$existing['id']
]
);
$updatedCount++;
} else {
// Insertar nueva plantilla con componentes
$db->execute(
"INSERT INTO message_templates (
name,
template_name,
language_code,
category,
status,
body_text,
header_text,
header_type,
footer_text,
components,
example_parameters,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())",
[
$templateName, // nombre descriptivo igual al nombre técnico
$templateName,
$language,
strtolower($category),
strtolower($status),
$bodyText,
$headerText,
$headerType,
$footerText,
$componentsJson,
$exampleJson
]
);
$syncedCount++;
}
} catch (Exception $e) {
$errors[] = "Error procesando plantilla '{$templateName}': " . $e->getMessage();
}
}
// Log de la sincronización
writeLog('INFO', "Plantillas sincronizadas desde Facebook: {$syncedCount} nuevas, {$updatedCount} actualizadas, {$skippedCount} sin cambios");
echo json_encode([
'success' => true,
'message' => 'Plantillas sincronizadas correctamente',
'data' => [
'total' => count($templates),
'synced' => $syncedCount,
'updated' => $updatedCount,
'skipped' => $skippedCount,
'errors' => $errors
]
]);
} catch (Exception $e) {
error_log("Error in sync_templates_from_facebook.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+50
View File
@@ -0,0 +1,50 @@
<?php
/**
* API - Activar/Desactivar usuario administrador
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Leer datos del request
$input = json_decode(file_get_contents('php://input'), true);
$userId = $input['user_id'] ?? null;
$isActive = $input['is_active'] ?? null;
if (!$userId) {
throw new Exception('ID de usuario requerido');
}
// No permitir desactivar el propio usuario
if ($userId == $_SESSION['user_id']) {
throw new Exception('No puedes desactivar tu propio usuario');
}
// Actualizar estado
$db->execute(
"UPDATE admin_users SET is_active = ? WHERE id = ?",
[$isActive ? 1 : 0, $userId]
);
echo json_encode([
'success' => true,
'message' => 'Estado actualizado correctamente'
]);
} catch (Exception $e) {
error_log('Error toggling admin user status: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+61
View File
@@ -0,0 +1,61 @@
<?php
/**
* API - Actualizar usuario administrador
* Fecha: 3 de febrero de 2026
*/
require_once '../config/config.php';
// Verificar autenticación
requireAuthentication();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
try {
$db = Database::getInstance();
// Leer datos del request
$input = json_decode(file_get_contents('php://input'), true);
$userId = $input['user_id'] ?? null;
$username = trim($input['username'] ?? '');
$fullName = trim($input['full_name'] ?? '');
$email = trim($input['email'] ?? '');
if (!$userId) {
throw new Exception('ID de usuario requerido');
}
if (empty($username)) {
throw new Exception('El nombre de usuario es requerido');
}
// Verificar si el username ya existe (excepto para el usuario actual)
$existing = $db->fetch(
"SELECT id FROM admin_users WHERE username = ? AND id != ?",
[$username, $userId]
);
if ($existing) {
throw new Exception('El nombre de usuario ya está en uso');
}
// Actualizar usuario
$db->execute(
"UPDATE admin_users SET username = ?, full_name = ?, email = ? WHERE id = ?",
[$username, $fullName, $email, $userId]
);
echo json_encode([
'success' => true,
'message' => 'Usuario actualizado correctamente'
]);
} catch (Exception $e) {
error_log('Error updating admin user: ' . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+101
View File
@@ -0,0 +1,101 @@
<?php
/**
* API - Actualizar mensaje programado
*/
require_once '../config/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: PUT, POST');
header('Access-Control-Allow-Headers: Content-Type');
requireAuthentication();
try {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Datos inválidos');
}
$id = isset($input['id']) ? intval($input['id']) : null;
if (!$id) {
throw new Exception('ID de mensaje programado requerido');
}
$db = Database::getInstance();
// Verificar que existe
$existing = $db->fetch("SELECT * FROM scheduled_messages WHERE id = ?", [$id]);
if (!$existing) {
throw new Exception('Mensaje programado no encontrado');
}
// No permitir editar mensajes ya enviados
if ($existing['status'] === 'sent') {
throw new Exception('No se puede editar un mensaje ya enviado');
}
// Preparar campos a actualizar
$updates = [];
$params = [];
if (isset($input['scheduled_date'])) {
$updates[] = "scheduled_date = ?";
$params[] = $input['scheduled_date'];
}
if (isset($input['scheduled_time'])) {
$updates[] = "scheduled_time = ?";
$params[] = $input['scheduled_time'];
}
if (isset($input['template_parameters'])) {
$updates[] = "template_parameters = ?";
$params[] = is_array($input['template_parameters'])
? json_encode($input['template_parameters'], JSON_UNESCAPED_UNICODE)
: $input['template_parameters'];
}
if (isset($input['message_content'])) {
$updates[] = "message_content = ?";
$params[] = $input['message_content'];
}
if (isset($input['status'])) {
$updates[] = "status = ?";
$params[] = $input['status'];
}
if (empty($updates)) {
throw new Exception('No hay campos para actualizar');
}
// Agregar updated_at
$updates[] = "updated_at = NOW()";
// Agregar ID al final de params
$params[] = $id;
// Ejecutar actualización
$sql = "UPDATE scheduled_messages SET " . implode(', ', $updates) . " WHERE id = ?";
$db->execute($sql, $params);
writeLog('INFO', "Mensaje programado actualizado: ID={$id}");
echo json_encode([
'success' => true,
'message' => 'Mensaje programado actualizado exitosamente'
]);
} catch (Exception $e) {
error_log("Error in update_scheduled_message.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+12 -4
View File
@@ -163,10 +163,18 @@ try {
}
}
// Construir URL pública
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$publicUrl = $protocol . '://' . $host . '/uploads/' . $uniqueName;
// Construir URL pública usando APP_URL del .env si está disponible
// Esto asegura que los archivos sean accesibles públicamente desde WhatsApp (datos móviles)
$appUrl = getenv('APP_URL');
if ($appUrl && $appUrl !== '') {
// Usar APP_URL del .env (ej: https://bot.u-s.app/)
$publicUrl = rtrim($appUrl, '/') . '/uploads/' . $uniqueName;
} else {
// Fallback: auto-detectar desde HTTP_HOST (desarrollo local)
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$publicUrl = $protocol . '://' . $host . '/uploads/' . $uniqueName;
}
// Determinar tipo de medio
$mediaType = 'document';
+135
View File
@@ -279,3 +279,138 @@
[2026-01-28 03:19:02] 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-01-28 03:19:02] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1769588642&hash=ARkCJbkmByU2tdLOv2EHpSH0F12MJjM_Oqj-hiGgNVSYAg
[2026-01-28 03:19:02] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1769588642&hash=ARl75SVpu4vAnsH1AXoBGSv5sjftMr7JPocagoN5kIdMWQ
[2026-02-03 08:52:36] 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-03 08:52:36] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:52:36] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:52:36] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127056&hash=ARmn_DQsFhxynN1KXT-Ge4Xf8WLnjXtDlWoDsUomB4lLKg
[2026-02-03 08:52:36] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127056&hash=ARnKjD4rWdn1Lf97robC2d7AyEHpxH6a2_oPLhmgZ0I9pA
[2026-02-03 08:52:57] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:52:57] 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-03 08:52:57] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:52:57] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127077&hash=ARldzoeUm09JzYXDKy0ypZS4VLSt5A541gqPHgUu-u9geA
[2026-02-03 08:52:57] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127077&hash=ARmAMwa-nPL80hEmwrHHiSHiMhllm6JUl3UG413ZbMkpnA
[2026-02-03 08:52:59] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:52:59] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:52:59] 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-03 08:52:59] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127079&hash=ARmB9s1otyY0Zp-uU577Ezen_iMY_waheMG8EMOMTf4yvA
[2026-02-03 08:52:59] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127079&hash=ARlMcDHlgltZRpheK41ar-GLM_sSPXV_8yudSsLXa4IOJw
[2026-02-03 08:53:52] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:53:52] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:53:52] 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-03 08:53:53] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127133&hash=ARmVgexderjai88OImGBkEGRonyefGBrm1-ow633oVjF5w
[2026-02-03 08:53:53] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127133&hash=ARleR9CvdH_ZXgcEKan0BggNv6osT2pUwtC3qumta4gttA
[2026-02-03 08:53:53] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 08:53:53] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 08:53:53] 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-03 08:53:53] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770127133&hash=ARmVgexderjai88OImGBkEGRonyefGBrm1-ow633oVjF5w
[2026-02-03 08:53:53] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770127133&hash=ARleR9CvdH_ZXgcEKan0BggNv6osT2pUwtC3qumta4gttA
[2026-02-03 11:15:58] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 11:15:58] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 11:15:58] 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-03 11:15:59] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770135659&hash=ARlKSkH_tfyiy1Ctx7mwxi3aZ2JrsWdWcK9I73EldYmKeg
[2026-02-03 11:15:59] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770135659&hash=ARmOinKL4W0exQVUyOxwpLCkmPghiI0rzBv22kDEYwbePA
[2026-02-03 12:00:15] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 12:00:15] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 12:00:15] 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-03 12:00:15] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770138315&hash=ARnn0qsPekd9pOK3dw85bzArN5oMXws99mc_zs6cdK0wtQ
[2026-02-03 12:00:15] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770138315&hash=ARlAM-vxY5Y6ST1Lwp8ox6SBOAefCdZ9yWVZKM-04kcgqQ
[2026-02-03 12:45:14] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 12:45:14] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 12:45:14] 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-03 12:45:14] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770141014&hash=ARn79fTfe65Rz38MXO0hhmyLSuVJsGuIzM30zAngMKVSeA
[2026-02-03 12:45:14] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770141014&hash=ARknEbp3Lf9RYQ2daDdcExwkmvWlMb1F_DLBhGQYJhqXtQ
[2026-02-03 14:22:03] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:22:03] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:22:03] 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-03 14:22:03] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770146823&hash=ARmkpGGK9nwQCAqwRqTNu8hZdxaLvFe_QqKCZDa60vjQMw
[2026-02-03 14:22:03] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770146823&hash=ARkmJ5x9KoRtE0e7E2j3ao1uDWPpEzVsJ3mSCUza8UjbKQ
[2026-02-03 14:23:56] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:23:56] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:23:56] 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-03 14:23:57] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770146937&hash=ARlFlA7KVAre979qNou-DdifbKg_QVenRMQ-ocl3p7vqjg
[2026-02-03 14:23:57] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770146937&hash=ARnJ0xX1NUbsvypvvoZA6UwXnFioUbFugAFUqwrBpQh23Q
[2026-02-03 14:28:39] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:28:39] 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-03 14:28:39] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:28:39] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770147219&hash=ARkHFZc5AuDMwNI48w_-THsJM1CoGx3G8CtP-N7fcdBBtQ
[2026-02-03 14:28:39] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770147219&hash=ARmjcb4sF70-VmiQiXeLl3p8XSaAjjGF9zbh339aUlnFOg
[2026-02-03 14:34:25] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:34:25] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:34:25] 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-03 14:34:25] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770147565&hash=ARk8Q6oLaHEvxv7WNOGtOwkgB8RDWPs7xk672ACpSuC-3A
[2026-02-03 14:34:25] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770147565&hash=ARkNv8rIgcXUHoj2i9ZwZQBW88W7p1yab6a_arnqGzbJtw
[2026-02-03 14:39:57] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:39:57] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:39:57] 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-03 14:39:58] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770147898&hash=ARnRWSW5FuBObUTXyIRf_5mEV90eLmf4Kx4hft5MAQH7fw
[2026-02-03 14:39:58] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770147898&hash=ARlVjspTqGVhZzOq1D-cMbpj7AXP3bNSacmtV3ZIg_ALqA
[2026-02-03 14:49:54] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:49:54] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:49:54] 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-03 14:49:55] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770148494&hash=ARka8CBIIFFl2uNVBdAnHVJldB8vBjgU0NbvGVDabWBfsw
[2026-02-03 14:49:55] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770148494&hash=ARnz6olwrZgd3SpTw-AbpdOo1lcbFr72hX3T6CcVDX9yzw
[2026-02-03 14:50:00] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 14:50:00] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 14:50:00] 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-03 14:50:00] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770148500&hash=ARkBigqY86ltDKoAFz_5B1g5uDgpegUSHtL4OGJeQME8rw
[2026-02-03 14:50:00] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770148500&hash=ARmtsjCTyftZSmwlVDnK0QCoYEzA8-NtYX6avOCtr-belQ
[2026-02-03 15:43:01] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:43:01] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:43:01] 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-03 15:43:02] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151682&hash=ARnTTvbQWWCa7LYWf9S55Dg6MSLjjFKtI7nWR0bN2zvDQQ
[2026-02-03 15:43:02] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151682&hash=ARlo5HOK4ztNMJITAnXP2SkjsasuE9VDkgnxXYFUC-t4ng
[2026-02-03 15:46:16] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:46:16] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:46:16] 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-03 15:46:16] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151876&hash=ARkTtYYdsUFAiZwrYv2rgdGGiEc-D59t4NMElrHDHtA_Vw
[2026-02-03 15:46:16] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151876&hash=ARlQUE5zvswz5tBhKIWtAYtD-E7qr9nTCcpuEoYi3nzDLA
[2026-02-03 15:46:33] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:46:33] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:46:33] 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-03 15:46:34] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151894&hash=ARm8Ao4ZAndoLc5sPn6u44DIOtYTW9ck-en0zxlbgOCIRA
[2026-02-03 15:46:34] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151894&hash=ARlLvqyHLBLf1wJwmDZZTlqQT0BpMvqj_hqmhijSVdCbxw
[2026-02-03 15:47:11] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:47:11] 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-03 15:47:11] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:47:11] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770151931&hash=ARmC5M11laF_p8bRXwSW0aUa6BZ5p6HLJ4xFt5i_rhGelg
[2026-02-03 15:47:11] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770151931&hash=ARklCUN4q5XJ2DcsvgXtl1clURKnFJGZ9FB1VMXZ4JUTlA
[2026-02-03 15:52:33] 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-03 15:52:33] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:52:33] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:52:34] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770152254&hash=ARkc8acjikfeqPK2EEA-lsA6To-kLYCbC0G1N5I-sgDklw
[2026-02-03 15:52:34] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770152254&hash=ARkq5Bv1Yx0I8YZVnRPl1GY3mRSjiTZsTeKecOm3WWP6fA
[2026-02-03 15:54:38] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:54:38] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:54:38] 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-03 15:54:38] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770152378&hash=ARkd0Smk6IQrA7Z-TtMBMGK080-7tiill2lA697bnJdzQQ
[2026-02-03 15:54:38] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770152378&hash=ARmCvtVqy3cbEBhreSiBlGMz_k0H0C1q3FglE1cAzOMtsA
[2026-02-03 15:54:55] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 15:54:55] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 15:54:56] 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-03 15:54:56] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770152396&hash=ARm4I-TH2FgoFVdYU6vyaEldYprMRcQMSUZD7-mteyE2bQ
[2026-02-03 15:54:56] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770152396&hash=ARmySCPJWQXfIqv5WakKH2es6o2rlrkmd_QeRWbDRDhMjA
[2026-02-03 16:17:56] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 16:17:56] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 16:17:56] 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-03 16:17:56] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153776&hash=ARkpL17QRsFrIZ9gL6jP87dZLx9WL80OCkUG2QOBFeE6Iw
[2026-02-03 16:17:56] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153776&hash=ARnVTurshL8o0fva9f7ThLQBCNVvOwYex9n-d0Guelq_3w
[2026-02-03 16:18:30] 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-03 16:18:30] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 16:18:30] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 16:18:30] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153810&hash=ARkFailq-_F7K1gQIj0Tyqh1KJ_mD8HHlC79mPeupj6g7A
[2026-02-03 16:18:30] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153810&hash=ARmcNab_e2NsTTxNcuj_VUmj1_G1h2pdSPXQmEF_T7IZ7A
[2026-02-03 16:20:02] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 16:20:02] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 16:20:02] 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-03 16:20:03] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153902&hash=ARmkQUNq3ncSrBHcO337pHLH9_Mrv89BMga1M52iHJjAAA
[2026-02-03 16:20:03] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153903&hash=ARkcsEFiF-VZtOHz8UIqPbUlftOVB3pxOmlyOhHWvHwNUA
[2026-02-03 16:20:10] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[2026-02-03 16:20:10] Graph API request to https://graph.facebook.com/v22.0/1217156256734654
[2026-02-03 16:20: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-03 16:20:11] Proxying Facebook media URL with Authorization to https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=1217156256734654&source=getMedia&ext=1770153911&hash=ARlsB6UeRRokyVUn_K3GY56WCvw4M7IiTI-0YYDgWKtDXA
[2026-02-03 16:20:11] Resolved media URL from Graph for mid 2371342336638440: https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=2371342336638440&source=getMedia&ext=1770153911&hash=ARnhrJuYCnG-1FpfxhUU638YidJDdCp_9Pusp4nmWCCepQ
[2026-02-03 20:31:47] 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-03 20:31:47] Request: GET /api/version/media-url.php?id=1217156256734654 GET:{"id":"1217156256734654"} POST:[]
[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