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:
212
discord_bot.php
212
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;
|
||||
@@ -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();
|
||||
// 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) {
|
||||
// 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);
|
||||
$interaction->respondWithMessage(
|
||||
MessageBuilder::new()->setContent('ℹ️ El mensaje ya está en este idioma.'),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener bandera desde supported_languages
|
||||
$flag = '';
|
||||
try {
|
||||
$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 : '🏳️';
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
$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");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($channelId, '<@' . $userId . "> Error al traducir: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($channelId, '<@' . $userId . '> No se encontró contenido para traducir.');
|
||||
$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;
|
||||
});
|
||||
|
||||
if (empty($targetLangs)) {
|
||||
$logger->info("[TRADUCCIÓN] No hay idiomas disponibles para traducir desde '$detectedLang'");
|
||||
return;
|
||||
}
|
||||
|
||||
$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
|
||||
]);
|
||||
// Crear botones con banderas
|
||||
$components = [];
|
||||
$actionRow = ActionRow::new();
|
||||
$buttonCount = 0;
|
||||
|
||||
$logger->info("[QUEUE] Mensaje de Discord #{$message->id} encolado para traducción de '$detectedLang' a '$targetLang'.");
|
||||
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()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================
|
||||
# Script para construir y subir imagen Docker
|
||||
# Target: 10.10.4.3:5000/bot-lastwar:v2
|
||||
# ============================================
|
||||
# ==================================================
|
||||
# Script para construir y subir una imagen Docker.
|
||||
#
|
||||
# Uso:
|
||||
# ./deploy.sh
|
||||
#
|
||||
# El script te preguntará interactivamente:
|
||||
# - Tag para la imagen
|
||||
# - Si deseas usar caché al construir
|
||||
# ==================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Cambiar al directorio del proyecto (carpeta padre de donde está este script)
|
||||
# --- Configuración ---
|
||||
REGISTRY_URL="10.10.4.3:5000"
|
||||
IMAGE_NAME="bot-lastwar"
|
||||
|
||||
# Preguntar por el tag si no se proporciona como argumento
|
||||
if [ -n "$1" ]; then
|
||||
TAG="$1"
|
||||
else
|
||||
read -p "Introduce el tag para la imagen [latest]: " TAG
|
||||
TAG="${TAG:-latest}"
|
||||
fi
|
||||
|
||||
# Preguntar si quiere usar caché
|
||||
read -p "¿Usar caché al construir? (s/n) [n]: " USE_CACHE
|
||||
if [ "${USE_CACHE,,}" = "s" ]; then
|
||||
BUILD_CACHE_FLAG=""
|
||||
CACHE_STATUS="con caché"
|
||||
else
|
||||
BUILD_CACHE_FLAG="--no-cache"
|
||||
CACHE_STATUS="sin caché"
|
||||
fi
|
||||
# --- Fin de la Configuración ---
|
||||
|
||||
# Nombres completos de la imagen
|
||||
LOCAL_IMAGE_NAME="${IMAGE_NAME}:${TAG}"
|
||||
REMOTE_IMAGE_NAME="${REGISTRY_URL}/${IMAGE_NAME}:${TAG}"
|
||||
|
||||
# Cambiar al directorio raíz del proyecto (un nivel arriba de este script)
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "=========================================="
|
||||
echo "Construyendo imagen Docker..."
|
||||
echo "Directorio: $(pwd)"
|
||||
echo "Directorio de trabajo: $(pwd)"
|
||||
echo "Construyendo imagen: ${LOCAL_IMAGE_NAME}"
|
||||
echo "Opción de build: ${CACHE_STATUS}"
|
||||
echo "Destino del registry: ${REMOTE_IMAGE_NAME}"
|
||||
echo "=========================================="
|
||||
|
||||
# Construir la imagen usando el Dockerfile en docker/
|
||||
docker build -t bot-lastwar:latest -f docker/Dockerfile .
|
||||
# 1. Construir la imagen Docker
|
||||
echo "Paso 1: Construyendo imagen Docker ${CACHE_STATUS}..."
|
||||
docker build ${BUILD_CACHE_FLAG} -t "${LOCAL_IMAGE_NAME}" -f docker/Dockerfile .
|
||||
|
||||
# 2. Etiquetar la imagen para el registry
|
||||
echo "Paso 2: Etiquetando imagen como ${REMOTE_IMAGE_NAME}..."
|
||||
docker tag "${LOCAL_IMAGE_NAME}" "${REMOTE_IMAGE_NAME}"
|
||||
|
||||
# 3. Subir la imagen al registry
|
||||
echo "Paso 3: Subiendo imagen al registry..."
|
||||
docker push "${REMOTE_IMAGE_NAME}"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Etiquetando imagen para el registry..."
|
||||
echo "=========================================="
|
||||
|
||||
# Etiquetar la imagen con la versión v2
|
||||
docker tag bot-lastwar:latest 10.10.4.3:5000/bot-lastwar:v2
|
||||
|
||||
echo "=========================================="
|
||||
echo "Subiendo imagen al registry..."
|
||||
echo "=========================================="
|
||||
|
||||
# Subir al registry
|
||||
docker push 10.10.4.3:5000/bot-lastwar:v2
|
||||
|
||||
echo "=========================================="
|
||||
echo "Imagen subida exitosamente:"
|
||||
echo "10.10.4.3:5000/bot-lastwar:v2"
|
||||
echo "✅ Imagen subida exitosamente:"
|
||||
echo "${REMOTE_IMAGE_NAME}"
|
||||
echo "=========================================="
|
||||
|
||||
Reference in New Issue
Block a user