Commit inicial con archivos existentes

This commit is contained in:
2026-01-17 16:14:00 -06:00
parent 48671dc88e
commit 4c48c279de
2539 changed files with 2412708 additions and 0 deletions

View File

@@ -0,0 +1,765 @@
<?php
// discord_bot.php
// Pausa intencionada para evitar condiciones de carrera en el arranque
sleep(3);
// Cargar configuración y dependencias
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../includes/db.php';
require_once __DIR__ . '/../../includes/logger.php';
require_once __DIR__ . '/../../src/DiscordSender.php';
require_once __DIR__ . '/../../src/HtmlToDiscordMarkdownConverter.php';
require_once __DIR__ . '/../../src/Translate.php';
require_once __DIR__ . '/../../src/CommandLocker.php';
// Importar clases necesarias
use Discord\Discord;
use Discord\WebSockets\Intents;
use Discord\WebSockets\Event;
use Discord\Parts\Channel\Message;
use Discord\Parts\Interactions\Interaction;
use Discord\Parts\WebSockets\MessageReaction;
use Discord\Builders\MessageBuilder;
use Discord\Parts\Embed\Embed;
use Discord\Parts\Guild\Member;
use Discord\Builders\Components\ActionRow;
use Discord\Builders\Components\Button;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('DiscordBot');
$logger->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();
}
/**
* Guarda un destinatario (usuario o canal) en la base de datos si no existe.
*
* @param PDO $pdo La conexión a la base de datos.
* @param Logger $logger El logger para registrar eventos.
* @param string $platform La plataforma (ej. 'discord').
* @param string $type El tipo de destinatario ('user' o 'channel').
* @param string $platformId El ID de la plataforma.
* @param string|null $name El nombre del destinatario.
*/
function saveRecipientIfNotExists(PDO $pdo, Logger $logger, string $platform, string $type, string $platformId, ?string $name): void {
$logger->debug("[Registro BD] Verificando destinatario...", ['type' => $type, 'id' => $platformId, 'name' => $name]);
if (empty($platformId) || empty($name)) {
$logger->warning("[Registro BD] Se omitió guardar destinatario por ID o nombre vacío.", ['type' => $type, 'id' => $platformId, 'name' => $name]);
return;
}
try {
// Verificar si el destinatario ya existe
$stmt = $pdo->prepare("SELECT id FROM recipients WHERE platform = ? AND platform_id = ?");
$stmt->execute([$platform, $platformId]);
if ($stmt->rowCount() === 0) {
// Si no existe, insertarlo
$insertStmt = $pdo->prepare(
"INSERT INTO recipients (platform, type, platform_id, name) VALUES (?, ?, ?, ?)"
);
$insertStmt->execute([$platform, $type, $platformId, $name]);
$logger->info("[Registro BD] Nuevo destinatario guardado: Tipo={$type}, ID={$platformId}, Nombre={$name}");
}
} catch (Throwable $e) {
// Registrar cualquier error durante la operación de base de datos
$logger->error("[Registro BD] Error al guardar destinatario: " . $e->getMessage(), [
'platformId' => $platformId,
'name' => $name,
'trace' => $e->getTraceAsString()
]);
}
}
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::MESSAGE_CONTENT,
'logger' => $logger
]);
$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 para nuevos miembros en el servidor
// Nota: DiscordPHP puede enviar Discord\Parts\User\Member en lugar de Discord\Parts\Guild\Member
// Por eso NO usamos type hint aquí
$discord->on(Event::GUILD_MEMBER_ADD, function ($member, Discord $discord) use ($pdo, $logger) {
$logger->info("[NUEVO MIEMBRO] Usuario {$member->user->username} ({$member->id}) se ha unido al servidor.");
// Asegurarse de que el usuario existe en la BD
saveRecipientIfNotExists($pdo, $logger, 'discord', 'user', $member->id, $member->user->username);
try {
// 1. Obtener idiomas activos de la BD
$stmt = $pdo->query("SELECT language_code, language_name, flag_emoji FROM supported_languages WHERE is_active = 1 ORDER BY language_name ASC");
$activeLangs = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($activeLangs)) {
$logger->warning("[BIENVENIDA] No se envió mensaje de bienvenida porque no hay idiomas activos.");
return;
}
// 2. Construir el mensaje de bienvenida multi-idioma
$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'] . ' ' : '';
// Traducir si no es el idioma base (español)
$translatedDesc = ($langCode === 'es')
? $baseDescription
: $translator->translateText($baseDescription, 'es', $langCode);
if ($translatedDesc) {
$fullDescription .= $flag . $translatedDesc . "\n\n";
}
}
$embed = new Embed($discord);
$embed->setTitle($baseTitle); // El título se mantiene simple y en español
$embed->setDescription(trim($fullDescription));
$embed->setColor("#5865F2"); // Discord Blurple
if ($member->user && $member->user->avatar) {
$avatarUrl = "https://cdn.discordapp.com/avatars/{$member->user->id}/{$member->user->avatar}.png";
$embed->setThumbnail($avatarUrl);
}
$builder = MessageBuilder::new()->addEmbed($embed);
// 3. Construir los botones en filas de 5
$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 % 5 === 0) {
$actionRows[] = $currentRow;
$currentRow = ActionRow::new();
}
}
if ($buttonCount % 5 !== 0) {
$actionRows[] = $currentRow;
}
foreach ($actionRows as $row) {
$builder->addComponent($row);
}
// 4. Enviar el mensaje como Mensaje Directo (DM) al nuevo usuario.
$member->sendMessage($builder)->done(function () use ($logger, $member) {
$logger->info("[BIENVENIDA] Mensaje de selección de idioma enviado por DM a {$member->user->username}.");
}, function ($error) use ($logger, $member) {
$logger->error("[BIENVENIDA] No se pudo enviar DM de bienvenida a {$member->user->username}.", ['error' => $error]);
});
return;
} catch (Throwable $e) {
$logger->error("[BIENVENIDA] Error fatal al procesar nuevo miembro.", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
}
});
// Evento para manejar interacciones (clics en botones)
$discord->on(Event::INTERACTION_CREATE, function (Interaction $interaction, Discord $discord) use ($pdo, $logger) {
// Guardar usuario y canal que interactúan
saveRecipientIfNotExists($pdo, $logger, 'discord', '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($pdo, $logger, 'discord', 'channel', $interaction->channel_id, $channelName);
// 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 {
// MANEJAR SELECCIÓN DE IDIOMA DE BIENVENIDA
if (strpos($customId, 'set_lang_') === 0) {
try {
$langCode = substr($customId, strlen('set_lang_'));
// Actualizar la base de datos
$stmt = $pdo->prepare("UPDATE recipients SET language_code = ? WHERE platform_id = ? AND platform = 'discord'");
$stmt->execute([$langCode, $userId]);
// Obtener el nombre del idioma para el mensaje de confirmación
$langNameStmt = $pdo->prepare("SELECT language_name FROM supported_languages WHERE language_code = ?");
$langNameStmt->execute([$langCode]);
$langName = $langNameStmt->fetchColumn() ?: strtoupper($langCode);
// Crear el mensaje de confirmación
$confirmEmbed = new Embed($discord);
$confirmEmbed->setTitle("✅ Idioma Configurado");
$confirmEmbed->setDescription("Tu idioma ha sido establecido a: **" . htmlspecialchars($langName) . "**");
$confirmEmbed->setColor("#57F287"); // Discord Green
// Actualizar el mensaje original para mostrar la confirmación y quitar los botones
$builder = MessageBuilder::new()->addEmbed($confirmEmbed);
// Acknowledge the interaction and then edit the message
$interaction->acknowledge()->done(function() use ($interaction, $builder, $logger, $langCode, $userId) {
$interaction->message->edit($builder)->done(function() use ($logger, $langCode, $userId) {
$logger->info("[BIENVENIDA] El usuario $userId ha establecido su idioma a '$langCode' y el mensaje fue editado.");
}, function ($error) use ($logger, $langCode, $userId, $interaction) {
$logger->error("[BIENVENIDA] Error al editar el mensaje original para {$userId}.", ['lang' => $langCode, 'error' => $error]);
// Fallback response: if editing fails, send a new ephemeral message
$interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ Tu idioma ha sido guardado, pero no se pudo actualizar el mensaje de bienvenida. Error: " . $error->getMessage()), true);
});
}, function ($error) use ($logger, $langCode, $userId, $interaction) {
$logger->error("[BIENVENIDA] Error al reconocer interacción para {$userId}.", ['lang' => $langCode, 'error' => $error]);
$interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ Error al procesar tu solicitud. Por favor, inténtalo de nuevo."), true);
});
$logger->info("[BIENVENIDA] El usuario $userId ha establecido su idioma a '$langCode'.");
} catch (Throwable $e) {
$logger->error("[BIENVENIDA] Error al procesar botón de idioma.", ['customId' => $customId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
$interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ Hubo un error al guardar tu selección de idioma."), true);
}
return; // Finalizar para no procesar otros ifs
}
if (strpos($customId, 'translate_auto:') === 0) {
// Extraer el ID del mensaje original del custom_id
$originalMessageId = substr($customId, strlen('translate_auto:'));
// Obtener el idioma preferido del usuario desde la tabla recipients
$stmt = $pdo->prepare("SELECT language_code FROM recipients WHERE platform_id = ? AND platform = 'discord'");
$stmt->execute([$userId]);
$userLang = $stmt->fetchColumn();
// Si no tiene idioma configurado, usar español por defecto
if (!$userLang) {
$userLang = 'es';
$logger->info("[TRANSLATION] Usuario {$userId} no tiene idioma configurado, usando 'es' por defecto");
}
$targetLang = $userLang;
// Obtener el mensaje original usando su ID
$interaction->channel->messages->fetch($originalMessageId)->done(function ($originalMessage) use ($interaction, $discord, $pdo, $logger, $userId, $targetLang) {
$originalContent = trim((string) ($originalMessage->content ?? ''));
if (empty($originalContent)) {
$interaction->respondWithMessage(
MessageBuilder::new()->setContent("❌ No se encontró contenido para traducir."),
true
);
return;
}
try {
$translator = new Translate();
$sourceLang = $translator->detectLanguage($originalContent) ?? 'es';
// Evitar traducir si ya está en el idioma destino
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)) {
// Obtener información del idioma
$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);
// Crear embed para la traducción
$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");
// Si el mensaje original tiene imágenes, agregar la primera como imagen del embed
if (count($originalMessage->attachments) > 0) {
$firstAttachment = $originalMessage->attachments->first();
if ($firstAttachment && isset($firstAttachment->url)) {
// Verificar si es una imagen
$contentType = $firstAttachment->content_type ?? '';
if (strpos($contentType, 'image/') === 0) {
$embed->setImage($firstAttachment->url);
}
}
}
// Enviar como mensaje EFÍMERO (solo visible para quien hizo clic)
$builder = MessageBuilder::new()->addEmbed($embed);
$interaction->respondWithMessage($builder, true);
$logger->info("[TRANSLATION] Usuario {$userId} tradujo mensaje de {$sourceLang} a {$targetLang}");
} else {
$interaction->respondWithMessage(
MessageBuilder::new()->setContent("⚠️ No se pudo traducir el mensaje."),
true
);
}
} catch (\Throwable $e) {
$logger->error("[TRANSLATION] Error al traducir", ['error' => $e->getMessage()]);
$interaction->respondWithMessage(
MessageBuilder::new()->setContent("❌ Error al traducir: " . $e->getMessage()),
true
);
}
}, function ($error) use ($interaction, $logger) {
$logger->error("[TRANSLATION] Error al obtener mensaje original", ['error' => $error->getMessage()]);
$interaction->respondWithMessage(
MessageBuilder::new()->setContent("❌ No se pudo obtener el mensaje original."),
true
);
});
return;
}
// 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();
$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 un botón.", ['customId' => $customId, 'error' => $e->getMessage()]);
// Mensaje genérico para otros botones que no sean de idioma
$interaction->respondWithMessage(MessageBuilder::new()->setContent("Hubo un error al procesar esta acción."), true);
}
});
// Evento para manejar mensajes
$discord->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) use ($pdo, $logger) {
if ($message->author->bot) return;
// Guardar usuario
saveRecipientIfNotExists($pdo, $logger, 'discord', 'user', $message->author->id, $message->author->username);
// Guardar canal con nombre mejorado
$channelName = $message->channel->name;
if ($message->channel->is_private) {
$channelName = 'DM con ' . $message->author->username;
} elseif ($message->channel->guild) {
$channelName = $message->channel->guild->name . ' - ' . $channelName;
}
saveRecipientIfNotExists($pdo, $logger, 'discord', 'channel', $message->channel->id, $channelName);
$logger->info("[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) {
// --- 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) {
// El registro ya se hizo con saveRecipientIfNotExists, solo necesitamos el modo
$logger->info("[NUEVO USUARIO DM] Usuario $userId no tenía modo. Asignando 'agent'.");
$updateStmt = $pdo->prepare("UPDATE recipients SET chat_mode = 'agent' WHERE platform_id = ? AND platform = 'discord'");
$updateStmt->execute([$userId]);
$userChatMode = 'agent';
}
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()
]);
}
});
$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 {
$translator = new Translate();
$text = $message->content;
// 1. Detectar idioma original
$detectedLang = $translator->detectLanguage(strip_tags($text)) ?? 'es';
// 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 un solo botón de traducción con el ID del mensaje original
$button = Button::new(Button::STYLE_PRIMARY, 'translate_auto:' . $message->id)
->setLabel('Traducir')
->setEmoji('🌐');
$actionRow = ActionRow::new()->addComponent($button);
// 6. Enviar mensaje del bot con botón como respuesta al mensaje original
$builder = MessageBuilder::new()
->setContent('🌐 Traducción disponible')
->setComponents([$actionRow]);
$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()]);
}
}