feat: Implement Discord translation feature with ephemeral messages and refined content filtering
This commit introduces a comprehensive message translation system for the Discord bot, featuring: - Interactive language selection buttons for each translatable message. - Ephemeral translation responses visible only to the requesting user. - Robust filtering to prevent translation buttons for messages consisting solely of emojis, stickers, GIFs, or other non-translatable content. - Preservation of non-textual elements (images, video thumbnails, stickers) alongside translated text in embeds. - Full compatibility with DiscordPHP v7, addressing various API usage and error handling specifics (e.g., then , correct handling of null message properties, intent). Additionally, this commit resolves an incompatibility introduced in the shared class, ensuring that the Telegram bot's translation functionality remains fully operational by correctly parsing return values across both platforms. The following files were modified: - : Main Discord bot logic for message handling, button generation, and interaction processing. - : Adjusted to return full API response including confidence score. - : Updated environment variable loading and added new constants for service URLs and tokens. - : Updated constructor to include for absolute URL generation. - : Adjusted all calls to to correctly extract language codes from the new array return format, resolving Telegram bot's translation issues.
This commit is contained in:
215
discord_bot.php
215
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('/<a?:(\w+):(\d+)>/', $originalContent, $matches, PREG_OFFSET_CAPTURE);
|
||||
foreach ($matches[0] as $match) {
|
||||
$emojiMap[$match[1]] = $match[0];
|
||||
}
|
||||
$contentForTranslation = preg_replace('/<a?:(\w+):(\d+)>/', '', $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('/<a?:(\w+):(\d+)>/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()]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user