Files
sistema_funcionando_lastwar/discord_bot.php
nickpons666 7953a56501 Merge: Complete merge of remote changes, including user's requested additions.
This commit completes the merge process, incorporating remote changes that conflicted with local modifications. It also stages and commits all remaining modified and untracked files as per the user's instruction to 'upload everything without exception'.
2026-02-08 16:33:43 -06:00

963 lines
50 KiB
PHP
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// discord_bot.php
// 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__ . '/discord/DiscordSender.php';
require_once __DIR__ . '/discord/converters/HtmlToDiscordMarkdownConverter.php';
require_once __DIR__ . '/src/Translate.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();
}
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('/<a?:(\w+):(\d+)>/', $originalContent, $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $match) {
$emojiMap[$match[1]] = $match[0];
}
$contentForTranslation = preg_replace('/<a?:(\w+):(\d+)>/', '', $originalContent);
$contentForTranslation = trim($contentForTranslation);
if (empty($contentForTranslation)) {
$interaction->editOriginalResponse(MessageBuilder::new()->setContent("⚠️ El mensaje no contiene texto traducible."));
return;
}
try {
$translator = new Translate(LIBRETRANSLATE_URL);
$detectionResultForSource = $translator->detectLanguage($contentForTranslation);
$sourceLang = $detectionResultForSource[0]['language'] ?? 'es';
if ($sourceLang === $targetLang) {
$interaction->editOriginalResponse(MessageBuilder::new()->setContent("⚠️ El mensaje ya está en el idioma seleccionado ({$targetLang})."));
return;
}
$translatedText = $translator->translateText($contentForTranslation, $sourceLang, $targetLang);
if (!empty($translatedText)) {
foreach ($emojiMap as $emojiTag) {
$translatedText .= ' ' . $emojiTag;
}
$translatedText = trim($translatedText);
$stmt = $pdo->prepare("SELECT flag_emoji, language_name FROM supported_languages WHERE language_code = ? AND is_active = 1");
$stmt->execute([$targetLang]);
$langInfo = $stmt->fetch(PDO::FETCH_ASSOC);
$flag = $langInfo['flag_emoji'] ?? '🏳️';
$langName = $langInfo['language_name'] ?? strtoupper($targetLang);
$embed = new Embed($discord);
$embed->setTitle("{$flag} Traducción a {$langName}");
$embed->setDescription($translatedText);
$embed->setColor("#5865F2");
$embed->setFooter("Traducido de {$sourceLang} • Solo tú puedes ver esto");
$imageUrl = null;
if ($originalMessage->attachments !== null && count($originalMessage->attachments) > 0) {
$firstAttachment = $originalMessage->attachments->first();
if ($firstAttachment && isset($firstAttachment->url)) {
$contentType = $firstAttachment->content_type ?? '';
if (strpos($contentType, 'image/') === 0 || strpos($contentType, 'video/') === 0) {
$imageUrl = $firstAttachment->url;
}
}
}
if (!$imageUrl && $originalMessage->stickers !== null && count($originalMessage->stickers) > 0) {
$firstSticker = $originalMessage->stickers->first();
if ($firstSticker && isset($firstSticker->image_url)) {
$imageUrl = $firstSticker->image_url;
}
}
if ($imageUrl) {
$embed->setImage($imageUrl);
}
// 3. Editar la respuesta original con el resultado
$builder = MessageBuilder::new()->addEmbed($embed);
$interaction->sendFollowUpMessage($builder, true);
$logger->info("[TRANSLATION_BUTTON] Usuario {$userId} tradujo mensaje #{$originalMessageId} de {$sourceLang} a {$targetLang}");
} else {
$interaction->editOriginalResponse(MessageBuilder::new()->setContent("⚠️ No se pudo traducir el mensaje."));
}
} catch (\Throwable $e) {
$logger->error("[TRANSLATION_BUTTON] Error al traducir", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
$interaction->editOriginalResponse(MessageBuilder::new()->setContent("❌ Error al traducir: " . $e->getMessage()));
}
}, function ($error) use ($interaction, $logger) {
$logger->error("[TRANSLATION_BUTTON] Error al obtener mensaje original", ['error' => $error->getMessage()]);
$interaction->editOriginalResponse(MessageBuilder::new()->setContent("❌ No se pudo obtener el mensaje original."));
});
}, function ($error) use ($logger, $interaction) {
$logger->error("[TRANSLATION_BUTTON] Error al reconocer (defer) la interacción.", ['error' => $error->getMessage()]);
// If even deferring fails, we can't edit the response.
});
return; // Finalizar para no procesar otros ifs
}
// Traducción de plantillas completas (comandos #)
if (strpos($customId, 'translate_template:') === 0) {
$payload = substr($customId, strlen('translate_template:'));
$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('/<a?:(\w+):(\d+)>/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>
'/<a?:([a-zA-Z0-9_]+):(\d+)>/',
// 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;
}