Files
sistema_funcionando_lastwar/telegram/TelegramSender.php
nickpons666 bc77082c20 feat: Implement Discord translation feature with ephemeral messages and refined content filtering
This commit introduces a comprehensive message translation system for the Discord bot, featuring:
- Interactive language selection buttons for each translatable message.
- Ephemeral translation responses visible only to the requesting user.
- Robust filtering to prevent translation buttons for messages consisting solely of emojis, stickers, GIFs, or other non-translatable content.
- Preservation of non-textual elements (images, video thumbnails, stickers) alongside translated text in embeds.
- Full compatibility with DiscordPHP v7, addressing various API usage and error handling specifics (e.g.,  then , correct handling of null message properties,  intent).

Additionally, this commit resolves an incompatibility introduced in the shared  class, ensuring that the Telegram bot's translation functionality remains fully operational by correctly parsing  return values across both platforms.

The following files were modified:
- : Main Discord bot logic for message handling, button generation, and interaction processing.
- : Adjusted  to return full API response including confidence score.
- : Updated environment variable loading and added new constants for service URLs and tokens.
- : Updated constructor to include  for absolute URL generation.
- : Adjusted all calls to  to correctly extract language codes from the new array return format, resolving Telegram bot's translation issues.
2026-02-08 16:28:17 -06:00

853 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;
private $baseUrl; // Nueva propiedad para almacenar BOT_BASE_URL
public function __construct($botToken, $pdo, string $baseUrl)
{
$this->botToken = $botToken;
$this->pdo = $pdo;
$this->baseUrl = rtrim($baseUrl, '/'); // Asegurarse de que no tenga una barra al final
}
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 = $this->baseUrl;
$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 = $this->baseUrl;
$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 = $this->baseUrl;
$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;
}
}