PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; return new PDO($dsn, $user, $pass, $options); } $logInteraction("Iniciando bot de Discord..."); try { $db = getDBConnection(); $logInteraction("Conexión a la base de datos establecida."); } catch (\Throwable $e) { $logInteraction("Error fatal al conectar a la base de datos", ['error' => $e->getMessage()]); die("Error de base de datos."); } if (empty($_ENV['DISCORD_BOT_TOKEN'])) { $logInteraction("Error Fatal: La variable de entorno DISCORD_BOT_TOKEN no está definida o está vacía."); die(); } function saveRecipientIfNotExists(PDO $db, callable $logInteraction, string $type, string $discordId, ?string $name): void { $logInteraction("[Registro BD] Verificando destinatario...", ['type' => $type, 'id' => $discordId, 'name' => $name]); if (empty($discordId) || empty($name)) { $logInteraction("Se omitió guardar destinatario por ID o nombre vacío.", ['type' => $type, 'id' => $discordId, 'name' => $name]); return; } try { $stmt = $db->prepare("SELECT id FROM destinatarios_discord WHERE discord_id = ?"); $stmt->execute([$discordId]); if ($stmt->rowCount() === 0) { $insertStmt = $db->prepare( "INSERT INTO destinatarios_discord (tipo, discord_id, nombre) VALUES (?, ?, ?)" ); $insertStmt->execute([$type, $discordId, $name]); $logInteraction("Nuevo destinatario guardado", ['tipo' => $type, 'discord_id' => $discordId, 'nombre' => $name]); } } catch (Throwable $e) { $logInteraction("Error al guardar destinatario: " . $e->getMessage(), [ 'discord_id' => $discordId, 'nombre' => $name, 'trace' => $e->getTraceAsString() ]); } } try { $discord = new Discord([ 'token' => $_ENV['DISCORD_BOT_TOKEN'], 'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS | Intents::MESSAGE_CONTENT, 'logger' => null ]); $discord->on('ready', function (Discord $discord) use ($logInteraction) { $logInteraction("=================================================="); $logInteraction("Bot conectado y listo para escuchar!"); $logInteraction("Usuario: {$discord->user->username}#{$discord->user->discriminator}"); $logInteraction("=================================================="); }); $discord->on(Event::GUILD_MEMBER_ADD, function ($member, Discord $discord) use ($db, $logInteraction) { $logInteraction("[NUEVO MIEMBRO] Usuario {$member->user->username} ({$member->id}) se ha unido al servidor."); saveRecipientIfNotExists($db, $logInteraction, 'user', $member->id, $member->user->username); try { $stmt = $db->query("SELECT codigo as language_code, nombre as language_name, bandera as flag_emoji FROM idiomas WHERE activo = 1 ORDER BY nombre ASC"); $activeLangs = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($activeLangs)) { $logInteraction("ADVERTENCIA: [BIENVENIDA] No se envió mensaje de bienvenida porque no hay idiomas activos."); return; } $translator = new Translate(); $baseTitle = "👋 ¡Hola, {$member->user->username}! Bienvenido/a a el Discord de Cereal Kiiller."; $baseDescription = "Por favor, selecciona tu idioma preferido:"; $fullDescription = ""; foreach($activeLangs as $lang) { $langCode = $lang['language_code']; $flag = $lang['flag_emoji'] ? $lang['flag_emoji'] . ' ' : ''; $translatedDesc = ($langCode === 'es') ? $baseDescription : $translator->translateText($baseDescription, 'es', $langCode); if ($translatedDesc) { $fullDescription .= $flag . $translatedDesc . "\n\n"; } } $embed = new Embed($discord); $embed->setTitle($baseTitle); $embed->setDescription(trim($fullDescription)); $embed->setColor("#5865F2"); if ($member->user && $member->user->avatar) { $embed->setThumbnail($member->user->getAvatar()); } $builder = MessageBuilder::new()->addEmbed($embed); $actionRows = []; $currentRow = ActionRow::new(); $buttonCount = 0; foreach ($activeLangs as $lang) { $button = Button::new(Button::STYLE_SECONDARY, 'set_lang_' . $lang['language_code']) ->setLabel($lang['language_name']); if (!empty($lang['flag_emoji'])) { $button->setEmoji($lang['flag_emoji']); } $currentRow->addComponent($button); $buttonCount++; if ($buttonCount > 0 && $buttonCount % 5 === 0) { $actionRows[] = $currentRow; $currentRow = ActionRow::new(); } } if ($buttonCount % 5 !== 0) { $actionRows[] = $currentRow; } foreach ($actionRows as $row) { $builder->addComponent($row); } $member->sendMessage($builder)->then( function () use ($logInteraction, $member) { $logInteraction("[BIENVENIDA] Mensaje de selección de idioma enviado por DM a {$member->user->username}."); }, function ($error) use ($logInteraction, $member) { $logInteraction("ERROR: [BIENVENIDA] No se pudo enviar DM de bienvenida a {$member->user->username}.", ['error' => $error]); } ); return; } catch (Throwable $e) { $logInteraction("ERROR: [BIENVENIDA] Error fatal al procesar nuevo miembro.", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); } }); $discord->on(Event::INTERACTION_CREATE, function (Interaction $interaction, Discord $discord) use ($db, $logInteraction) { saveRecipientIfNotExists($db, $logInteraction, 'user', $interaction->user->id, $interaction->user->username); $channelName = 'Canal Desconocido'; if ($interaction->channel) { $channelName = $interaction->channel->name; if ($interaction->channel->guild) { $channelName = $interaction->channel->guild->name . ' - ' . $channelName; } elseif ($interaction->channel->is_private) { $channelName = 'DM con ' . $interaction->user->username; } } saveRecipientIfNotExists($db, $logInteraction, 'channel', $interaction->channel_id, $channelName); $type = (int) ($interaction->type ?? 0); $componentType = (int) ($interaction->data->component_type ?? 0); if ($type !== 3 || $componentType !== 2) return; $customId = $interaction->data->custom_id; $userId = $interaction->user->id; $logInteraction("[INTERACCION] Usuario $userId hizo clic en el botón: $customId"); try { if (strpos($customId, 'set_lang_') === 0) { $langCode = substr($customId, strlen('set_lang_')); $stmt = $db->prepare("UPDATE destinatarios_discord SET idioma_detectado = ? WHERE discord_id = ?"); $stmt->execute([$langCode, $userId]); $langNameStmt = $db->prepare("SELECT nombre FROM idiomas WHERE codigo = ?"); $langNameStmt->execute([$langCode]); $langName = $langNameStmt->fetchColumn() ?: strtoupper($langCode); $confirmEmbed = new Embed($discord); $confirmEmbed->setTitle("✅ Idioma Configurado"); $confirmEmbed->setDescription("Tu idioma ha sido establecido a: **" . htmlspecialchars($langName) . "**"); $confirmEmbed->setColor("#57F287"); $builder = MessageBuilder::new()->addEmbed($confirmEmbed); $interaction->acknowledge()->then(function() use ($interaction, $builder, $logInteraction, $langCode, $userId) { $interaction->message->edit($builder)->then(function() use ($logInteraction, $langCode, $userId) { $logInteraction("[BIENVENIDA] El usuario $userId ha establecido su idioma a '$langCode' y el mensaje fue editado."); }, function ($error) use ($logInteraction, $langCode, $userId) { $logInteraction("ERROR: [BIENVENIDA] Error al editar el mensaje original para {$userId}.", ['lang' => $langCode, 'error' => $error]); }); }); return; } if (strpos($customId, 'translate_auto:') === 0) { $originalMessageId = substr($customId, strlen('translate_auto:')); $stmt = $db->prepare("SELECT idioma_detectado FROM destinatarios_discord WHERE discord_id = ?"); $stmt->execute([$userId]); $targetLang = $stmt->fetchColumn() ?: 'es'; $interaction->channel->messages->fetch($originalMessageId)->then( function ($originalMessage) use ($interaction, $discord, $db, $logInteraction, $userId, $targetLang) { $originalContent = trim((string) ($originalMessage->content ?? '')); if (empty($originalContent)) { $interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ No se encontró contenido para traducir."), true); return; } $translator = new Translate(); $sourceLang = $translator->detectLanguage($originalContent) ?? 'es'; if ($sourceLang === $targetLang) { $interaction->respondWithMessage(MessageBuilder::new()->setContent("⚠️ El mensaje ya está en tu idioma ({$targetLang})."), true); return; } $translatedText = $translator->translateText($originalContent, $sourceLang, $targetLang); if (!empty($translatedText)) { $stmt = $db->prepare("SELECT bandera, nombre FROM idiomas WHERE codigo = ? AND activo = 1"); $stmt->execute([$targetLang]); $langInfo = $stmt->fetch(PDO::FETCH_ASSOC); $flag = $langInfo['bandera'] ?? '🏳️'; $langName = $langInfo['nombre'] ?? 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"); if (count($originalMessage->attachments) > 0) { $firstAttachment = $originalMessage->attachments->first(); if ($firstAttachment && isset($firstAttachment->url) && strpos($firstAttachment->content_type ?? '', 'image/') === 0) { $embed->setImage($firstAttachment->url); } } $builder = MessageBuilder::new() ->setContent("Aquí tienes tu traducción (solo visible para ti):") ->addEmbed($embed); $interaction->respondWithMessage($builder, true); $logInteraction("[TRANSLATION] Usuario {$userId} tradujo mensaje de {$sourceLang} a {$targetLang}"); } else { $interaction->respondWithMessage(MessageBuilder::new()->setContent("⚠️ No se pudo traducir el mensaje."), true); } }, function ($error) use ($interaction, $logInteraction) { $logInteraction("ERROR: [TRANSLATION] Error al obtener mensaje original", ['error' => $error->getMessage()]); $interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ No se pudo obtener el mensaje original para traducir."), true); } ); return; } if (strpos($customId, 'translate_template:') === 0) { // Not implemented in this merge for now to keep it simple } if ($customId === 'platicar_bot' || $customId === 'usar_ia') { $newMode = ($customId === 'platicar_bot') ? 'bot' : 'ia'; $stmt = $db->prepare("UPDATE destinatarios_discord SET chat_mode = ? WHERE discord_id = ?"); $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); $logInteraction("[MODO AGENTE] Usuario $userId cambió al modo: $newMode"); } } catch (Throwable $e) { $logInteraction("ERROR: [INTERACCION] Error al procesar un botón.", ['customId' => $customId, 'error' => $e->getMessage()]); $interaction->respondWithMessage(MessageBuilder::new()->setContent("Hubo un error al procesar esta acción."), true); } }); $discord->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) use ($db, $logInteraction) { if ($message->author->bot) return; saveRecipientIfNotExists($db, $logInteraction, 'user', $message->author->id, $message->author->username); $channelName = $message->channel->is_private ? 'DM con ' . $message->author->username : $message->channel->name; if (!$message->channel->is_private && $message->channel->guild) { $channelName = $message->channel->guild->name . ' - ' . $channelName; } saveRecipientIfNotExists($db, $logInteraction, 'channel', $message->channel->id, $channelName); $logInteraction("[Mensaje Recibido] En canal '{$channelName}' de @{$message->author->username}: {$message->content}"); $isPrivateChat = $message->channel->is_private; $userId = $message->author->id; $content = $message->content; try { if ($isPrivateChat) { $stmt = $db->prepare("SELECT chat_mode FROM destinatarios_discord WHERE discord_id = ?"); $stmt->execute([$userId]); $userChatMode = $stmt->fetchColumn(); if ($userChatMode === false) { $updateStmt = $db->prepare("UPDATE destinatarios_discord SET chat_mode = 'agent' WHERE discord_id = ?"); $updateStmt->execute([$userId]); $userChatMode = 'agent'; } if (trim($content) === '/agente') { $stmt = $db->prepare("UPDATE destinatarios_discord SET chat_mode = 'agent' WHERE discord_id = ?"); $stmt->execute([$userId]); $userChatMode = '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': $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_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($postData), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_TIMEOUT => 10 ]); curl_exec($ch); curl_close($ch); } return; case 'bot': break; } } if (strpos($content, '#') === 0 || strpos($content, '/') === 0) { handleDiscordCommand($message, $db, $logInteraction); } else if (strtolower($content) === '!ping') { $message->reply('pong!'); } else { handleDiscordTranslation($message, $db, $logInteraction); } } catch (Throwable $e) { $logInteraction("ERROR FATAL CAPTURADO", ['error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]); } }); $discord->run(); } catch (Throwable $e) { $logInteraction("CRITICAL: ERROR FATAL AL INICIAR", ['error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]); die(); } function handleDiscordCommand(Message $message, PDO $db, $logInteraction) { $text = trim($message->content); if (strpos($text, '#') === 0) { $command = ltrim($text, '#'); // Command is "Saludos" $logInteraction("[Comando] Usuario @{$message->author->username} solicitó: #{$command}"); try { // Step 1: Look up command in comandos_discord to get plantilla_id $stmt = $db->prepare("SELECT plantilla_id FROM comandos_discord WHERE comando = ?"); $stmt->execute([$command]); $command_data = $stmt->fetch(PDO::FETCH_ASSOC); if ($command_data && $command_data['plantilla_id']) { $plantilla_id = $command_data['plantilla_id']; // Step 2: Get template content from plantillas_discord $stmt = $db->prepare("SELECT contenido FROM plantillas_discord WHERE id = ?"); $stmt->execute([$plantilla_id]); $template = $stmt->fetch(PDO::FETCH_ASSOC); if ($template) { $sender = new DiscordSender($_ENV['DISCORD_BOT_TOKEN']); $sender->sendMessage($message->channel_id, $template['contenido']); } else { $message->reply("El comando `#{$command}` fue encontrado, pero la plantilla asociada (ID: {$plantilla_id}) no existe."); } } else { $message->reply("El comando `#{$command}` no fue encontrado."); } } catch (Throwable $e) { $logInteraction("ERROR: [Comando] Procesando #{$command}", ['error' => $e->getMessage()]); $message->reply("Ocurrió un error inesperado al procesar tu comando."); } } elseif (strpos($text, '/comandos') === 0) { $stmt = $db->query("SELECT comando, nombre FROM plantillas_discord WHERE comando IS NOT NULL AND comando != '' ORDER BY nombre ASC"); $commands = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($commands)) { $message->reply("ℹ️ No hay comandos personalizados disponibles."); } else { $response = "**LISTA DE COMANDOS DISPONIBLES (Modo Debug)**\n\n"; foreach ($commands as $cmd) { $raw_command = $cmd['comando']; // The raw value from DB $display_command = trim($raw_command); if (strpos($display_command, '#') !== 0 && strpos($display_command, '/') !== 0) { $display_command = '#' . $display_command; } $response .= "`" . $display_command . "` (DB: `" . $raw_command . "`) - " . trim($cmd['nombre']) . "\n"; } $message->channel->sendMessage($response); } } } function handleDiscordTranslation(Message $message, PDO $db, $logInteraction) { try { $langStmt = $db->query("SELECT codigo FROM idiomas WHERE activo = 1"); $activeLangsCount = $langStmt->rowCount(); if ($activeLangsCount < 2) return; $button = Button::new(Button::STYLE_PRIMARY, 'translate_auto:' . $message->id) ->setLabel('Traducir / Translate') ->setEmoji('🌐'); $actionRow = ActionRow::new()->addComponent($button); $builder = MessageBuilder::new()->addComponent($actionRow); $message->reply($builder); } catch (Throwable $e) { $logInteraction("ERROR: [TRANSLATION_BUTTONS]", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); } }