Files
lastwar/discord_bot.php
nickpons666 e8912bdb63 Mejoras en el envío de plantillas con imágenes
- Agregar detección de URLs de Discord gifts para evitar botones de traducción
- Enviar imágenes en orden correcto (texto-imagen-texto-imagen) en Discord y Telegram
- Usar APP_URL del .env para las URLs de imágenes
- Agregar funciones sendContentWithOrderedImagesAndButtons en ambos bots
2026-02-19 19:12:28 -06:00

588 lines
23 KiB
PHP
Executable File

<?php
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/env_loader.php';
require_once __DIR__ . '/includes/emoji_helper.php';
use Discord\Discord;
use Discord\Parts\Channel\Message;
use Discord\Parts\Guild\Guild;
use Discord\Parts\User\Member;
use Discord\WebSockets\Intents;
use Discord\WebSockets\Event;
$token = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
$discord = new Discord([
'token' => $token,
'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS | Intents::MESSAGE_CONTENT,
]);
$discord->on('ready', function (Discord $discord) {
echo "Bot de Discord conectado como: {$discord->user->username}" . PHP_EOL;
$guild = $discord->guilds->first();
if ($guild) {
echo "Servidor: {$guild->name}" . PHP_EOL;
}
});
$discord->on(Event::GUILD_MEMBER_ADD, function (Member $member, Discord $discord) {
echo "Nuevo miembro: {$member->user->username}" . PHP_EOL;
try {
$pdo = getDbConnection();
$stmt = $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)
");
$stmt->execute([
$member->user->id,
$member->user->username
]);
echo "Usuario registrado en la base de datos" . PHP_EOL;
if (!isExistingDiscordUser($pdo, $member->user->id)) {
sendDiscordWelcomeMessage($pdo, $member, $discord);
}
} catch (Exception $e) {
echo "Error al registrar usuario: " . $e->getMessage() . PHP_EOL;
}
});
$discord->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) {
if ($message->author->bot) {
return;
}
$content = $message->content;
$channelId = $message->channel_id;
$userId = $message->author->id;
$username = $message->author->username;
try {
$pdo = getDbConnection();
$isNewUser = !isExistingDiscordUser($pdo, $userId);
registerDiscordUser($pdo, $message->author);
if ($isNewUser) {
sendDiscordWelcomeMessageOnMessage($pdo, $message, $username);
}
// Ignorar si es solo contenido vacío sin texto (solo attachments/GIFs/imágenes)
if (empty(trim($content))) {
return;
}
if (str_starts_with($content, '#')) {
$command = ltrim($content, '#');
handleTemplateCommand($pdo, $message, $command);
} elseif (str_starts_with($content, '/')) {
handleSlashCommand($pdo, $message, $content);
} else {
handleRegularMessage($pdo, $message, $content);
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . PHP_EOL;
}
});
$discord->on(Event::INTERACTION_CREATE, function ($interaction, Discord $discord) {
try {
handleButtonInteraction($interaction, $discord);
} catch (Exception $e) {
echo "Error en interacción: " . $e->getMessage() . PHP_EOL;
}
});
function handleTemplateCommand(PDO $pdo, Message $message, string $command): void
{
try {
$stmt = $pdo->prepare("SELECT * FROM recurrent_messages WHERE telegram_command = ?");
$stmt->execute([$command]);
$template = $stmt->fetch();
if ($template) {
require_once __DIR__ . '/discord/DiscordSender.php';
$sender = new \Discord\DiscordSender();
// Parsear el contenido manteniendo el orden texto-imagen
$segments = $sender->parseContent($template['message_content']);
if (!empty($segments)) {
// Obtener texto completo para botones de traducción
$plainText = $template['message_content'];
$plainText = preg_replace('/<br\s*\/?>/i', "\n", $plainText);
$plainText = preg_replace('/<\/p>/i', "\n", $plainText);
$plainText = preg_replace('/<p[^>]*>/i', '', $plainText);
$plainText = strip_tags($plainText);
$plainText = html_entity_decode($plainText, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$plainText = trim($plainText);
$translationButtons = getDiscordTranslationButtons($pdo, $plainText);
// Enviar contenido ordenado con botones
$sender->sendContentWithOrderedImagesAndButtons((string)$message->channel_id, $segments, $translationButtons);
}
} else {
$message->channel->sendMessage("❌ Plantilla no encontrada: #{$command}");
}
} catch (Exception $e) {
$message->channel->sendMessage("❌ Error: " . $e->getMessage());
}
}
function getDiscordTranslationButtons(PDO $pdo, string $text): array
{
$stmt = $pdo->query("SELECT language_code, flag_emoji FROM supported_languages WHERE is_active = 1");
$languages = $stmt->fetchAll();
if (count($languages) <= 1) {
return [];
}
// Guardar texto en BD con hash consistente
$textHash = md5($text);
$stmt = $pdo->prepare("INSERT INTO translation_cache (text_hash, original_text) VALUES (?, ?) ON DUPLICATE KEY UPDATE original_text = VALUES(original_text)");
$stmt->execute([$textHash, $text]);
$buttons = [];
foreach ($languages as $lang) {
$buttons[] = [
'label' => $lang['flag_emoji'] . ' ' . strtoupper($lang['language_code']),
'custom_id' => 'translate_' . $lang['language_code'] . ':' . $textHash,
'style' => 1
];
}
return [
[
'type' => 1,
'components' => array_slice($buttons, 0, 5)
]
];
}
function handleSlashCommand(PDO $pdo, Message $message, string $content): void
{
$parts = explode(' ', $content);
$cmd = strtolower(str_replace('/', '', $parts[0]));
$args = array_slice($parts, 1);
echo "Comando recibido: {$cmd}" . PHP_EOL;
switch ($cmd) {
case 'comandos':
require_once __DIR__ . '/discord/DiscordSender.php';
$msg = "📋 **Comandos disponibles:**\n\n";
$msg .= "`/comandos` - Ver comandos\n";
$msg .= "`/setlang [código]` - Establecer idioma\n";
$msg .= "`/bienvenida` - Mensaje de bienvenida\n";
$msg .= "`/agente` - Cambiar a modo IA\n";
$stmt = $pdo->query("SELECT name, telegram_command FROM recurrent_messages WHERE telegram_command IS NOT NULL AND telegram_command != '' ORDER BY name");
$customCommands = $stmt->fetchAll();
if (!empty($customCommands)) {
$msg .= "\n📦 **Plantillas:**\n";
foreach ($customCommands as $cmd) {
$msg .= "`#" . htmlspecialchars($cmd['telegram_command']) . "` - " . htmlspecialchars($cmd['name']) . "\n";
}
}
$translationButtons = getDiscordTranslationButtons($pdo, $msg);
$sender = new \Discord\DiscordSender();
$sender->sendMessage((string)$message->channel_id, $msg, null, $translationButtons);
break;
case 'setlang':
$langCode = $args[0] ?? 'es';
$stmt = $pdo->prepare("UPDATE recipients SET language_code = ? WHERE platform_id = ? AND platform = 'discord'");
$stmt->execute([$langCode, $message->author->id]);
$message->channel->sendMessage("✅ Idioma actualizado a: " . strtoupper($langCode));
break;
case 'bienvenida':
$stmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
$config = $stmt->fetch();
if ($config && $config['is_active']) {
require_once __DIR__ . '/discord/DiscordSender.php';
$text = str_replace('{user_name}', $message->author->username, $config['message_text']);
// Convertir HTML a texto plano para botones de traducción
$plainText = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Limpiar espacios múltiples pero preservar saltos de línea
$plainText = preg_replace('/[ \t]+/', ' ', $plainText);
$plainText = preg_replace('/\n\s*\n/', "\n", $plainText);
$translationButtons = getDiscordTranslationButtons($pdo, $plainText);
$sender = new \Discord\DiscordSender();
$sender->sendMessage((string)$message->channel_id, $text, null, $translationButtons);
}
break;
case 'agente':
$builder = \Discord\Builders\MessageBuilder::new();
$builder->setContent("🤖 **Selecciona un modo de chat:**");
$row = new \Discord\Builders\Components\ActionRow();
$btnBot = \Discord\Builders\Components\Button::new(\Discord\Builders\Components\Button::STYLE_PRIMARY)
->setLabel('Seguir con Bot')
->setCustomId('chat_mode_bot:' . $message->author->id);
$btnIa = \Discord\Builders\Components\Button::new(\Discord\Builders\Components\Button::STYLE_SUCCESS)
->setLabel('Platicar con IA')
->setCustomId('chat_mode_ia:' . $message->author->id);
$row->addComponent($btnBot);
$row->addComponent($btnIa);
$builder->addComponent($row);
$message->channel->sendMessage($builder);
break;
default:
$message->channel->sendMessage("Comando desconocido. Usa /comandos para ver los disponibles.");
}
}
function handleRegularMessage(PDO $pdo, Message $message, string $content): void
{
$stmt = $pdo->prepare("SELECT chat_mode FROM recipients WHERE platform_id = ? AND platform = 'discord'");
$stmt->execute([$message->author->id]);
$recipient = $stmt->fetch();
if ($recipient && $recipient['chat_mode'] === 'ia') {
sendToN8NIA($message, $content);
} else {
handleAutoTranslationWithButtons($pdo, $message, $content);
}
}
function sendToN8NIA(Message $message, string $userMessage): void
{
require_once __DIR__ . '/includes/env_loader.php';
$webhookUrl = trim($_ENV['N8N_IA_WEBHOOK_URL_DISCORD'] ?? getenv('N8N_IA_WEBHOOK_URL_DISCORD') ?? '');
$webhookUrl = trim($webhookUrl, '"');
if (!empty($webhookUrl)) {
$data = [
'user_id' => (string)$message->author->id,
'username' => $message->author->username,
'message' => $userMessage
];
$jsonData = json_encode($data);
$ch = curl_init($webhookUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($jsonData)
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 200 && $httpCode < 300) {
$result = json_decode($response, true);
$reply = $result['reply'] ?? $result['message'] ?? $response;
$message->channel->sendMessage($reply);
return;
}
error_log("N8N Discord IA Error: HTTP $httpCode - Response: $response");
}
require_once __DIR__ . '/src/IA/Agent.php';
$agent = new \IA\Agent();
try {
$response = $agent->generateResponse($userMessage);
$message->channel->sendMessage($response);
} catch (\Exception $e) {
$message->channel->sendMessage("❌ Error: " . $e->getMessage());
error_log("IA Agent Discord Error: " . $e->getMessage());
}
}
function handleAutoTranslationWithButtons(PDO $pdo, Message $message, string $text): void
{
try {
// Si no hay contenido real (solo emojis, espacios), ignorar
if (!hasRealContent($text)) {
return;
}
require_once __DIR__ . '/src/Translate.php';
$translator = new src\Translate();
// Detectar idioma del mensaje (sin emojis para mejor precisión)
$textForDetection = stripEmojisForDetection($text);
$detectedLang = $translator->detectLanguage($textForDetection) ?? 'es';
// Obtener idiomas activos de la base de datos
$stmt = $pdo->query("SELECT language_code, flag_emoji FROM supported_languages WHERE is_active = 1");
$activeLanguages = $stmt->fetchAll();
if (count($activeLanguages) <= 1) {
return; // No hay suficientes idiomas para traducir
}
// Guardar texto en la base de datos con hash consistente
$textHash = md5($text);
$stmt = $pdo->prepare("INSERT INTO translation_cache (text_hash, original_text) VALUES (?, ?) ON DUPLICATE KEY UPDATE original_text = VALUES(original_text)");
$stmt->execute([$textHash, $text]);
// Preparar botones
$buttons = [];
foreach ($activeLanguages as $lang) {
if ($lang['language_code'] !== $detectedLang) {
$buttons[] = [
'label' => $lang['flag_emoji'] . ' ' . strtoupper($lang['language_code']),
'custom_id' => 'translate_' . $lang['language_code'] . ':' . $textHash,
'style' => 1
];
}
}
// Enviar mensaje con botones usando MessageBuilder correctamente
if (!empty($buttons)) {
$messageText = "🌐 **Traducciones disponibles:**\nHaz clic en una bandera para ver la traducción (solo tú la verás)";
// Crear MessageBuilder
$builder = \Discord\Builders\MessageBuilder::new();
$builder->setContent($messageText);
// Crear ActionRow con botones
$row = new \Discord\Builders\Components\ActionRow();
foreach ($buttons as $btn) {
$button = \Discord\Builders\Components\Button::new(\Discord\Builders\Components\Button::STYLE_PRIMARY)
->setLabel($btn['label'])
->setCustomId($btn['custom_id']);
$row->addComponent($button);
}
$builder->addComponent($row);
$message->channel->sendMessage($builder);
}
} catch (Exception $e) {
error_log("Discord translation buttons error: " . $e->getMessage());
}
}
function handleButtonInteraction($interaction, Discord $discord): void
{
$data = $interaction->data;
$customId = $data->custom_id ?? '';
if (str_starts_with($customId, 'translate_')) {
handleTranslateInteraction($interaction, $customId);
} elseif (str_starts_with($customId, 'chat_mode_')) {
try {
require_once __DIR__ . '/includes/db.php';
$pdo = getDbConnection();
$parts = explode(':', $customId, 2);
$mode = str_replace('chat_mode_', '', $parts[0]);
$userId = $parts[1] ?? '';
if (empty($userId)) {
$builder = \Discord\Builders\MessageBuilder::new()
->setContent('❌ Error: Usuario no identificado');
$interaction->respondWithMessage($builder, true);
return;
}
$chatMode = ($mode === 'ia') ? 'ia' : 'bot';
$stmt = $pdo->prepare("UPDATE recipients SET chat_mode = ? WHERE platform_id = ? AND platform = 'discord'");
$stmt->execute([$chatMode, $userId]);
if ($chatMode === 'ia') {
$builder = \Discord\Builders\MessageBuilder::new()
->setContent("✅ **Modo IA activado.** Ahora puedes platicar conmigo. Escribe cualquier cosa y la enviaré a la IA.");
} else {
$builder = \Discord\Builders\MessageBuilder::new()
->setContent("✅ **Modo Bot activado.** Ahora puedes usar comandos como #comando y traducción.");
}
$interaction->respondWithMessage($builder, true);
} catch (Exception $e) {
error_log("Discord chat mode error: " . $e->getMessage());
$builder = \Discord\Builders\MessageBuilder::new()
->setContent('❌ Error al cambiar el modo de chat');
$interaction->respondWithMessage($builder, true);
}
}
}
function isExistingDiscordUser(PDO $pdo, int $userId): bool
{
$stmt = $pdo->prepare("SELECT id FROM recipients WHERE platform_id = ? AND platform = 'discord'");
$stmt->execute([$userId]);
return $stmt->fetch() !== false;
}
function sendDiscordWelcomeMessage(PDO $pdo, Member $member, Discord $discord): void
{
$stmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
$config = $stmt->fetch();
$username = $member->user->username;
if ($config && $config['is_active']) {
$text = str_replace('{user_name}', $username, $config['message_text']);
try {
$member->sendMessage($text);
} catch (Exception $e) {
echo "No se pudo enviar mensaje privado: " . $e->getMessage() . PHP_EOL;
}
} else {
try {
$member->sendMessage("¡Hola {$username}! 👋\n\nUsa /comandos para ver los comandos disponibles.");
} catch (Exception $e) {
echo "No se pudo enviar mensaje privado: " . $e->getMessage() . PHP_EOL;
}
}
}
function registerDiscordUser(PDO $pdo, $user): void
{
$stmt = $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)
");
$name = $user->username ?? 'Usuario';
$stmt->execute([$user->id, $name]);
}
function sendDiscordWelcomeMessageOnMessage(PDO $pdo, Message $message, string $username): void
{
$stmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
$config = $stmt->fetch();
if ($config && $config['is_active']) {
$text = str_replace('{user_name}', $username, $config['message_text']);
$message->author->sendMessage($text);
} else {
$message->author->sendMessage("¡Hola {$username}! 👋\n\nUsa /comandos para ver los comandos disponibles.");
}
}
function handleTranslateInteraction($interaction, string $customId): void
{
try {
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/src/Translate.php';
$pdo = getDbConnection();
$translator = new src\Translate();
$parts = explode(':', $customId, 2);
$targetLang = str_replace('translate_', '', $parts[0]);
$textHash = $parts[1] ?? '';
// Obtener texto de la base de datos primero
$stmt = $pdo->prepare("SELECT original_text FROM translation_cache WHERE text_hash = ?");
$stmt->execute([$textHash]);
$row = $stmt->fetch();
if (!$row || empty($row['original_text'])) {
// Enviar error efímero
$builder = \Discord\Builders\MessageBuilder::new()
->setContent('❌ Error: Texto no encontrado');
$interaction->respondWithMessage($builder, true);
return;
}
$originalText = $row['original_text'];
// Responder inmediatamente con "Traduciendo..."
$loadingBuilder = \Discord\Builders\MessageBuilder::new()
->setContent("⏳ Traduciendo a " . strtoupper($targetLang) . "...");
$interaction->respondWithMessage($loadingBuilder, true);
// Ahora traducir (esto puede tardar)
// Usar texto sin emojis para detectar idioma y traducir con mayor precisión
$textForDetection = stripEmojisForDetection($originalText);
$sourceLang = $translator->detectLanguage($textForDetection) ?? 'es';
$translated = $translator->translate($textForDetection, $sourceLang, $targetLang);
if ($translated) {
// Limpiar todo el HTML
$translated = strip_tags($translated);
// Limpiar asteriscos duplicados
$translated = preg_replace('/\*+/', '', $translated);
// Limpiar comandos con espacios
$translated = preg_replace('/\/(\s+)(\w+)/', '/$2', $translated);
$translated = preg_replace('/#(\s+)(\w+)/', '#$2', $translated);
// Limpiar saltos de línea extras
$translated = preg_replace('/\n\s*\n/', "\n", $translated);
// Actualizar la respuesta con la traducción real
// Preservar emojis del original si los hay
$displayText = trim($translated);
if (strpos($originalText, '👍') !== false || preg_match('/[\x{1F300}-\x{1F9FF}]/u', $originalText)) {
// Si el original tenía emojis, intentar identificarlos
// Para mensajes simples, los emojis generalmente están al inicio o final
$emojiPrefix = '';
$emojiSuffix = '';
if (preg_match('/^([\x{1F300}-\x{1F9FF}]+)\s*/u', $originalText, $match)) {
$emojiPrefix = $match[1] . ' ';
}
if (preg_match('/\s*([\x{1F300}-\x{1F9FF}]+)$/u', $originalText, $match)) {
$emojiSuffix = ' ' . $match[1];
}
$displayText = $emojiPrefix . $displayText . $emojiSuffix;
}
$finalBuilder = \Discord\Builders\MessageBuilder::new()
->setContent("🌐 **Traducción (" . strtoupper($targetLang) . "):**\n\n" . $displayText);
$interaction->updateOriginalResponse($finalBuilder);
} else {
// Actualizar con error
$errorBuilder = \Discord\Builders\MessageBuilder::new()
->setContent("❌ Error al traducir");
$interaction->updateOriginalResponse($errorBuilder);
}
} catch (Exception $e) {
error_log("Discord translate error: " . $e->getMessage());
try {
$builder = \Discord\Builders\MessageBuilder::new()
->setContent('❌ Error en el proceso de traducción');
$interaction->respondWithMessage($builder, true);
} catch (Exception $inner) {
error_log("Error responding to interaction: " . $inner->getMessage());
}
}
}
$discord->run();