Files
sistema_funcionando_lastwar/telegram/TelegramSender.php

851 lines
37 KiB
PHP
Executable File

<?php
require_once __DIR__ . '/../includes/logger.php';
require_once __DIR__ . '/../src/Translate.php';
class TelegramSender
{
private $botToken;
private $apiUrl = 'https://api.telegram.org/bot';
private $pdo;
public function __construct($botToken, $pdo)
{
$this->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('/(<img[^>]+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;
}
}