851 lines
37 KiB
PHP
Executable File
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;
|
|
}
|
|
} |