diff --git a/config/config.php b/config/config.php index 0295bdc..d862b7b 100755 --- a/config/config.php +++ b/config/config.php @@ -7,45 +7,37 @@ require_once __DIR__ . '/../vendor/autoload.php'; // Verificar si estamos en un contenedor Docker $is_docker = (getenv('DOCKER_CONTAINER') === '1') || ($_SERVER['DOCKER_CONTAINER'] ?? null) === '1'; -if ($is_docker) { - // Docker: cargar archivo .env creado por el entrypoint - $dotenv = null; - if (file_exists(dirname(__DIR__) . '/.env')) { - $dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__)); - try { - $dotenv->load(); - } catch (Exception $e) { - die('Error al cargar el archivo de entorno en Docker: ' . $e->getMessage()); - } - } -} else { - // Entorno local: cargar archivo .env según APP_ENVIRONMENT - $env = getenv('APP_ENVIRONMENT') ?: ($_SERVER['APP_ENVIRONMENT'] ?? 'pruebas'); +// Entorno: cargar archivo .env según APP_ENVIRONMENT +$env = getenv('APP_ENVIRONMENT') ?: ($_SERVER['APP_ENVIRONMENT'] ?? 'pruebas'); + +if ($env === 'reod') { $envFile = '.env'; - if ($env) { - $envFile = '.env.' . $env; - } - - $dotenv = null; - if (file_exists(dirname(__DIR__) . '/' . $envFile)) { - $dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__), $envFile); - } elseif (file_exists(dirname(__DIR__) . '/.env')) { - $dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__)); - } - - if ($dotenv) { - try { - $dotenv->load(); - } catch (Exception $e) { - die('Error al cargar el archivo de entorno: ' . $e->getMessage()); - } - $dotenv->required([ - 'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS', - 'JWT_SECRET', 'APP_URL' - ]); - } +} else { + $envFile = '.env.' . $env; } +$dotenv = null; +if (file_exists(dirname(__DIR__) . '/' . $envFile)) { + $dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__), $envFile); +} elseif (file_exists(dirname(__DIR__) . '/.env')) { + $dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__)); +} + +if ($dotenv) { + try { + $dotenv->load(); + } catch (Exception $e) { + die('Error al cargar el archivo de entorno: ' . $e->getMessage()); + } + // ... el resto de la configuración ... + // Aquí es donde definimos las variables obligatorias + $dotenv->required([ + 'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS', + 'JWT_SECRET', 'APP_URL' + ]); +} + + // Environment Configuration define('ENVIRONMENT', $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'production'); @@ -54,7 +46,9 @@ $is_cli = (php_sapi_name() === 'cli' || defined('STDIN')); // Helper function to get env vars function getEnvVar($name, $default = null) { - return $_ENV[$name] ?? $_SERVER[$name] ?? getenv($name) ?? $default; + // Priorizamos getenv() para las variables cargadas por Dotenv, + // ya que sabemos que funciona y $_ENV puede estar corrupto o contener Array en algunos entornos Docker. + return getenv($name) ?: ($_ENV[$name] ?? $_SERVER[$name] ?? $default); } // Configurar la URL base y el protocolo diff --git a/discord_bot.php b/discord_bot.php index 0df8e78..e0c0309 100755 --- a/discord_bot.php +++ b/discord_bot.php @@ -8,7 +8,7 @@ require_once __DIR__ . '/includes/db.php'; require_once __DIR__ . '/includes/logger.php'; require_once __DIR__ . '/discord/DiscordSender.php'; require_once __DIR__ . '/discord/converters/HtmlToDiscordMarkdownConverter.php'; -require_once __DIR__ . '/includes/Translate.php'; +require_once __DIR__ . '/src/Translate.php'; // Importar clases necesarias use Discord\Discord; @@ -38,7 +38,7 @@ if (!defined('DISCORD_BOT_TOKEN') || empty(DISCORD_BOT_TOKEN)) { try { $discord = new Discord([ 'token' => DISCORD_BOT_TOKEN, - 'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS, + 'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS | Intents::MESSAGE_CONTENT, 'logger' => $logger ]); @@ -96,7 +96,7 @@ try { if (!empty($originalContent)) { try { - $translator = new Translate(); + $translator = new Translate(LIBRETRANSLATE_URL); $sourceLang = $translator->detectLanguage($originalContent) ?? 'es'; if ($sourceLang === $targetLang) { // Fallback: muchas plantillas están en ES; intenta forzar ES como origen @@ -143,6 +143,111 @@ try { return; // Salir después de manejar la interacción } + // MANEJAR TRADUCCIÓN A IDIOMA ESPECÍFICO (desde botones de idiomas) + if (strpos($customId, 'translate_to_lang:') === 0) { + // Extraer el ID del mensaje original y el idioma destino del custom_id + $parts = explode(':', $customId); + if (count($parts) < 3) { + $interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ Error: Formato de botón de traducción inválido."), true); + return; + } + $originalMessageId = $parts[1]; + $targetLang = $parts[2]; + + // 1. Deferir la respuesta de forma efímera para que el usuario sepa que se está procesando + $interaction->acknowledge(true)->done(function() use ($originalMessageId, $targetLang, $interaction, $discord, $pdo, $logger, $userId) { + // 2. Obtener el mensaje original usando su ID + $interaction->channel->messages->fetch($originalMessageId)->done(function ($originalMessage) use ($originalMessageId, $interaction, $discord, $pdo, $logger, $userId, $targetLang) { + $originalContent = (string) ($originalMessage->content ?? ''); + + $emojiMap = []; + preg_match_all('//', $originalContent, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $match) { + $emojiMap[$match[1]] = $match[0]; + } + $contentForTranslation = preg_replace('//', '', $originalContent); + $contentForTranslation = trim($contentForTranslation); + + if (empty($contentForTranslation)) { + $interaction->editOriginalResponse(MessageBuilder::new()->setContent("⚠️ El mensaje no contiene texto traducible.")); + return; + } + + try { + $translator = new Translate(LIBRETRANSLATE_URL); + $detectionResultForSource = $translator->detectLanguage($contentForTranslation); + $sourceLang = $detectionResultForSource[0]['language'] ?? 'es'; + + if ($sourceLang === $targetLang) { + $interaction->editOriginalResponse(MessageBuilder::new()->setContent("⚠️ El mensaje ya está en el idioma seleccionado ({$targetLang}).")); + return; + } + + $translatedText = $translator->translateText($contentForTranslation, $sourceLang, $targetLang); + + if (!empty($translatedText)) { + foreach ($emojiMap as $emojiTag) { + $translatedText .= ' ' . $emojiTag; + } + $translatedText = trim($translatedText); + + $stmt = $pdo->prepare("SELECT flag_emoji, language_name FROM supported_languages WHERE language_code = ? AND is_active = 1"); + $stmt->execute([$targetLang]); + $langInfo = $stmt->fetch(PDO::FETCH_ASSOC); + $flag = $langInfo['flag_emoji'] ?? '🏳️'; + $langName = $langInfo['language_name'] ?? strtoupper($targetLang); + + $embed = new Embed($discord); + $embed->setTitle("{$flag} Traducción a {$langName}"); + $embed->setDescription($translatedText); + $embed->setColor("#5865F2"); + $embed->setFooter("Traducido de {$sourceLang} • Solo tú puedes ver esto"); + + $imageUrl = null; + if ($originalMessage->attachments !== null && count($originalMessage->attachments) > 0) { + $firstAttachment = $originalMessage->attachments->first(); + if ($firstAttachment && isset($firstAttachment->url)) { + $contentType = $firstAttachment->content_type ?? ''; + if (strpos($contentType, 'image/') === 0 || strpos($contentType, 'video/') === 0) { + $imageUrl = $firstAttachment->url; + } + } + } + + if (!$imageUrl && $originalMessage->stickers !== null && count($originalMessage->stickers) > 0) { + $firstSticker = $originalMessage->stickers->first(); + if ($firstSticker && isset($firstSticker->image_url)) { + $imageUrl = $firstSticker->image_url; + } + } + + if ($imageUrl) { + $embed->setImage($imageUrl); + } + + // 3. Editar la respuesta original con el resultado + $builder = MessageBuilder::new()->addEmbed($embed); + $interaction->sendFollowUpMessage($builder, true); + + $logger->info("[TRANSLATION_BUTTON] Usuario {$userId} tradujo mensaje #{$originalMessageId} de {$sourceLang} a {$targetLang}"); + } else { + $interaction->editOriginalResponse(MessageBuilder::new()->setContent("⚠️ No se pudo traducir el mensaje.")); + } + } catch (\Throwable $e) { + $logger->error("[TRANSLATION_BUTTON] Error al traducir", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); + $interaction->editOriginalResponse(MessageBuilder::new()->setContent("❌ Error al traducir: " . $e->getMessage())); + } + }, function ($error) use ($interaction, $logger) { + $logger->error("[TRANSLATION_BUTTON] Error al obtener mensaje original", ['error' => $error->getMessage()]); + $interaction->editOriginalResponse(MessageBuilder::new()->setContent("❌ No se pudo obtener el mensaje original.")); + }); + }, function ($error) use ($logger, $interaction) { + $logger->error("[TRANSLATION_BUTTON] Error al reconocer (defer) la interacción.", ['error' => $error->getMessage()]); + // If even deferring fails, we can't edit the response. + }); + return; // Finalizar para no procesar otros ifs + } + // Traducción de plantillas completas (comandos #) if (strpos($customId, 'translate_template:') === 0) { $payload = substr($customId, strlen('translate_template:')); @@ -183,7 +288,7 @@ try { return; } - $translator = new Translate(); + $translator = new Translate(LIBRETRANSLATE_URL); $sourceLang = $translator->detectLanguage($fullText) ?? 'es'; if ($sourceLang === $targetLang) { // Fallback: fuerza ES como origen si coincide con el destino (común en nuestras plantillas) @@ -499,37 +604,89 @@ function handleDiscordCommand(Message $message, PDO $pdo, Logger $logger) function handleDiscordTranslation(Message $message, PDO $pdo, Logger $logger) { try { - $translator = new Translate(); + $translator = new Translate(LIBRETRANSLATE_URL); // Instanciar al inicio - $text = $message->content; - $detectedLang = $translator->detectLanguage(strip_tags($text)) ?? 'es'; + $messageContentOriginal = trim($message->content); + + // Determine if there is translatable text content based on LibreTranslate confidence + $hasTranslatableText = false; + $contentForDetection = ''; // Initialize outside if block for broader scope - // Obtener idiomas activos y encolar traducciones - $langStmt = $pdo->query("SELECT language_code FROM supported_languages WHERE is_active = 1"); - $activeLangs = $langStmt->fetchAll(PDO::FETCH_COLUMN); + if (!empty($messageContentOriginal)) { // Only try to detect if message has any content + // Prepare content for language detection: remove custom Discord emojis + $contentForDetection = preg_replace('//u', '', $messageContentOriginal); + // Removed: $contentForDetection = strip_tags($contentForDetection); // This was the culprit + $contentForDetection = trim($contentForDetection); - if (in_array($detectedLang, $activeLangs)) { - foreach ($activeLangs as $targetLang) { - if ($detectedLang === $targetLang) { - continue; // No traducir al mismo idioma + if (!empty($contentForDetection)) { // Only if there's something left to detect + $detectionResult = $translator->detectLanguage($contentForDetection); + $confidence = $detectionResult[0]['confidence'] ?? 0.0; + if ($confidence > 0.0) { + $hasTranslatableText = true; } - - $sql = "INSERT INTO translation_queue (platform, message_id, chat_id, user_id, text_to_translate, source_lang, target_lang) VALUES (?, ?, ?, ?, ?, ?, ?)"; - $stmt = $pdo->prepare($sql); - $stmt->execute([ - 'discord', - $message->id, - $message->channel_id, - $message->author->id, - $text, - $detectedLang, - $targetLang - ]); - - $logger->info("[QUEUE] Mensaje de Discord #{$message->id} encolado para traducción de '$detectedLang' a '$targetLang'."); } } + + // If no translatable text is found, do not send buttons. + if (!$hasTranslatableText) { + $logger->info("[TRANSLATION_BUTTONS] Mensaje de Discord #{$message->id} sin contenido de texto traducible, no se envían botones de traducción."); + return; + } + + // 1. Detectar idioma original (using the content prepared for detection, which we know has text) + $finalDetectionResult = $translator->detectLanguage($contentForDetection); // This will be the full array or null + $detectedLang = $finalDetectionResult[0]['language'] ?? 'es'; // Correctly extract language + + // 2. Obtener todos los idiomas activos con información completa + $langStmt = $pdo->query("SELECT language_code, language_name, flag_emoji FROM supported_languages WHERE is_active = 1 ORDER BY language_name ASC"); + $activeLangs = $langStmt->fetchAll(PDO::FETCH_ASSOC); + + // 3. Filtrar los idiomas de destino (todos los activos menos el original) + $targetLangs = array_filter($activeLangs, function($lang) use ($detectedLang) { + return $lang['language_code'] !== $detectedLang; + }); + + // 4. Si no hay idiomas a los que traducir, no hacer nada + if (empty($targetLangs)) { + $logger->info("[TRANSLATION_BUTTONS] No se requieren botones de traducción para el mensaje de Discord #{$message->id} desde '$detectedLang'."); + return; + } + + // 5. Crear botones de traducción para cada idioma destino + $components = []; + $actionRow = ActionRow::new(); + $buttonCount = 0; + + foreach ($targetLangs as $lang) { + $button = Button::new(Button::STYLE_SECONDARY, 'translate_to_lang:' . $message->id . ':' . $lang['language_code']) + ->setLabel($lang['language_name']); + if (!empty($lang['flag_emoji'])) { + $button->setEmoji($lang['flag_emoji']); + } + $actionRow->addComponent($button); + $buttonCount++; + + if ($buttonCount % 5 === 0) { + $components[] = $actionRow; + $actionRow = ActionRow::new(); + } + } + if ($buttonCount % 5 !== 0) { + $components[] = $actionRow; + } + + // 6. Enviar mensaje del bot con botones como respuesta al mensaje original + $builder = MessageBuilder::new() + ->setContent('🌐 Select a language to translate to:') + ->setComponents($components); + + $message->reply($builder)->done(function () use ($logger, $message, $detectedLang) { + $logger->info("[TRANSLATION_BUTTONS] Botones de traducción enviados para mensaje #{$message->id} (idioma detectado: $detectedLang)"); + }, function ($error) use ($logger, $message) { + $logger->error("[TRANSLATION_BUTTONS] Error al enviar botones para mensaje #{$message->id}", ['error' => $error->getMessage()]); + }); + } catch (Throwable $e) { - $logger->error("[Error Encolando Traducción Discord]", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); + $logger->error("[TRANSLATION_BUTTONS] Error al procesar mensaje para botones de traducción", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); } } diff --git a/src/Translate.php b/src/Translate.php index 1550dcb..10d567a 100755 --- a/src/Translate.php +++ b/src/Translate.php @@ -4,21 +4,22 @@ class Translate { private $apiUrl; - public function __construct() + public function __construct(string $apiUrl) { - $this->apiUrl = rtrim($_ENV['LIBRETRANSLATE_URL'], '/'); + $this->apiUrl = rtrim($apiUrl, '/'); } public function detectLanguage($text) { if (empty(trim($text))) { - return null; + return null; // Or return an empty array indicating no detection } $response = $this->request('/detect', ['q' => $text]); + // Return the full response array if detection was successful if (isset($response[0]['language'])) { - return $response[0]['language']; + return $response; // Return the array, e.g., [{"confidence":90.0,"language":"es"}] } return null; diff --git a/telegram/TelegramSender.php b/telegram/TelegramSender.php index 99d8221..d1f9782 100755 --- a/telegram/TelegramSender.php +++ b/telegram/TelegramSender.php @@ -8,11 +8,13 @@ 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) + 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) @@ -302,7 +304,7 @@ class TelegramSender // Convertir rutas relativas a absolutas si es necesario if (!preg_match('/^https?:\/\//', $image_url)) { - $base_url = rtrim($_ENV['APP_URL'], '/'); + $base_url = $this->baseUrl; $image_url = $base_url . '/' . ltrim($image_url, '/'); custom_log("[DEBUG] URL de imagen convertida: " . $image_url); @@ -605,7 +607,7 @@ class TelegramSender // Si la URL es relativa, intentar convertirla a absoluta if (!preg_match('/^https?:\/\//i', $url)) { - $baseUrl = rtrim($_ENV['APP_URL'], '/'); + $baseUrl = $this->baseUrl; $absoluteUrl = $baseUrl . '/' . ltrim($url, '/'); custom_log("${logPrefix} URL relativa detectada, convirtiendo a absoluta: $absoluteUrl"); $url = $absoluteUrl; @@ -712,7 +714,7 @@ class TelegramSender } if (!preg_match('/^https?:\/\//', $url)) { - $baseUrl = rtrim($_ENV['APP_URL'], '/'); + $baseUrl = $this->baseUrl; $url = $baseUrl . '/' . ltrim($url, '/'); custom_log("[DEBUG] isValidImageUrl: URL convertida a absoluta: " . $url); } diff --git a/telegram/webhook/telegram_bot_webhook.php b/telegram/webhook/telegram_bot_webhook.php index 93bf756..9dd2ef3 100755 --- a/telegram/webhook/telegram_bot_webhook.php +++ b/telegram/webhook/telegram_bot_webhook.php @@ -22,7 +22,7 @@ ini_set('log_errors', 1); // Verificación de autenticación $authToken = $_GET['auth_token'] ?? ''; -$expectedToken = $_ENV['TELEGRAM_WEBHOOK_TOKEN'] ?? ''; +$expectedToken = TELEGRAM_WEBHOOK_TOKEN; if (!empty($expectedToken) && $authToken !== $expectedToken) { http_response_code(403); custom_log("Acceso no autorizado: token inválido."); @@ -30,7 +30,7 @@ if (!empty($expectedToken) && $authToken !== $expectedToken) { } // Verificar token del bot -$botToken = $_ENV['TELEGRAM_BOT_TOKEN'] ?? ''; +$botToken = TELEGRAM_BOT_TOKEN; if (empty($botToken)) { http_response_code(500); custom_log("Token de bot no configurado."); @@ -49,8 +49,8 @@ if (!$update) { custom_log("Update recibido: " . json_encode($update, JSON_PRETTY_PRINT)); try { - $telegram = new TelegramSender($botToken, $pdo); - $translator = new Translate(); + $telegram = new TelegramSender(TELEGRAM_BOT_TOKEN, $pdo, BOT_BASE_URL); + $translator = new Translate(LIBRETRANSLATE_URL); $commandLocker = new CommandLocker($pdo); $message = $update['message'] ?? $update['channel_post'] ?? null; @@ -248,7 +248,8 @@ try { if (!empty($originalText)) { try { - $sourceLang = $translator->detectLanguage($originalText); + $detectionResult = $translator->detectLanguage($originalText); + $sourceLang = $detectionResult[0]['language'] ?? null; if ($sourceLang && $sourceLang !== $targetLang) { $translatedText = $translator->translateHtml($originalText, $sourceLang, $targetLang); $telegram->answerCallbackQuery($callbackId, ['text' => $translatedText, 'show_alert' => true]); @@ -279,7 +280,8 @@ try { if ($originalContent) { // Detectar idioma real del contenido y aplicar fallback si coincide con el destino $plain = strip_tags(html_entity_decode($originalContent, ENT_QUOTES | ENT_HTML5, 'UTF-8')); - $sourceLang = $translator->detectLanguage($plain) ?? 'es'; + $detectionResult = $translator->detectLanguage($plain); + $sourceLang = $detectionResult[0]['language'] ?? 'es'; if ($sourceLang === $targetLang) { $fallbackSrc = 'es'; if ($fallbackSrc !== $targetLang) { @@ -351,7 +353,8 @@ try { function handleCommand($pdo, $telegram, $commandLocker, $translator, $text, $from, $chatId, $messageId) { $userId = $from['id']; - $detectedLang = $translator->detectLanguage($text) ?? 'es'; + $detectionResult = $translator->detectLanguage($text); + $detectedLang = $detectionResult[0]['language'] ?? 'es'; if (strpos($text, '/setlang') === 0 || strpos($text, '/setlanguage') === 0) { $parts = explode(' ', $text, 2); @@ -459,7 +462,8 @@ function handleCommand($pdo, $telegram, $commandLocker, $translator, $text, $fro $converter = new HtmlToTelegramHtmlConverter(); $content = $converter->convert($content); - $detectedLang = $translator->detectLanguage(strip_tags($content)) ?? 'es'; + $detectionResult = $translator->detectLanguage(strip_tags($content)); + $detectedLang = $detectionResult[0]['language'] ?? 'es'; if ($detectedLang !== 'es') { $translatedContent = $translator->translateHtml($content, $detectedLang, 'es'); if ($translatedContent) $content = $translatedContent; @@ -520,7 +524,8 @@ function handleRegularMessage($pdo, $telegram, $commandLocker, $translator, $tex try { // 1. Detectar el idioma del mensaje entrante - $detectedLang = $translator->detectLanguage(strip_tags($text)) ?? 'es'; + $detectionResult = $translator->detectLanguage(strip_tags($text)); + $detectedLang = $detectionResult[0]['language'] ?? 'es'; // 2. Guardar la interacción original en la base de datos try {