pushHandler(new StreamHandler(__DIR__.'/logs/discord_bot.log', Logger::DEBUG)); $logger->info("Iniciando bot de Discord..."); if (!defined('DISCORD_BOT_TOKEN') || empty(DISCORD_BOT_TOKEN)) { $logger->error("Error Fatal: La constante DISCORD_BOT_TOKEN no está definida o está vacía."); die(); } try { $discord = new Discord([ 'token' => DISCORD_BOT_TOKEN, 'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS, 'logger' => $logger, 'loop' => \React\EventLoop\Loop::get(), 'socket_options' => [ 'heartbeat_interval' => 30, // Enviar heartbeat cada 30 segundos 'reconnect_timeout' => 60, // Timeout para reconexión ] ]); // Manejar eventos de conexión y desconexión $discord->on('ready', function (Discord $discord) { $discord->getLogger()->info("=================================================="); $discord->getLogger()->info("Bot conectado y listo para escuchar!"); $discord->getLogger()->info("Usuario: {$discord->user->username}#{$discord->user->discriminator}"); $discord->getLogger()->info("=================================================="); }); // Evento de reconexión $discord->on('reconnected', function (Discord $discord) { $logger->info("[RECONEXIÓN] Bot reconectado exitosamente"); }); // Evento de desconexión $discord->on('disconnected', function (Discord $discord, $reason) { $logger->warning("[DESCONEXIÓN] Bot desconectado. Razón: $reason"); }); // Evento de error $discord->on('error', function ($error, Discord $discord) { $logger->error("[ERROR DISCORD] Error en la conexión: " . $error->getMessage()); }); // Evento para nuevos miembros en el servidor $discord->on(Event::GUILD_MEMBER_ADD, function (Member $member, Discord $discord) use ($pdo, $logger) { $logger->info("[NUEVO MIEMBRO] Usuario {$member->user->username} ({$member->id}) se ha unido al servidor."); try { $stmt = $pdo->prepare( "INSERT INTO recipients (platform_id, name, type, platform, language_code) VALUES (?, ?, 'user', 'discord', 'es') ON DUPLICATE KEY UPDATE name = VALUES(name)" ); $stmt->execute([$member->id, $member->user->username]); $logger->info("[NUEVO MIEMBRO] Usuario {$member->user->username} registrado/actualizado en la base de datos."); } catch (Throwable $e) { $logger->error("[NUEVO MIEMBRO] Error al registrar al usuario.", ['error' => $e->getMessage()]); } }); // Evento para manejar interacciones (clics en botones) $discord->on(Event::INTERACTION_CREATE, function (Interaction $interaction, Discord $discord) use ($pdo, $logger) { // DiscordPHP v7: No existe isButton(). Botones llegan como MESSAGE_COMPONENT (type = 3) y component_type = 2 $type = (int) ($interaction->type ?? 0); // 3 = MESSAGE_COMPONENT $componentType = (int) ($interaction->data->component_type ?? 0); // 2 = BUTTON if ($type !== 3 || $componentType !== 2) return; $customId = $interaction->data->custom_id; $userId = $interaction->user->id; $logger->info("[INTERACCION] Usuario $userId hizo clic en el botón: $customId"); try { if (strpos($customId, 'translate_manual:') === 0) { $targetLang = substr($customId, strlen('translate_manual:')); $userId = $interaction->user->id; $originalMessage = $interaction->message; $channel = $interaction->channel; 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; } // Extraer y preservar emojis, stickers y elementos de Discord $processedContent = preserveDiscordElements($originalContent); $translator = new Translate(LIBRETRANSLATE_URL); $sourceLang = $translator->detectLanguage($processedContent['text']) ?? 'es'; if ($sourceLang === $targetLang) { $interaction->respondWithMessage( MessageBuilder::new()->setContent('ℹ️ El mensaje ya está en este idioma.'), true ); return; } $translatedText = $translator->translateText($processedContent['text'], $sourceLang, $targetLang); // Restaurar emojis y elementos de Discord $finalText = restoreDiscordElements($translatedText, $processedContent['placeholders']); // 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" . $finalText; $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; } // Extraer y preservar emojis, stickers y elementos de Discord $processedContent = preserveDiscordElements($originalContent); $translator = new Translate(LIBRETRANSLATE_URL); $sourceLang = $translator->detectLanguage($processedContent['text']) ?? 'es'; if ($sourceLang === $targetLang) { $interaction->respondWithMessage( MessageBuilder::new()->setContent('ℹ️ El mensaje ya está en este idioma.'), true ); return; } $translatedText = $translator->translateText($processedContent['text'], $sourceLang, $targetLang); // Restaurar emojis y elementos de Discord $finalText = restoreDiscordElements($translatedText, $processedContent['placeholders']); // 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" . $finalText; $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 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 } // Traducción de plantillas completas (comandos #) if (strpos($customId, 'translate_template:') === 0) { $payload = substr($customId, strlen('translate_template:')); $parts = explode(':', $payload, 2); if (count($parts) !== 2) { $interaction->respondWithMessage(MessageBuilder::new()->setContent('Formato de botón inválido.'), true); return; } [$commandKey, $targetLang] = $parts; $interaction->respondWithMessage(MessageBuilder::new()->setContent('⌛ Traduciendo plantilla...'), true); try { // Obtener contenido HTML original de la plantilla $stmt = $pdo->prepare("SELECT message_content FROM recurrent_messages WHERE telegram_command = ?"); $stmt->execute([$commandKey]); $template = $stmt->fetch(PDO::FETCH_ASSOC); if (!$template) { $sender = new DiscordSender(DISCORD_BOT_TOKEN); $sender->sendRawMessage($interaction->channel_id, '<@' . $userId . '> No se encontró la plantilla.'); return; } $originalHtml = $template['message_content'] ?? ''; $converter = new HtmlToDiscordMarkdownConverter(); $partsArr = $converter->convertToArray($originalHtml); // Unir todos los segmentos de texto (las imágenes no se traducen) $fullText = ''; foreach ($partsArr as $p) { if (($p['type'] ?? '') === 'text') { $fullText .= ($fullText === '' ? '' : "\n\n") . trim((string)$p['content']); } } if ($fullText === '') { $sender = new DiscordSender(DISCORD_BOT_TOKEN); $sender->sendRawMessage($interaction->channel_id, '<@' . $userId . '> No hay contenido de texto para traducir en la plantilla.'); return; } $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) $fallbackSrc = 'es'; if ($fallbackSrc !== $targetLang) { $translated = $translator->translateText($fullText, $fallbackSrc, $targetLang); $sourceLang = $fallbackSrc; } else { $translated = ''; } } else { $translated = $translator->translateText($fullText, $sourceLang, $targetLang); } if ($translated === '') { $sender = new DiscordSender(DISCORD_BOT_TOKEN); $sender->sendRawMessage($interaction->channel_id, '<@' . $userId . '> El contenido ya está en este idioma.'); return; } // Bandera $flag = ''; try { $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 : '🏳️'; $sender = new DiscordSender(DISCORD_BOT_TOKEN); // Pequeña espera para que el mensaje efímero aparezca primero usleep(300000); $sender->sendRawMessage($interaction->channel_id, '<@' . $userId . "> {$flag} Traducción plantilla ({$sourceLang} → {$targetLang}):\n" . $translated); } catch (\Throwable $e) { $sender = new DiscordSender(DISCORD_BOT_TOKEN); $sender->sendRawMessage($interaction->channel_id, '<@' . $userId . "> Error al traducir plantilla: " . $e->getMessage()); } return; } if ($customId === 'platicar_bot' || $customId === 'usar_ia') { $newMode = ($customId === 'platicar_bot') ? 'bot' : 'ia'; $stmt = $pdo->prepare("UPDATE recipients SET chat_mode = ? WHERE platform_id = ? AND platform = 'discord'"); $stmt->execute([$newMode, $userId]); $responseText = $newMode === 'bot' ? "🤖 Modo cambiado a 'Platicar con bot'. Ahora puedes usar los comandos normales como `/comandos`." : "🧠 Modo cambiado a 'Usar IA'. Todo lo que escribas será procesado por la IA.\n\nEscribe `/agente` para volver a este menú."; $interaction->respondWithMessage(MessageBuilder::new()->setContent($responseText), true); $logger->info("[MODO AGENTE] Usuario $userId cambió al modo: $newMode"); } } catch (Throwable $e) { $logger->error("[INTERACCION] Error al procesar el botón.", ['error' => $e->getMessage()]); $interaction->respondWithMessage(MessageBuilder::new()->setContent("Hubo un error al cambiar de modo."), true); } }); // Evento para manejar mensajes $discord->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) use ($pdo, $logger) { if ($message->author->bot) return; $logger->info("[Mensaje Recibido] En canal '{$message->channel->name}' de @{$message->author->username}: {$message->content}"); $isPrivateChat = $message->channel->is_private; $userId = $message->author->id; $content = $message->content; try { if ($isPrivateChat) { // --- LÓGICA DE CHAT PRIVADO (AGENTE/IA/BOT) --- $logger->info("[MODO AGENTE] Mensaje en chat privado de $userId."); $stmt = $pdo->prepare("SELECT chat_mode FROM recipients WHERE platform_id = ? AND platform = 'discord'"); $stmt->execute([$userId]); $userChatMode = $stmt->fetchColumn(); if ($userChatMode === false) { // --- Nuevo Usuario en DM --- $logger->info("[NUEVO USUARIO DM] Usuario $userId no encontrado. Registrando..."); $userName = $message->author->username; $insertStmt = $pdo->prepare( "INSERT INTO recipients (platform_id, name, type, platform, language_code, chat_mode) VALUES (?, ?, 'user', 'discord', 'es', 'agent') ON DUPLICATE KEY UPDATE name = VALUES(name)" ); $insertStmt->execute([$userId, $userName]); $logger->info("[NUEVO USUARIO DM] Usuario $userId ($userName) registrado con modo 'agent'."); $userChatMode = 'agent'; // Forzar modo agente para la primera interacción } if (trim($content) === '/agente') { $stmt = $pdo->prepare("UPDATE recipients SET chat_mode = 'agent' WHERE platform_id = ? AND platform = 'discord'"); $stmt->execute([$userId]); $userChatMode = 'agent'; $logger->info("[MODO AGENTE] Usuario $userId usó /agente. Reseteando a modo 'agent'."); } switch ($userChatMode) { case 'agent': $builder = MessageBuilder::new()->setContent("👋 Hola! ¿Cómo quieres interactuar?"); $actionRow = ActionRow::new() ->addComponent(Button::new(Button::STYLE_PRIMARY, 'platicar_bot')->setLabel('🤖 Platicar con bot')) ->addComponent(Button::new(Button::STYLE_SUCCESS, 'usar_ia')->setLabel('🧠 Usar IA')); $builder->addComponent($actionRow); $message->channel->sendMessage($builder); return; case 'ia': $logger->info("[MODO IA] Mensaje de $userId para la IA: $content"); $n8nWebhookUrl = $_ENV['N8N_IA_WEBHOOK_URL_DISCORD'] ?? null; if ($n8nWebhookUrl) { $postData = [ 'chat_id' => $message->channel_id, 'user_id' => $userId, 'message' => $content, 'name' => $message->author->username ]; $ch = curl_init($n8nWebhookUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData)); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_exec($ch); curl_close($ch); $logger->info("[MODO IA] Mensaje reenviado a n8n."); } else { $logger->error("[MODO IA] La variable N8N_IA_WEBHOOK_URL no está configurada."); } return; case 'bot': // Continuar con la lógica normal de bot abajo break; } } // --- LÓGICA DE BOT NORMAL (CHATS PÚBLICOS O MODO 'bot') --- if (strpos($content, '/') === 0 || strpos($content, '#') === 0) { handleDiscordCommand($message, $pdo, $logger); } else if (strtolower($content) === '!ping') { $message->reply('pong!'); } else { handleDiscordTranslation($message, $pdo, $logger); } } catch (Throwable $e) { $logger->error("!!!!!!!!!! ERROR FATAL CAPTURADO !!!!!!!!!!", [ 'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString() ]); } }); // Sistema de verificación de salud periódica $discord->on('ready', function (Discord $discord) use ($logger) { // Programar verificación de salud cada 2 minutos $discord->getLoop()->addPeriodicTimer(120, function() use ($discord, $logger) { try { // Verificar si el bot sigue conectado if ($discord->user && $discord->user->id) { $logger->info("[HEALTH CHECK] Bot conectado y respondiendo. Usuario: {$discord->user->username}"); } else { $logger->warning("[HEALTH CHECK] Bot no responde correctamente"); } } catch (\Throwable $e) { $logger->error("[HEALTH CHECK] Error en verificación: " . $e->getMessage()); } }); }); $discord->run(); } catch (Throwable $e) { $logger->critical("!!!!!!!!!! ERROR FATAL AL INICIAR !!!!!!!!!!", [ 'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]); die(); } function handleDiscordCommand(Message $message, PDO $pdo, Logger $logger) { $text = trim($message->content); $userId = $message->author->id; if (strpos($text, '#') === 0) { $command = ltrim($text, '#'); $logger->info("[Comando] Usuario @{$message->author->username} solicitó: #{$command}"); try { $stmt = $pdo->prepare("SELECT message_content FROM recurrent_messages WHERE telegram_command = ?"); $stmt->execute([$command]); $template = $stmt->fetch(PDO::FETCH_ASSOC); if ($template) { $logger->info("[Comando] Plantilla encontrada para #{$command}."); $originalHtml = $template['message_content']; $sender = new DiscordSender(DISCORD_BOT_TOKEN); $sentMessageIds = $sender->sendMessage($message->channel_id, $originalHtml); if ($sentMessageIds && !empty($sentMessageIds)) { $firstMessageId = $sentMessageIds[0]; $logger->info("Mensajes enviados, primer ID: {$firstMessageId}"); // Añadir botones de traducción dinámicos $message->channel->messages->fetch($firstMessageId)->done(function (Message $sentMessage) use ($pdo, $logger, $command) { $langStmt = $pdo->query("SELECT language_code, language_name, flag_emoji FROM supported_languages WHERE is_active = 1"); $activeLangs = $langStmt->fetchAll(PDO::FETCH_ASSOC); if (count($activeLangs) > 1) { $components = []; $actionRow = ActionRow::new(); $buttonCount = 0; foreach ($activeLangs as $lang) { // Para plantillas, incluir el comando en el custom_id para traducir el contenido completo original $button = Button::new(Button::STYLE_SECONDARY, 'translate_template:' . $command . ':' . $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; } $builder = MessageBuilder::new()->setComponents($components); $sentMessage->edit($builder); $logger->info("Botones de traducción añadidos al mensaje {$sentMessage->id}"); } }); } } else { $message->reply("El comando `#{$command}` no fue encontrado."); } } catch (Throwable $e) { $logger->error("[Error Comando] Procesando #{$command}", ['error' => $e->getMessage()]); $message->reply("Ocurrió un error inesperado al procesar tu comando."); } } elseif (strpos($text, '/setlang') === 0) { $parts = explode(' ', $text, 2); if (count($parts) < 2 || strlen(trim($parts[1])) !== 2) { $message->reply("❌ Formato incorrecto. Usa: `/setlang es`"); return; } $newLangCode = strtolower(trim($parts[1])); $stmt = $pdo->prepare("UPDATE recipients SET language_code = ? WHERE platform_id = ? AND platform = 'discord'"); $stmt->execute([$newLangCode, $userId]); $message->reply("✅ Tu idioma ha sido establecido a '" . strtoupper($newLangCode) . "'."); } elseif (strpos($text, '/bienvenida') === 0) { $configStmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1"); $welcomeConfig = $configStmt->fetch(PDO::FETCH_ASSOC); if ($welcomeConfig && $welcomeConfig['is_active']) { $messageText = str_replace('{user_name}', $message->author->username, $welcomeConfig['message_text']); $converter = new HtmlToDiscordMarkdownConverter(); $markdownText = $converter->convert($messageText); $builder = MessageBuilder::new()->setContent($markdownText); $actionRow = ActionRow::new() ->addComponent(Button::new(Button::STYLE_LINK, $welcomeConfig['group_invite_link'])->setLabel($welcomeConfig['button_text'])); $builder->addComponent($actionRow); $message->channel->sendMessage($builder); } else { $message->reply("ℹ️ La función de bienvenida no está activa."); } } elseif (strpos($text, '/comandos') === 0) { $stmt = $pdo->query("SELECT telegram_command, name FROM recurrent_messages WHERE telegram_command IS NOT NULL AND telegram_command != '' ORDER BY name ASC"); $commands = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($commands)) { $message->reply("ℹ️ No hay comandos personalizados disponibles."); } else { $response = "**LISTA DE COMANDOS DISPONIBLES**\n\n"; foreach ($commands as $cmd) { $command = trim($cmd['telegram_command']); if (strpos($command, '#') !== 0) $command = '#' . $command; $name = trim($cmd['name']); $response .= "`" . $command . "` - " . $name . "\n"; } $response .= "\nℹ️ Escribe el comando para usarlo."; $message->channel->sendMessage($response); } } elseif (strpos($text, '/agente') === 0) { $prompt = trim(substr($text, strlen('/agente'))); $logger->info("[Comando] Usuario @{$message->author->username} solicitó: /agente con prompt: '{$prompt}'"); if (empty($prompt)) { $message->reply("Por favor, escribe tu consulta después de /agente."); return; } $n8nWebhookUrl = $_ENV['N8N_IA_WEBHOOK_URL_DISCORD'] ?? null; if ($n8nWebhookUrl) { $postData = [ 'chat_id' => $message->channel_id, 'user_id' => $message->author->id, 'message' => $prompt, 'name' => $message->author->username ]; $ch = curl_init($n8nWebhookUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData)); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_exec($ch); curl_close($ch); $logger->info("[Comando /agente] Prompt reenviado a n8n."); $message->reply("Tu solicitud ha sido enviada al agente. Recibirás una respuesta en breve."); } else { $logger->error("[Comando /agente] La variable N8N_IA_WEBHOOK_URL no está configurada."); $message->reply("No se pudo procesar tu solicitud. El servicio de agente no está configurado."); } } } function handleDiscordTranslation(Message $message, PDO $pdo, Logger $logger) { try { $text = $message->content; $attachments = $message->attachments; // Verificar si el mensaje tiene contenido de texto $hasTextContent = !empty(trim($text)); // Verificar si hay texto en los attachments (captions de imágenes/videos) $attachmentText = ''; if (!empty($attachments)) { foreach ($attachments as $attachment) { // Algunos attachments tienen propiedad 'description' o 'caption' if (!empty($attachment->description)) { $attachmentText .= $attachment->description . ' '; } if (!empty($attachment->caption)) { $attachmentText .= $attachment->caption . ' '; } } } // Combinar texto principal con texto de attachments $fullText = trim($text . ' ' . $attachmentText); $hasAnyText = !empty($fullText); // Verificar si el mensaje tiene solo imágenes/adjuntos sin texto $hasOnlyAttachments = !$hasAnyText && !empty($attachments) && count($attachments) > 0; // Si el mensaje no tiene texto o tiene solo imágenes, no mostrar botones de traducción if (!$hasAnyText || $hasOnlyAttachments) { $logger->info("[TRADUCCIÓN] Mensaje #{$message->id} no tiene contenido de texto, omitiendo botones de traducción."); return; } $translator = new Translate(LIBRETRANSLATE_URL); $detectedLang = $translator->detectLanguage(strip_tags($fullText)) ?? 'es'; // 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); // 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; } // 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 Traducción Discord]", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); } } /** * Función para preservar emojis, stickers y elementos de Discord durante la traducción */ function preserveDiscordElements($text) { $placeholders = []; $processedText = $text; // Patrones para detectar elementos de Discord (excluyendo emojis Unicode) $patterns = [ // Emojis personalizados de Discord <:name:id> '//', // Menciones de usuarios <@id> y <@!id> '/<@!?(\d+)>/', // Menciones de canales <#id> '/<#(\d+)>/', // Menciones de roles <@&id> '/<@&(\d+)>/', // Stickers y GIFs animados (pueden venir como URLs especiales) '/https?:\/\/(?:media|cdn)\.discordapp\.(?:com|net)\/(stickers|attachments)\/\S+/i' ]; $index = 0; foreach ($patterns as $pattern) { $processedText = preg_replace_callback($pattern, function($matches) use (&$placeholders, &$index) { $placeholder = "DISCORD_ELEMENT_{$index}"; $placeholders[$placeholder] = $matches[0]; $index++; return $placeholder; }, $processedText); } return [ 'text' => $processedText, 'placeholders' => $placeholders ]; } /** * Función para restaurar emojis, stickers y elementos de Discord después de la traducción */ function restoreDiscordElements($translatedText, $placeholders) { $restoredText = $translatedText; foreach ($placeholders as $placeholder => $originalElement) { $restoredText = str_replace($placeholder, $originalElement, $restoredText); } return $restoredText; }