Files
sistema_para_juego/bot_daemon.php

484 lines
23 KiB
PHP
Executable File
Raw Permalink 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 Daemon - New Version
*/
// Cargar dependencias
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/discord/src/DiscordSender.php';
require_once __DIR__ . '/discord/src/HtmlToDiscordMarkdownConverter.php';
require_once __DIR__ . '/discord/src/Translate.php';
require_once __DIR__ . '/discord/src/CommandLocker.php';
// --- Cargar variables de entorno ---
if (file_exists(__DIR__ . '/.env')) {
$lines = file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
if (strpos($line, '=') === false) continue;
list($key, $value) = explode('=', $line, 2);
$_ENV[trim($key)] = trim($value);
}
}
// Importar clases necesarias de DiscordPHP
use Discord\Discord;
use Discord\WebSockets\Intents;
use Discord\WebSockets\Event;
use Discord\Parts\Channel\Message;
use Discord\Parts\Interactions\Interaction;
use Discord\Builders\MessageBuilder;
use Discord\Parts\Embed\Embed;
use Discord\Parts\Guild\Member;
use Discord\Builders\Components\ActionRow;
use Discord\Builders\Components\Button;
// --- Logger y Conexión a BD ---
$logInteraction = function($message, $data = []) {
$pid = getmypid();
$logMessage = date('[Y-m-d H:i:s] ') . "[PID: {$pid}] " . $message . "\n";
if (!empty($data)) {
$logMessage .= "Datos: " . json_encode($data, JSON_PRETTY_PRINT) . "\n";
}
error_log($logMessage, 3, __DIR__ . '/logs/discord_bot_new.log');
};
function getDBConnection(): PDO {
$host = $_ENV['DB_HOST'] ?? '127.0.0.1';
$dbName = $_ENV['DB_NAME'] ?? 'db';
$user = $_ENV['DB_USER'] ?? 'root';
$pass = $_ENV['DB_PASS'] ?? '';
$port = $_ENV['DB_PORT'] ?? '3306';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;port=$port;dbname=$dbName;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => 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()]);
}
}