Compare commits

...

2 Commits

Author SHA1 Message Date
nickpons666
76b0584667 fix: Corregir funcionalidad de reordenamiento y generación automática de turnos
- Añadir jQuery para resolver error 'jQuery is not defined'
- Corregir variable $db indefinida en reordenamiento de rotación
- Añadir método getDb() a clase Asignacion para acceso a base de datos
- Implementar procesamiento correcto de acción 'rotacion_automatica'
- Actualizar inputs ocultos al reordenar elementos visuales
- Añadir redirección para recargar datos actualizados después de guardar orden
- Mejorar manejo de mensajes de éxito/errores en generación automática
2026-01-30 22:30:55 -06:00
nickpons666
4c7f298acd refactor: Centralizar y corregir la comunicación con la API de Telegram
Se refactoriza toda la comunicación con la API de Telegram para solucionar un problema de latencia severa en el entorno Docker. El problema era causado por un retraso en la resolución de red.

- Se mejora la función  en  para forzar el uso de IPv4, añadir timeouts y soportar métodos GET/POST.
- Se centraliza la lógica de la API en la clase , añadiendo los métodos ,  y .
- Se modifica  para que utilice los nuevos métodos centralizados, eliminando código cURL duplicado y aplicando la solución de red.
- Se mantiene la instrumentación en  para futuros diagnósticos, según lo solicitado.
2026-01-20 20:20:59 -06:00
6 changed files with 192 additions and 64 deletions

View File

@@ -0,0 +1,30 @@
# Plan de Diagnóstico de Rendimiento del Bot de Telegram
Este archivo rastrea los pasos para diagnosticar la latencia en las respuestas del bot cuando se ejecuta en Docker.
## Análisis Final
- **Diagnóstico Definitivo:** El problema no era la base de datos, sino 100% la capa de red. Cada petición (`cURL`) desde el contenedor Docker a la API de Telegram (`api.telegram.org`) sufría una demora de ~5.6 segundos. Esto se debía probablemente a un intento fallido de conexión por IPv6 antes de recurrir a IPv4.
- **Solución Aplicada:** Se modificó la función `private function request` en `bot/TelegramBot.php` para forzar el uso de IPv4 en todas las peticiones `cURL`, además de añadir timeouts de seguridad. Se refactorizó `public/admin/webhook.php` para usar los métodos centralizados en `TelegramBot.php`, asegurando que la solución se aplique de manera consistente en toda la aplicación.
## Pasos de Diagnóstico (Historial)
- [x] **Paso 1: Medir Tiempos dentro del Script (Instrumentación)**
- **Estado:** Completado.
- **Análisis:** Los logs revelaron que cada llamada a la API de Telegram tardaba ~5.6s, y las acciones con dos llamadas (como `ver_turnos`) tardaban ~11.2s.
- [x] **Paso 2: Diagnóstico de Red y DNS desde DENTRO del Contenedor**
- **Estado:** Revisado.
- **Análisis:** La prueba inicial con `curl` ya apuntaba a una conexión lenta (~0.8s), lo que fue el primer indicio del problema de red.
- [ ] **Paso 3: Análisis de Recursos del Contenedor**
- **Estado:** Cancelado.
- **Análisis:** Se determinó que el problema era de red, no de consumo de CPU/Memoria, por lo que este paso ya no es necesario.
- [x] **Paso 4: Revisión y Corrección de la Capa de Red (TelegramBot.php y admin/webhook.php)**
- **Estado:** Completado.
- **Acción:** Se analizó y mejoró `bot/TelegramBot.php` y se refactorizó `public/admin/webhook.php` para usar la lógica de comunicación centralizada y corregida.
- [ ] **Paso 5: Verificación de la Solución**
- **Estado:** En progreso.
- **Acción:** Esperando los resultados de `logs/webhook_timing.log` y la confirmación visual de la página `admin/webhook.php` después de reiniciar el contenedor y probar el bot con el parche aplicado.

View File

@@ -14,14 +14,39 @@ class TelegramBot {
$this->apiUrl = "https://api.telegram.org/bot{$this->token}";
}
private function request($method, $data = []) {
private function request($method, $data = [], $httpMethod = 'POST') {
$url = "{$this->apiUrl}/{$method}";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// --- MEJORAS ---
// 1. Forzar el uso de IPv4. Un problema común en entornos Docker
// es un timeout al intentar resolver AAAA (IPv6) antes de usar A (IPv4).
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
// 2. Añadir timeouts para evitar que el script se cuelgue indefinidamente.
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 segundos para conectar
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // 10 segundos para la transferencia total
if ($httpMethod === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_URL, $url);
} else { // GET request
$url .= empty($data) ? '' : '?' . http_build_query($data);
curl_setopt($ch, CURLOPT_URL, $url);
}
$response = curl_exec($ch);
// Añadir manejo de errores de cURL
if (curl_errno($ch)) {
error_log("cURL Error en {$method}: " . curl_error($ch));
curl_close($ch);
return null;
}
curl_close($ch);
return json_decode($response, true);
}
@@ -39,13 +64,30 @@ class TelegramBot {
}
public function getMe() {
return $this->request('getMe');
return $this->request('getMe', [], 'GET');
}
public function getUpdates($offset = 0) {
return $this->request('getUpdates', ['offset' => $offset, 'timeout' => 60]);
return $this->request('getUpdates', ['offset' => $offset, 'timeout' => 60], 'GET');
}
// --- NUEVOS MÉTODOS PARA CENTRALIZAR COMUNICACIONES ---
public function getWebhookInfo() {
return $this->request('getWebhookInfo', [], 'GET');
}
public function deleteWebhook() {
return $this->request('deleteWebhook', []);
}
public function setWebhook($webhookUrl, $allowedUpdates = ['message', 'callback_query']) {
return $this->request('setWebhook', [
'url' => $webhookUrl,
'allowed_updates' => $allowedUpdates
]);
}
public function sendKeyboard($chatId, $text) {
$keyboard = [
'inline_keyboard' => [

View File

@@ -1,5 +1,35 @@
<?php
// ---- INICIO DE INSTRUMENTACIÓN ----
$startTime = microtime(true);
function log_timing($message) {
static $lastTime = null;
global $startTime;
if ($lastTime === null) {
$lastTime = $startTime;
}
$now = microtime(true);
$elapsed = $now - $lastTime;
$totalElapsed = $now - $startTime;
$logMessage = sprintf(
"[%s] Total: %.4fs | Step: %.4fs | %s\n",
date('Y-m-d H:i:s'),
$totalElapsed,
$elapsed,
$message
);
// Usar __DIR__ para asegurar la ruta correcta
file_put_contents(__DIR__ . '/../logs/webhook_timing.log', $logMessage, FILE_APPEND);
$lastTime = $now;
}
// ---- FIN DE INSTRUMENTACIÓN ----
// Configurar logging de errores
error_reporting(E_ALL);
ini_set('log_errors', 1);
@@ -13,20 +43,25 @@ class TurnoBot {
private $config;
public function __construct() {
log_timing("TurnoBot: __construct start");
$this->config = require __DIR__ . '/../config/config.php';
$this->bot = new TelegramBot();
log_timing("TurnoBot: __construct end");
}
public function handleUpdate($update) {
log_timing("handleUpdate: start");
try {
// Manejar callback de botones inline
if (isset($update['callback_query'])) {
log_timing("handleUpdate: detected callback_query");
$this->handleCallback($update['callback_query']);
return;
}
// Manejar mensajes normales
if (!isset($update['message'])) {
log_timing("handleUpdate: no message found, exiting");
return;
}
@@ -35,64 +70,87 @@ class TurnoBot {
$text = trim($message['text'] ?? '');
if (empty($text)) {
log_timing("handleUpdate: empty text, exiting");
return;
}
$textLower = mb_strtolower($text, 'UTF-8');
log_timing("handleUpdate: processing command '{$textLower}'");
// Comandos
if ($textLower === '/start' || $textLower === '/menu' || $textLower === 'menu') {
$this->sendMenu($chatId);
} elseif ($textLower === '/turnos' || $textLower === 'turnos') {
log_timing("handleUpdate: /turnos command start");
$this->bot->sendMessage($chatId, $this->bot->getTablaTurnos(8));
log_timing("handleUpdate: /turnos command end");
} elseif ($textLower === '/semana' || $textLower === 'semana' || $textLower === 'hoy') {
log_timing("handleUpdate: /semana command start");
$this->bot->sendMessage($chatId, $this->bot->getSemanaActual());
log_timing("handleUpdate: /semana command end");
} elseif ($textLower === '/ayudantes' || $textLower === 'ayudantes') {
log_timing("handleUpdate: /ayudantes command start");
$ayudantes = $this->bot->getListaAyudantesParaBusqueda();
$this->bot->sendMessage($chatId, "<b>AYUDANTES DISPONIBLES:</b>\n\n" . implode("\n", $ayudantes));
log_timing("handleUpdate: /ayudantes command end");
} elseif ($textLower === '/pdf' || $textLower === 'pdf' || $textLower === 'mi pdf') {
log_timing("handleUpdate: /pdf command start");
$this->bot->sendPDFGeneral($chatId);
log_timing("handleUpdate: /pdf command end");
} else {
// Buscar por nombre - verificar si existe el usuario
log_timing("handleUpdate: searching by name '{$text}'");
$config = require __DIR__ . '/../config/config.php';
try {
log_timing("handleUpdate: DB connection start");
$pdo = new PDO(
"mysql:host={$config['db']['host']};port={$config['db']['port']};dbname={$config['db']['database']};charset=utf8mb4",
$config['db']['username'],
$config['db']['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
log_timing("handleUpdate: DB connection end");
} catch (Exception $e) {
log_timing("handleUpdate: DB connection FAILED");
$this->bot->sendMessage($chatId, "Error de conexion.");
return;
}
log_timing("handleUpdate: DB query start");
$stmt = $pdo->prepare("SELECT * FROM users WHERE (nombre LIKE ? OR username LIKE ?) AND rol = 'ayudante' AND activo = 1 LIMIT 1");
$stmt->execute(["%$text%", "%$text%"]);
$user = $stmt->fetch();
log_timing("handleUpdate: DB query end");
if ($user) {
log_timing("handleUpdate: user found, sending PDF");
$this->bot->sendMessage($chatId, "Generando PDF de turnos...");
$this->bot->sendPDF($chatId, $user['id']);
log_timing("handleUpdate: PDF sent");
} else {
log_timing("handleUpdate: user not found, getting plain text turnos");
$this->bot->sendMessage($chatId, $this->bot->getTurnosAyudante($text));
log_timing("handleUpdate: plain text turnos sent");
}
}
} catch (Exception $e) {
error_log("Error en handleUpdate: " . $e->getMessage());
log_timing("handleUpdate: EXCEPTION: " . $e->getMessage());
if (isset($update['message']['chat']['id'])) {
$this->bot->sendMessage($update['message']['chat']['id'], "Error: " . $e->getMessage());
}
}
log_timing("handleUpdate: end");
}
private function handleCallback($callback) {
log_timing("handleCallback: start");
try {
$callbackId = $callback['id'];
$data = $callback['data'];
$message = $callback['message'];
$chatId = $message['chat']['id'];
$messageId = $message['message_id'];
log_timing("handleCallback: processing data '{$data}'");
switch ($data) {
case 'ver_turnos':
@@ -124,12 +182,16 @@ class TurnoBot {
default:
$this->bot->answerCallback($callbackId, 'Opcion no reconocida');
}
log_timing("handleCallback: data '{$data}' processed");
} catch (Exception $e) {
error_log("Error en handleCallback: " . $e->getMessage());
log_timing("handleCallback: EXCEPTION: " . $e->getMessage());
}
log_timing("handleCallback: end");
}
private function sendMenu($chatId) {
log_timing("sendMenu: start");
$mensaje = "<b>BOT DE TURNOS - CONTENEDOR IBIZA</b>\n\n";
$mensaje .= "Selecciona una opcion del menu:\n\n";
$mensaje .= "Ver Turnos - Tabla completa de asignaciones\n";
@@ -139,19 +201,25 @@ class TurnoBot {
$mensaje .= "Mi Turno - Ver tu proximo turno";
$this->bot->sendKeyboard($chatId, $mensaje);
log_timing("sendMenu: end");
}
}
// Recibir actualización
$update = json_decode(file_get_contents('php://input'), true);
log_timing("Webhook invoked");
// Log para debugging
error_log("Webhook recibido: " . json_encode($update));
// error_log("Webhook recibido: " . json_encode($update)); // Se puede comentar para no llenar el log de errores
if ($update) {
log_timing("Update received, initializing bot");
$bot = new TurnoBot();
$bot->handleUpdate($update);
log_timing("Script finished");
} else {
http_response_code(200);
echo "Webhook activo. Usa /start para ver el menu.";
log_timing("Webhook checked (no update provided)");
// echo "Webhook activo. Usa /start para ver el menu."; // No es necesario en producción
}

View File

@@ -19,6 +19,11 @@ $asignacionModel = new Asignacion();
$message = '';
$messageType = '';
if (isset($_GET['success']) && $_GET['success'] === 'orden_actualizado') {
$message = 'Orden actualizado correctamente. Se recalcularon las asignaciones futuras.';
$messageType = 'success';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!CSRF::isValidRequest()) {
$message = 'Error de validación del formulario';
@@ -47,6 +52,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$messageType = 'success';
}
}
} elseif ($action === 'rotacion_automatica') {
$resultado = $asignacionModel->asignarSemanasFuturasAutomaticas(12);
if ($resultado['success'] > 0) {
$message = "Se generaron {$resultado['success']} semanas futuras correctamente";
if (!empty($resultado['errors'])) {
$message .= ". Errores: " . implode(', ', $resultado['errors']);
$messageType = 'warning';
} else {
$messageType = 'success';
}
} else {
$message = 'No se pudieron generar asignaciones: ' . implode(', ', $resultado['errors']);
$messageType = 'danger';
}
} elseif ($action === 'asignar_masivo') {
$userIds = $_POST['user_ids'] ?? [];
$semanaInicio = $_POST['semana_inicio'] ?? '';
@@ -370,17 +390,7 @@ No hay asignación para la semana <?= $posicionSinAsignar ?> de 4 (<?= date('d/m
<?php endforeach; ?>
</div>
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'rotacion_automatica'): ?>
<?php
$resultado = $asignacionModel->asignarSemanasFuturasAutomaticas(12);
?>
<div class="alert alert-<?= !empty($resultado['errors']) ? 'warning' : 'success' ?>">
<strong>Resultado:</strong> Se asignaron <?= $resultado['success'] ?> semanas futuras
<?php if (!empty($resultado['errores'])): ?>
<br><small>Errores: <?= implode(', ', $resultado['errores']) ?></small>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="col-md-4">
<form method="POST" class="h-100 d-flex flex-column justify-content-center">
@@ -451,7 +461,7 @@ No hay asignación para la semana <?= $posicionSinAsignar ?> de 4 (<?= date('d/m
if (!empty($nuevosOrdenes)) {
foreach ($nuevosOrdenes as $index => $userId) {
$stmt = $db->prepare("
$stmt = $asignacionModel->getDb()->prepare("
UPDATE rotacion_orden
SET orden = ?
WHERE user_id = ? AND activo = 1
@@ -463,10 +473,9 @@ No hay asignación para la semana <?= $posicionSinAsignar ?> de 4 (<?= date('d/m
$resultado = $asignacionModel->recalcularAsignaciones(20);
if ($resultado['success'] > 0) {
echo '<div class="alert alert-success mt-3">';
echo 'Orden actualizado correctamente. ';
echo "Se recalcularon {$resultado['success']} semanas futuras.";
echo '</div>';
// Redireccionar para recargar los datos actualizados
header("Location: " . $_SERVER['PHP_SELF'] . "?success=orden_actualizado");
exit;
}
}
?>
@@ -475,6 +484,7 @@ No hay asignación para la semana <?= $posicionSinAsignar ?> de 4 (<?= date('d/m
</div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
@@ -491,6 +501,9 @@ No hay asignación para la semana <?= $posicionSinAsignar ?> de 4 (<?= date('d/m
function actualizarNumeros() {
$("#sortableList li").each(function(index) {
$(this).find('.badge').text('☰ ' + (index + 1));
// Actualizar el valor del input oculto con el nuevo orden
var userId = $(this).data('id');
$(this).find('input[type="hidden"]').attr('name', 'ordenes[' + index + ']').val(userId);
});
}

View File

@@ -17,7 +17,7 @@ $botInfo = null;
// Obtener información del bot
$botMe = $bot->getMe();
if ($botMe && isset($botMe['ok']) && $botMe['ok']) {
if ($botMe && $botMe['ok']) { // Simplificado
$botInfo = $botMe['result'];
}
@@ -26,14 +26,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'verificar') {
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/getWebhookInfo";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
$result = $bot->getWebhookInfo(); // Usar el método centralizado
if ($result && isset($result['ok'])) {
if ($result && $result['ok']) { // Simplificado
$webhookInfo = $result;
$message = 'Información del webhook obtenida';
$messageType = 'success';
@@ -42,17 +37,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$messageType = 'danger';
}
} elseif ($action === 'borrar') {
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/deleteWebhook";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
$result = $bot->deleteWebhook(); // Usar el método centralizado
if ($result && isset($result['ok']) && $result['ok']) {
if ($result && $result['ok']) { // Simplificado
$message = 'Webhook eliminado correctamente';
$messageType = 'success';
$webhookInfo = null;
@@ -70,20 +57,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = 'La URL ingresada no es válida';
$messageType = 'danger';
} else {
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/setWebhook";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'url' => $webhookUrl,
'allowed_updates' => ['message', 'callback_query']
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
$result = $bot->setWebhook($webhookUrl); // Usar el método centralizado
if ($result && isset($result['ok']) && $result['ok']) {
if ($result && $result['ok']) { // Simplificado
$message = "Webhook configurado correctamente en:\n" . htmlspecialchars($webhookUrl);
$messageType = 'success';
} else {
@@ -94,13 +70,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// Obtener estado actual del webhook
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/getWebhookInfo";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$webhookInfo = json_decode($response, true);
// Obtener estado actual del webhook al cargar la página
$webhookInfo = $bot->getWebhookInfo(); // Usar el método centralizado
$currentPage = 'webhook';
$pageTitle = 'Administración del Bot de Telegram';

View File

@@ -8,6 +8,10 @@ class Asignacion {
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
public function getDb() {
return $this->db;
}
public function getAsignacionActual() {
$hoy = new DateTime();