botToken = $botToken; $this->pdo = $pdo; } public function sendMessage($chatId, $content, $options = [], $addTranslateButton = false, $messageLanguage = 'es', $originalFullContent = null) { try { custom_log("Iniciando envío de mensaje a chat $chatId"); if (empty(trim($content))) { custom_log("Error: Contenido vacío para el chat $chatId"); return false; } // Obtener el language_code del destinatario $recipientLang = 'es'; // Idioma por defecto try { $stmt = $this->pdo->prepare("SELECT language_code FROM recipients WHERE platform_id = ? AND platform = 'telegram'"); $stmt->execute([$chatId]); $dbLang = $stmt->fetchColumn(); if ($dbLang) { $recipientLang = $dbLang; } } catch (PDOException $e) { custom_log("Error al obtener language_code del destinatario: " . $e->getMessage()); } custom_log("[DEBUG] Idioma del destinatario ($chatId): " . $recipientLang); $parts = $this->parseContent($content); if (empty($parts)) { custom_log("No se pudo parsear el contenido para el chat $chatId"); return false; } $messageIds = []; $all_sent_successfully = true; $totalParts = count($parts); custom_log("Procesando $totalParts partes del mensaje para el chat $chatId"); foreach ($parts as $index => $part) { $isLastPart = ($index === $totalParts - 1); $partOptions = $isLastPart ? $options : []; try { custom_log("Procesando parte " . ($index + 1) . "/$totalParts de tipo: " . $part['type']); $response = null; $partSuccess = false; if ($part['type'] === 'text') { // Asegurarse de que el parámetro addTranslateButton se pase correctamente if ($isLastPart && $addTranslateButton) { $partOptions['add_translate_button'] = true; } $response = $this->sendText($chatId, $part['content'], $partOptions); $partSuccess = ($response && ($response['ok'] ?? false)); } elseif ($part['type'] === 'image') { // Verificar la URL de la imagen antes de intentar enviarla $imageUrl = $part['url'] ?? ''; custom_log("Verificando URL de imagen: " . $imageUrl); if ($this->isValidImageUrl($imageUrl)) { // Asegurarse de que el parámetro addTranslateButton se pase correctamente if ($isLastPart && $addTranslateButton) { $partOptions['add_translate_button'] = true; } $response = $this->sendPhoto($chatId, $imageUrl, $part['caption'] ?? '', $partOptions); $partSuccess = ($response && ($response['ok'] ?? false)); } else { $errorMsg = "URL de imagen no válida o inaccesible: " . $imageUrl; custom_log($errorMsg); // Enviar un mensaje de error al chat $errorText = "⚠️ No se pudo cargar una imagen: " . basename($imageUrl) . "\n"; if (!empty($part['caption'])) { $errorText .= "📝 " . $part['caption'] . "\n"; } $errorText .= "🔗 " . $imageUrl; $this->sendText($chatId, $errorText); $partSuccess = false; } } if ($partSuccess) { if (isset($response['result']['message_id'])) { $messageIds[] = [ 'success' => true, 'message_id' => $response['result']['message_id'], 'type' => $part['type'] ]; } } else { $error = $response['description'] ?? 'Error desconocido al procesar la parte del mensaje'; custom_log("Error al enviar parte " . ($index + 1) . "/$totalParts: " . json_encode($error)); // Agregar el error a los IDs de mensaje para referencia $messageIds[] = [ 'success' => false, 'error' => $error, 'type' => $part['type'], 'content' => $part['type'] === 'image' ? $part['url'] : substr($part['content'] ?? '', 0, 100) . '...' ]; $all_sent_successfully = false; } } catch (Exception $e) { custom_log("Excepción al procesar parte: " . $e->getMessage()); $all_sent_successfully = false; } if (!$isLastPart) { usleep(500000); // 500ms de pausa } } // Si hubo algún error, devolver información detallada if (!$all_sent_successfully) { $failedParts = array_filter($messageIds, function($msg) { return isset($msg['success']) && $msg['success'] === false; }); custom_log("No se pudieron enviar " . count($failedParts) . " partes del mensaje"); // Si no se pudo enviar ninguna parte, devolver falso if (count($failedParts) === count($messageIds)) { return false; } } // Obtener los mensajes enviados exitosamente $sentMessages = array_filter($messageIds, function($msg) { return !empty($msg['success']) && !empty($msg['message_id']); }); // Guardar el mensaje saliente en telegram_interactions con el idioma del remitente // Esto se hace una vez, después de que todas las partes del mensaje han sido enviadas. if ($all_sent_successfully && !empty($messageIds)) { $lastSentMessage = end($messageIds); $lastMessageId = $lastSentMessage['message_id'] ?? null; $contentToSave = $originalFullContent ?? $content; // Use originalFullContent or the assembled content if ($lastMessageId) { // Ensure contentToSave is not NULL if ($contentToSave === null) { $contentToSave = ''; } try { $stmt = $this->pdo->prepare("INSERT INTO telegram_interactions (chat_id, message_text, direction, telegram_message_id, language_code) VALUES (?, ?, 'out', ?, ?)"); $stmt->execute([$chatId, $contentToSave, $lastMessageId, $messageLanguage]); custom_log("[DEBUG] Mensaje completo guardado con idioma: $messageLanguage y message_id: " . $lastMessageId); } catch (PDOException $e) { custom_log("Error al guardar mensaje saliente en telegram_interactions (después del loop): " . $e->getMessage()); } } } // Lógica para los botones de traducción basados en supported_languages if ($addTranslateButton && !empty($sentMessages)) { $lastMessage = end($sentMessages); $lastMessageId = $lastMessage['message_id'] ?? null; if ($lastMessageId) { $messageLang = strtolower($messageLanguage); $buttons = []; try { $stmt = $this->pdo->query("SELECT language_code, flag_emoji FROM supported_languages WHERE is_active = 1 ORDER BY language_name ASC"); $langs = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($langs as $lang) { $code = strtolower($lang['language_code']); if ($code === $messageLang) continue; // no traducir al mismo idioma $flag = $lang['flag_emoji'] ?: ''; $buttons[] = [ 'text' => $flag !== '' ? $flag : strtoupper($code), 'callback_data' => 'translate:' . $lastMessageId . ':' . $code ]; } } catch (Exception $e) { custom_log('[DEBUG] No se pudieron obtener idiomas activos para botones: ' . $e->getMessage()); } if (!empty($buttons)) { // Armar filas de hasta 6 botones por fila para no saturar $inline = []; $row = []; foreach ($buttons as $i => $btn) { $row[] = $btn; if (count($row) >= 6) { $inline[] = $row; $row = []; } } if (!empty($row)) $inline[] = $row; $keyboard = ['inline_keyboard' => $inline]; custom_log('[DEBUG] Enviando ' . count($buttons) . ' botones de traducción para mensaje ' . $lastMessageId); $this->editMessageReplyMarkup($chatId, $lastMessageId, json_encode($keyboard)); } else { custom_log('[DEBUG] No hay idiomas activos distintos a ' . $messageLang . ' para mostrar botones.'); } } else { custom_log('[DEBUG] No se muestra botón de traducción. Razón: Falta message ID'); } } // Devolver los IDs de los mensajes que se enviaron correctamente return $messageIds; } catch (Exception $e) { custom_log("Error crítico en sendMessage para el chat $chatId: " . $e->getMessage()); return false; } } public function deleteMessage($chatId, $messageId) { $response = $this->apiRequest('deleteMessage', [ 'chat_id' => $chatId, 'message_id' => $messageId ]); if (!isset($response['ok']) || $response['ok'] !== true) { throw new Exception("Error de Telegram al eliminar"); } return true; } public function editMessageText($chatId, $messageId, $text, $options = []) { $params = array_merge([ 'chat_id' => $chatId, 'message_id' => $messageId, 'text' => $text ], $options); return $this->apiRequest('editMessageText', $params); } public function editMessageReplyMarkup($chatId, $messageId, $replyMarkup) { $params = [ 'chat_id' => $chatId, 'message_id' => $messageId ]; // Solo agregar reply_markup si no está vacío if (!empty($replyMarkup)) { $params['reply_markup'] = $replyMarkup; } else { // Para eliminar el teclado, necesitamos enviar un objeto JSON vacío $params['reply_markup'] = '{"inline_keyboard":[]}'; } return $this->apiRequest('editMessageReplyMarkup', $params); } public function answerCallbackQuery($callbackQueryId, $options = []) { $params = array_merge([ 'callback_query_id' => $callbackQueryId ], $options); return $this->apiRequest('answerCallbackQuery', $params); } private function parseContent($html) { $parts = []; if (empty(trim($html))) return []; if (strip_tags($html) === $html) { return [['type' => 'text', 'content' => $html]]; } preg_match_all('/(]+src=[\'\"]([^\'\"]+)[\'\"][^>]*>)/i', $html, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); if (empty($matches)) { return [['type' => 'text', 'content' => $html]]; } $last_pos = 0; foreach ($matches as $match) { $full_match = $match[0][0]; $image_url = $match[2][0]; $offset = $match[0][1]; $text_before = substr($html, $last_pos, $offset - $last_pos); if (!empty(trim($text_before))) { $parts[] = ['type' => 'text', 'content' => trim($text_before)]; } // Convertir rutas relativas a absolutas si es necesario if (!preg_match('/^https?:\/\//', $image_url)) { $base_url = rtrim($_ENV['APP_URL'], '/'); $image_url = $base_url . '/' . ltrim($image_url, '/'); custom_log("[DEBUG] URL de imagen convertida: " . $image_url); // Verificar si la URL es accesible $headers = @get_headers($image_url); if ($headers && strpos($headers[0], '200') === false) { error_log("[ERROR] La imagen no es accesible en: " . $image_url); } else { error_log("[DEBUG] La imagen es accesible en: " . $image_url); } } $parts[] = ['type' => 'image', 'url' => $image_url, 'caption' => '']; $last_pos = $offset + strlen($full_match); } $remaining_text = substr($html, $last_pos); if (!empty(trim($remaining_text))) { $parts[] = ['type' => 'text', 'content' => trim($remaining_text)]; } return $parts; } private function splitLongText($text, $maxLength = 4000) { if (mb_strlen($text) <= $maxLength) return [$text]; $parts = []; $paragraphs = preg_split('/\n\s*\n/', $text); $currentPart = ''; foreach ($paragraphs as $paragraph) { if ((mb_strlen($currentPart) + mb_strlen($paragraph) + 2) <= $maxLength) { $currentPart .= (empty($currentPart) ? '' : "\n\n") . $paragraph; } else { if (!empty($currentPart)) $parts[] = $currentPart; $currentPart = $paragraph; while (mb_strlen($currentPart) > $maxLength) { $chunk = mb_substr($currentPart, 0, $maxLength); $parts[] = $chunk; $currentPart = mb_substr($currentPart, $maxLength); } } } if (!empty($currentPart)) $parts[] = $currentPart; return $parts; } /** * Envía un mensaje de texto a un chat de Telegram, manejando mensajes largos y rate limiting * * @param int|string $chatId ID del chat de Telegram * @param string $text Texto a enviar * @param array $options Opciones adicionales para el mensaje * @param int $retryCount Número de reintentos actual (uso interno) * @param int $maxRetries Número máximo de reintentos * @return array|null Respuesta de la API o null en caso de error */ private function sendText($chatId, $text, $options = [], $retryCount = 0, $maxRetries = 3) { // Asegurarse de que $options sea un array if (!is_array($options)) { custom_log("ADVERTENCIA: Las opciones no son un array. Valor recibido: " . print_r($options, true)); $options = []; } // Verificar si se debe agregar el botón de traducción $addTranslateButton = $options['add_translate_button'] ?? false; unset($options['add_translate_button']); // Eliminar para evitar conflictos // Configuración de depuración $debug = $options['debug'] ?? false; unset($options['debug']); // Limitar la longitud de los mensajes de depuración $debugText = $debug ? substr($text, 0, 200) : ''; $logPrefix = "[SEND_TEXT] [Chat: $chatId]"; custom_log("$logPrefix === INICIO ==="); custom_log("$logPrefix Tamaño del texto: " . mb_strlen($text, 'UTF-8') . " caracteres"); if ($debug) { custom_log("$logPrefix Texto (primeros 200 caracteres): $debugText"); custom_log("$logPrefix Opciones: " . json_encode($options)); custom_log("$logPrefix Mostrar botón de traducción: " . ($addTranslateButton ? 'Sí' : 'No')); } // Limpiar el texto de etiquetas HTML innecesarias $clean_text = trim(strip_tags($text)); if (empty($clean_text)) { custom_log("$logPrefix ERROR: El texto está vacío después de limpiar etiquetas"); return null; } // Dividir el texto en partes manejables $textParts = $this->splitLongText($clean_text); $totalParts = count($textParts); $allResponses = []; $allSuccessful = true; custom_log("$logPrefix Texto dividido en $totalParts partes"); foreach ($textParts as $index => $part) { $isLastPart = ($index === $totalParts - 1); $partNumber = $index + 1; // Configurar parámetros básicos del mensaje $params = [ 'chat_id' => $chatId, 'text' => $part, 'disable_web_page_preview' => true, 'parse_mode' => 'HTML', 'disable_notification' => $options['disable_notification'] ?? false ]; // Agregar opciones adicionales solo a la última parte del mensaje if ($isLastPart) { // Mantener solo las opciones permitidas por la API de Telegram $allowedOptions = [ 'reply_markup', 'reply_to_message_id', 'allow_sending_without_reply', 'entities', 'protect_content', 'message_thread_id' ]; foreach ($allowedOptions as $option) { if (isset($options[$option])) { $params[$option] = $options[$option]; } } if ($debug) { custom_log("$logPrefix [Parte $partNumber/$totalParts] Opciones aplicadas: " . json_encode(array_keys($params))); } } // Intentar enviar la parte del mensaje con reintentos $attempt = 0; $maxAttempts = 3; $response = null; while ($attempt < $maxAttempts) { $attempt++; custom_log("$logPrefix [Parte $partNumber/$totalParts] Intento $attempt de $maxAttempts"); $response = $this->apiRequest('sendMessage', $params); // Verificar si la solicitud fue exitosa if (!empty($response) && ($response['ok'] ?? false)) { $allResponses[] = $response; if ($debug) { $msgId = $response['result']['message_id'] ?? 'desconocido'; custom_log("$logPrefix [Parte $partNumber/$totalParts] Enviado exitosamente (ID: $msgId)"); } // Pequeña pausa entre mensajes para evitar rate limiting if (!$isLastPart) { usleep(500000); // 0.5 segundos } break; // Salir del bucle de reintentos } else { $errorCode = $response['error_code'] ?? 'desconocido'; $errorDesc = $response['description'] ?? 'Error desconocido'; custom_log("$logPrefix [Parte $partNumber/$totalParts] Error $errorCode: $errorDesc"); // Manejar errores específicos if (strpos($errorDesc, 'Too Many Requests') !== false) { $retryAfter = $response['parameters']['retry_after'] ?? 5; // Por defecto 5 segundos custom_log("$logPrefix Rate limit alcanzado. Reintentando en $retryAfter segundos..."); sleep($retryAfter); continue; // Reintentar } // Para otros errores, esperar un tiempo antes de reintentar if ($attempt < $maxAttempts) { $backoffTime = pow(2, $attempt); // Retroceso exponencial custom_log("$logPrefix Reintentando en $backoffTime segundos..."); sleep($backoffTime); } else { custom_log("$logPrefix Se agotaron los reintentos para esta parte del mensaje"); $allSuccessful = false; } } } // Si no se pudo enviar después de los reintentos, marcar como fallido if (empty($response) || !($response['ok'] ?? false)) { $allSuccessful = false; // Si es un error crítico, detener el envío de las partes restantes $errorCode = $response['error_code'] ?? 0; $criticalErrors = [400, 403, 404]; // Errores que indican que no tiene sentido seguir intentando if (in_array($errorCode, $criticalErrors)) { custom_log("$logPrefix Error crítico ($errorCode). Deteniendo el envío de las partes restantes."); break; } } } // Devolver la última respuesta exitosa o null si hubo errores $result = $allSuccessful ? end($allResponses) : null; custom_log("$logPrefix === FIN === " . ($allSuccessful ? 'ÉXITO' : 'CON ERRORES')); return $result; } /** * Envía una foto a un chat de Telegram con manejo de errores y reintentos * * @param int|string $chatId ID del chat de Telegram * @param string $photoUrl URL de la foto a enviar * @param string $caption Texto opcional para la foto * @param array $options Opciones adicionales para el mensaje * @param int $retryCount Número de reintentos actual (uso interno) * @param int $maxRetries Número máximo de reintentos * @return array|bool Respuesta de la API o false en caso de error */ private function sendPhoto($chatId, $photoUrl, $caption = '', $options = [], $retryCount = 0, $maxRetries = 3) { $logPrefix = "[SEND_PHOTO] [Chat: $chatId]"; custom_log("$logPrefix === INICIO ==="); try { // Validar y normalizar la URL de la imagen $photoUrl = $this->normalizeAndValidateImageUrl($photoUrl, $logPrefix); // Si la URL no es válida, intentar enviar un mensaje de error if ($photoUrl === false) { $fallbackMessage = $this->createPhotoErrorFallback($photoUrl, $caption, "URL de imagen no válida o inaccesible"); return $this->sendText($chatId, $fallbackMessage, $options); } // Configurar parámetros básicos para el envío de la foto $params = [ 'chat_id' => $chatId, 'photo' => $photoUrl, 'caption' => $caption, 'parse_mode' => 'HTML', 'disable_notification' => $options['disable_notification'] ?? false ]; // Agregar opciones adicionales permitidas por la API de Telegram $allowedOptions = [ 'reply_markup', 'reply_to_message_id', 'allow_sending_without_reply', 'protect_content', 'message_thread_id' ]; foreach ($allowedOptions as $option) { if (isset($options[$option])) { $params[$option] = $options[$option]; } } // Realizar el envío con manejo de reintentos $result = $this->sendPhotoWithRetry($params, $photoUrl, $retryCount, $maxRetries, $logPrefix); // Si el envío falló después de los reintentos, enviar un mensaje de error if ($result === false || (isset($result['ok']) && !$result['ok'])) { $errorDesc = $result['description'] ?? 'Error desconocido'; custom_log("$logPrefix Error al enviar foto después de $maxRetries intentos: $errorDesc"); $fallbackMessage = $this->createPhotoErrorFallback($photoUrl, $caption, $errorDesc); return $this->sendText($chatId, $fallbackMessage, $options); } custom_log("$logPrefix Foto enviada exitosamente"); return $result; } catch (Exception $e) { custom_log("$logPrefix Excepción: " . $e->getMessage() . " - URL: $photoUrl"); // En caso de error inesperado, intentar enviar un mensaje de texto $fallbackMessage = $this->createPhotoErrorFallback( $photoUrl, $caption, "Error inesperado: " . $e->getMessage() ); return $this->sendText($chatId, $fallbackMessage, $options); } finally { custom_log("$logPrefix === FIN ==="); } } /** * Normaliza y valida una URL de imagen, convirtiendo URLs relativas a absolutas si es necesario * * @param string $url URL de la imagen a normalizar y validar * @param string $logPrefix Prefijo para los mensajes de log * @return string|false URL normalizada o false si no es válida */ private function normalizeAndValidateImageUrl($url, $logPrefix = '') { if (empty($url)) { custom_log("${logPrefix} URL de imagen vacía"); return false; } // Si la URL es relativa, intentar convertirla a absoluta if (!preg_match('/^https?:\/\//i', $url)) { $baseUrl = rtrim($_ENV['APP_URL'], '/'); $absoluteUrl = $baseUrl . '/' . ltrim($url, '/'); custom_log("${logPrefix} URL relativa detectada, convirtiendo a absoluta: $absoluteUrl"); $url = $absoluteUrl; } // Verificar si la URL es accesible y es una imagen if ($this->isValidImageUrl($url)) { custom_log("${logPrefix} URL de imagen válida: $url"); return $url; } custom_log("${logPrefix} URL de imagen no válida o inaccesible: $url"); return false; } /** * Crea un mensaje de error estandarizado para cuando falla el envío de una foto * * @param string $photoUrl URL de la foto que falló * @param string $caption Texto de la foto (opcional) * @param string $error Descripción del error * @return string Mensaje de error formateado */ private function createPhotoErrorFallback($photoUrl, $caption, $error) { $message = "⚠️ *Error al procesar la imagen*\n"; $message .= "\n🔗 *URL*: " . (strlen($photoUrl) > 100 ? substr($photo_url, 0, 100) . '...' : $photoUrl); if (!empty($error)) { $message .= "\n\n❌ *Error*: $error"; } if (!empty($caption)) { $message .= "\n\n📝 *Descripción*: " . (mb_strlen($caption) > 200 ? mb_substr($caption, 0, 200) . '...' : $caption); } return $message; } /** * Envía una foto a Telegram con manejo de reintentos y rate limiting * * @param array $params Parámetros para la API de Telegram * @param string $photoUrl URL de la foto (solo para registro) * @param int $retryCount Número de reintentos actual * @param int $maxRetries Número máximo de reintentos * @param string $logPrefix Prefijo para los mensajes de log * @return array|false Respuesta de la API o false en caso de error */ private function sendPhotoWithRetry($params, $photoUrl, $retryCount = 0, $maxRetries = 3, $logPrefix = '') { $attempt = 0; $lastError = null; while ($attempt <= $maxRetries) { $attempt++; custom_log("${logPrefix} Intento $attempt de $maxRetries - Enviando foto..."); $response = $this->apiRequest('sendPhoto', $params); // Si la solicitud fue exitosa, devolver la respuesta if (!empty($response) && ($response['ok'] ?? false)) { custom_log("${logPrefix} Foto enviada exitosamente en el intento $attempt"); return $response; } // Si hay un error, registrarlo y verificar si se debe reintentar $errorCode = $response['error_code'] ?? 'desconocido'; $errorDesc = $response['description'] ?? 'Error desconocido'; $lastError = $errorDesc; custom_log("${logPrefix} Error en el intento $attempt: [$errorCode] $errorDesc"); // Si es un error de rate limit, esperar el tiempo indicado if (isset($response['parameters']['retry_after'])) { $retryAfter = $response['parameters']['retry_after']; custom_log("${logPrefix} Rate limit alcanzado. Esperando $retryAfter segundos..."); sleep($retryAfter); continue; } // Para otros errores, esperar un tiempo antes de reintentar (backoff exponencial) if ($attempt < $maxRetries) { $backoffTime = pow(2, $attempt); // 2, 4, 8, 16... segundos custom_log("${logPrefix} Reintentando en $backoffTime segundos..."); sleep($backoffTime); } } // Si llegamos aquí, se agotaron los reintentos custom_log("${logPrefix} Se agotaron los $maxRetries intentos. Último error: $lastError"); return $response ?? ['ok' => false, 'description' => $lastError ?? 'Error desconocido']; } /** * Verifica si una URL es una imagen accesible */ private function isValidImageUrl($url) { if (empty($url)) { custom_log("[DEBUG] isValidImageUrl: URL vacía."); return false; } if (!preg_match('/^https?:\/\//', $url)) { $baseUrl = rtrim($_ENV['APP_URL'], '/'); $url = $baseUrl . '/' . ltrim($url, '/'); custom_log("[DEBUG] isValidImageUrl: URL convertida a absoluta: " . $url); } if (!filter_var($url, FILTER_VALIDATE_URL)) { custom_log("[DEBUG] isValidImageUrl: URL no válida después de la conversión: " . $url); return false; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_NOBODY, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_MAXREDIRS, 3); curl_setopt($ch, CURLOPT_TIMEOUT, 5); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); $headers = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); custom_log("[DEBUG] isValidImageUrl: Verificación con cURL para " . $url . " - Código HTTP: " . $httpCode . " - Content-Type: " . $contentType); if ($httpCode != 200) { custom_log("[DEBUG] isValidImageUrl: Código de estado HTTP no es 200."); return false; } if ($contentType === null) { custom_log("[DEBUG] isValidImageUrl: Content-Type es nulo."); return false; } $isImage = (bool)preg_match('/^image\/(jpe?g|png|gif|webp|bmp|svg\+?xml)/i', $contentType); custom_log("[DEBUG] isValidImageUrl: Es imagen (" . ($isImage ? 'Sí' : 'No') . ")."); return $isImage; } /** * Realiza una petición a la API de Telegram con manejo de reintentos y rate limiting * * @param string $method Método de la API de Telegram * @param array $params Parámetros de la petición * @param int $retryCount Número de reintentos actual (para uso interno) * @param int $maxRetries Número máximo de reintentos * @return array Respuesta de la API de Telegram */ private function apiRequest($method, $params, $retryCount = 0, $maxRetries = 3) { $url = $this->apiUrl . $this->botToken . '/' . $method; custom_log("[TELEGRAM API REQUEST] Method: {$method} | Params: " . json_encode($params)); // Inicializar cURL $ch = curl_init($url); // Configurar opciones de cURL curl_setopt_array($ch, [ CURLOPT_POST => 1, CURLOPT_POSTFIELDS => http_build_query($params), CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_CONNECTTIMEOUT => 10, // 10 segundos para conectar CURLOPT_TIMEOUT => 30, // 30 segundos para la ejecución total CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'], ]); // Ejecutar la petición $response_body = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); // Manejar errores de cURL if ($response_body === false) { custom_log("[TELEGRAM API ERROR] cURL Error: " . $error); // Si hay un error de conexión, reintentar con backoff exponencial if ($retryCount < $maxRetries) { $backoffTime = pow(2, $retryCount); // Backoff exponencial (2, 4, 8 segundos...) custom_log("Reintentando en {" . $backoffTime . "} segundos... (Intento " . ($retryCount + 1) . " de $maxRetries)"); sleep($backoffTime); return $this->apiRequest($method, $params, $retryCount + 1, $maxRetries); } return ['ok' => false, 'description' => 'Error de conexión: ' . $error]; } // Decodificar la respuesta $response_array = json_decode($response_body, true); // Si hay un error en la decodificación JSON if (json_last_error() !== JSON_ERROR_NONE) { custom_log("[TELEGRAM API ERROR] Error al decodificar JSON: " . json_last_error_msg()); return ['ok' => false, 'description' => 'Error al decodificar la respuesta JSON']; } // Registrar la respuesta custom_log("[TELEGRAM API RESPONSE] HTTP Code: $http_code | Body: " . $response_body); // Manejar errores de rate limiting (código 429) if ($http_code === 429) { $retry_after = $response_array['parameters']['retry_after'] ?? 5; // Por defecto 5 segundos if ($retryCount < $maxRetries) { custom_log("Rate limit alcanzado. Reintentando en {" . $retry_after . "} segundos... (Intento " . ($retryCount + 1) . " de $maxRetries)"); sleep($retry_after); return $this->apiRequest($method, $params, $retryCount + 1, $maxRetries); } custom_log("Se alcanzó el número máximo de reintentos para el método $method"); } // Manejar otros códigos de error HTTP if ($http_code < 200 || $http_code >= 400) { $error_msg = $response_array['description'] ?? 'Error desconocido'; custom_log("[TELEGRAM API ERROR] HTTP $http_code: $error_msg"); // Si es un error 5xx, reintentar con backoff exponencial if ($http_code >= 500 && $retryCount < $maxRetries) { $backoffTime = pow(2, $retryCount); custom_log("Error del servidor. Reintentando en {" . $backoffTime . "} segundos... (Intento " . ($retryCount + 1) . " de $maxRetries)"); sleep($backoffTime); return $this->apiRequest($method, $params, $retryCount + 1, $maxRetries); } return ['ok' => false, 'description' => "Error HTTP $http_code: $error_msg"]; } return $response_array; } }