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 {