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, <<<<<<< HEAD 'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS | Intents::MESSAGE_CONTENT, 'logger' => $logger ======= '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 ] >>>>>>> 26414094d4262e5ab092028955a4f0de57092f43 ]); // 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; <<<<<<< HEAD // 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(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); } // 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) { >>>>>>> 26414094d4262e5ab092028955a4f0de57092f43 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 } // 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:')); $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 { <<<<<<< HEAD $translator = new Translate(LIBRETRANSLATE_URL); // Instanciar al inicio $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 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 (!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; ======= $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 . ' '; >>>>>>> 26414094d4262e5ab092028955a4f0de57092f43 } } } <<<<<<< HEAD // 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) ======= // 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 >>>>>>> 26414094d4262e5ab092028955a4f0de57092f43 $targetLangs = array_filter($activeLangs, function($lang) use ($detectedLang) { return $lang['language_code'] !== $detectedLang; }); <<<<<<< HEAD // 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 ======= if (empty($targetLangs)) { $logger->info("[TRADUCCIÓN] No hay idiomas disponibles para traducir desde '$detectedLang'"); return; } // Crear botones con banderas >>>>>>> 26414094d4262e5ab092028955a4f0de57092f43 $components = []; $actionRow = ActionRow::new(); $buttonCount = 0; foreach ($targetLangs as $lang) { <<<<<<< HEAD $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++; ======= $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 >>>>>>> 26414094d4262e5ab092028955a4f0de57092f43 if ($buttonCount % 5 === 0) { $components[] = $actionRow; $actionRow = ActionRow::new(); } } <<<<<<< HEAD ======= >>>>>>> 26414094d4262e5ab092028955a4f0de57092f43 if ($buttonCount % 5 !== 0) { $components[] = $actionRow; } <<<<<<< HEAD // 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("[TRANSLATION_BUTTONS] Error al procesar mensaje para botones de traducción", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); ======= // 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()]); >>>>>>> 26414094d4262e5ab092028955a4f0de57092f43 } } /** * 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; }