feat: Mejorar bot Discord con traducción interactiva efímera

- Cambia traducción automática por botones interactivos con banderas
- Implementa traducciones privadas (solo visibles para usuario que presiona)
- Agrega opción de caché interactivo en script de deploy
- Simplifica mensaje de selección de idiomas
- Corrige errores de DatabaseConnection duplicada
This commit is contained in:
2026-02-06 17:54:34 -06:00
parent 3f0727984a
commit d1ec6bed5c
2 changed files with 205 additions and 101 deletions

View File

@@ -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;
@@ -79,66 +79,116 @@ try {
try {
if (strpos($customId, 'translate_manual:') === 0) {
$targetLang = substr($customId, strlen('translate_manual:'));
$userId = $interaction->user->id;
$originalMessage = $interaction->message;
$channelId = $originalMessage->channel_id;
$channel = $interaction->channel;
// Responder de inmediato para evitar timeout de interacción
$interaction->respondWithMessage(MessageBuilder::new()->setContent('⌛ Procesando traducción...'), true);
// Extraer contenido del mensaje: primero content plano, luego embeds como fallback
$originalContent = trim((string) ($originalMessage->content ?? ''));
if ($originalContent === '' && count($originalMessage->embeds) > 0) {
foreach ($originalMessage->embeds as $embed) {
$originalContent .= trim((string) ($embed->description ?? '')) . "\n";
}
$originalContent = trim($originalContent);
}
if (!empty($originalContent)) {
try {
$translator = new Translate();
$sourceLang = $translator->detectLanguage($originalContent) ?? 'es';
if ($sourceLang === $targetLang) {
// Fallback: muchas plantillas están en ES; intenta forzar ES como origen
$fallbackSrc = 'es';
if ($fallbackSrc !== $targetLang) {
$translatedText = $translator->translateText($originalContent, $fallbackSrc, $targetLang);
$sourceLang = $fallbackSrc;
} else {
$translatedText = null;
}
} else {
$translatedText = $translator->translateText($originalContent, $sourceLang, $targetLang);
}
// Obtener bandera desde supported_languages
$flag = '';
try {
// Buscar el mensaje original que se está traduciendo
// Para interacciones, el mensaje original está en el mensaje de la interacción
$originalContent = '';
// Primero intentar obtener el mensaje referenciado si existe
$messageReference = $originalMessage->message_reference;
if ($messageReference && $messageReference->message_id) {
$referencedMessageId = $messageReference->message_id;
$channel->messages->fetch($referencedMessageId)->done(function (Message $referencedMessage) use ($interaction, $targetLang, $logger, $discord, $userId, $pdo) {
try {
$originalContent = trim((string) ($referencedMessage->content ?? ''));
if (empty($originalContent)) {
$interaction->respondWithMessage(
MessageBuilder::new()->setContent('❌ No se encontró contenido para traducir.'),
true
);
return;
}
$translator = new Translate(LIBRETRANSLATE_URL);
$sourceLang = $translator->detectLanguage($originalContent) ?? 'es';
if ($sourceLang === $targetLang) {
$interaction->respondWithMessage(
MessageBuilder::new()->setContent(' El mensaje ya está en este idioma.'),
true
);
return;
}
$translatedText = $translator->translateText($originalContent, $sourceLang, $targetLang);
// Obtener bandera
$stmt = $pdo->prepare("SELECT flag_emoji FROM supported_languages WHERE language_code = ? AND is_active = 1");
$stmt->execute([$targetLang]);
$flag = $stmt->fetchColumn() ?: '';
} catch (\Throwable $e) { /* noop */ }
$flag = $flag !== '' ? $flag : '🏳️';
$flag = $flag !== '' ? $flag : '🏳️';
// Enviar resultado como mensaje normal al canal, mencionando al usuario
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
$mention = '<@' . $userId . '>';
$content = "{$mention} {$flag} Traducción ({$sourceLang}{$targetLang}):\n> " . $translatedText;
// Pequeña espera para que el mensaje efímero aparezca primero
usleep(300000);
$sender->sendRawMessage($channelId, $content);
if (empty($translatedText)) {
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
$sender->sendRawMessage($channelId, '<@' . $userId . '> El mensaje ya está en este idioma.');
// Responder efímeramente con la traducción
$response = "{$flag} **Traducción ({$sourceLang}{$targetLang}):**\n\n" . $translatedText;
$interaction->respondWithMessage(
MessageBuilder::new()->setContent($response),
true // Efímero - solo visible para el usuario
);
$logger->info("[TRADUCCIÓN EFÍMERA] Traducción {$sourceLang}{$targetLang} enviada efímeramente");
} catch (\Throwable $e) {
$logger->error("[Error Traducción Manual]", ['error' => $e->getMessage()]);
$interaction->respondWithMessage(
MessageBuilder::new()->setContent('❌ Error al procesar la traducción: ' . $e->getMessage()),
true
);
}
});
return; // Salir después de iniciar el fetch
} else {
// Fallback: usar el contenido del mensaje original si no hay referencia
$originalContent = trim((string) ($originalMessage->content ?? ''));
if (empty($originalContent)) {
$interaction->respondWithMessage(
MessageBuilder::new()->setContent('❌ No se encontró contenido para traducir.'),
true
);
return;
}
} catch (\Throwable $e) {
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
$sender->sendRawMessage($channelId, '<@' . $userId . "> Error al traducir: " . $e->getMessage());
$translator = new Translate(LIBRETRANSLATE_URL);
$sourceLang = $translator->detectLanguage($originalContent) ?? 'es';
if ($sourceLang === $targetLang) {
$interaction->respondWithMessage(
MessageBuilder::new()->setContent(' El mensaje ya está en este idioma.'),
true
);
return;
}
$translatedText = $translator->translateText($originalContent, $sourceLang, $targetLang);
// Obtener bandera
$stmt = $pdo->prepare("SELECT flag_emoji FROM supported_languages WHERE language_code = ? AND is_active = 1");
$stmt->execute([$targetLang]);
$flag = $stmt->fetchColumn() ?: '';
$flag = $flag !== '' ? $flag : '🏳️';
// Responder efímeramente con la traducción
$response = "{$flag} **Traducción ({$sourceLang}{$targetLang}):**\n\n" . $translatedText;
$interaction->respondWithMessage(
MessageBuilder::new()->setContent($response),
true // Efímero - solo visible para el usuario
);
$logger->info("[TRADUCCIÓN EFÍMERA] Traducción {$sourceLang}{$targetLang} enviada efímeramente");
}
} else {
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
$sender->sendRawMessage($channelId, '<@' . $userId . '> No se encontró contenido para traducir.');
} catch (\Throwable $e) {
$logger->error("[Error Interacción Manual]", ['error' => $e->getMessage()]);
$interaction->respondWithMessage(
MessageBuilder::new()->setContent('❌ Error al procesar la solicitud: ' . $e->getMessage()),
true
);
}
return; // Salir después de manejar la interacción
}
@@ -183,7 +233,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 +549,61 @@ function handleDiscordCommand(Message $message, PDO $pdo, Logger $logger)
function handleDiscordTranslation(Message $message, PDO $pdo, Logger $logger)
{
try {
$translator = new Translate();
$text = $message->content;
$translator = new Translate(LIBRETRANSLATE_URL);
$detectedLang = $translator->detectLanguage(strip_tags($text)) ?? 'es';
// 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);
// Obtener idiomas activos disponibles
$langStmt = $pdo->query("SELECT language_code, language_name, flag_emoji FROM supported_languages WHERE is_active = 1");
$activeLangs = $langStmt->fetchAll(PDO::FETCH_ASSOC);
if (in_array($detectedLang, $activeLangs)) {
foreach ($activeLangs as $targetLang) {
if ($detectedLang === $targetLang) {
continue; // No traducir al mismo idioma
}
// Filtrar idiomas diferentes al detectado
$targetLangs = array_filter($activeLangs, function($lang) use ($detectedLang) {
return $lang['language_code'] !== $detectedLang;
});
$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
]);
if (empty($targetLangs)) {
$logger->info("[TRADUCCIÓN] No hay idiomas disponibles para traducir desde '$detectedLang'");
return;
}
$logger->info("[QUEUE] Mensaje de Discord #{$message->id} encolado para traducción de '$detectedLang' a '$targetLang'.");
// Crear botones con banderas
$components = [];
$actionRow = ActionRow::new();
$buttonCount = 0;
foreach ($targetLangs as $lang) {
$button = Button::new(Button::STYLE_SECONDARY, 'translate_manual:' . $lang['language_code'])
->setLabel($lang['language_name']);
if (!empty($lang['flag_emoji'])) {
$button->setEmoji($lang['flag_emoji']);
}
$actionRow->addComponent($button);
$buttonCount++;
// Discord permite máximo 5 botones por ActionRow
if ($buttonCount % 5 === 0) {
$components[] = $actionRow;
$actionRow = ActionRow::new();
}
}
if ($buttonCount % 5 !== 0) {
$components[] = $actionRow;
}
// Enviar mensaje con botones efímeros
$builder = MessageBuilder::new()
->setContent("🌍 **Selecciona idioma para traducir:**")
->setComponents($components);
$message->reply($builder, true); // true = ephemeral (solo visible para el usuario)
$logger->info("[TRADUCCIÓN] Botones de traducción enviados para mensaje #{$message->id}");
} catch (Throwable $e) {
$logger->error("[Error Encolando Traducción Discord]", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
$logger->error("[Error Traducción Discord]", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
}
}