Bot Discord - Commit completo con todos los cambios
This commit is contained in:
266
discord/DiscordSender.php
Executable file
266
discord/DiscordSender.php
Executable file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
class DiscordSender
|
||||
{
|
||||
private const API_BASE_URL = 'https://discord.com/api/v10';
|
||||
private const MESSAGE_CHAR_LIMIT = 1990;
|
||||
private const LOG_FILE = __DIR__ . '/../logs/discord_api.log';
|
||||
|
||||
private string $token;
|
||||
|
||||
public function __construct(string $token)
|
||||
{
|
||||
custom_log('[DiscordSender] Initializing...');
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function sendMessage(string $discordId, string $content, string $recipientType = 'channel') {
|
||||
custom_log("[DiscordSender] sendMessage: Called for ID {$discordId} and recipient type {$recipientType}.");
|
||||
|
||||
try {
|
||||
if (empty(trim($content))) {
|
||||
$this->logMessage("Error: No se puede enviar un mensaje vacío");
|
||||
return false;
|
||||
}
|
||||
|
||||
$targetChannelId = $this->getTargetChannelId($discordId, $recipientType);
|
||||
custom_log("[DiscordSender] sendMessage: Target channel ID is {$targetChannelId}.");
|
||||
|
||||
$parts = [];
|
||||
|
||||
preg_match_all('/<img[^>]+src=[\'"]([^\'"]+)[\'"][^>]*>/i', $content, $imageMatches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
||||
|
||||
$lastPos = 0;
|
||||
|
||||
foreach ($imageMatches as $match) {
|
||||
$imageTag = $match[0][0];
|
||||
$imageUrl = $match[1][0];
|
||||
$imagePos = $match[0][1];
|
||||
|
||||
$textBefore = trim(substr($content, $lastPos, $imagePos - $lastPos));
|
||||
if (!empty($textBefore)) {
|
||||
$textWithNewlines = str_ireplace(['<p>', '</p>', '<br>', '<br />'], ["", "\n", "\n", "\n"], $textBefore);
|
||||
$text = trim(strip_tags($textWithNewlines));
|
||||
if (!empty($text)) {
|
||||
$parts[] = ['type' => 'text', 'content' => $text];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($imageUrl)) {
|
||||
$absoluteImageUrl = $imageUrl;
|
||||
if (strpos($imageUrl, 'http') !== 0 && strpos($imageUrl, '//') !== 0) {
|
||||
$base = rtrim(BOT_BASE_URL, '/');
|
||||
$path = ltrim($imageUrl, '/');
|
||||
$absoluteImageUrl = "{$base}/{$path}";
|
||||
}
|
||||
$parts[] = ['type' => 'image', 'url' => $absoluteImageUrl];
|
||||
}
|
||||
|
||||
$lastPos = $imagePos + strlen($imageTag);
|
||||
}
|
||||
|
||||
$textAfter = trim(substr($content, $lastPos));
|
||||
if (!empty($textAfter)) {
|
||||
$textWithNewlines = str_ireplace(['<p>', '</p>', '<br>', '<br />'], ["", "\n", "\n", "\n"], $textAfter);
|
||||
$text = trim(strip_tags($textWithNewlines));
|
||||
if (!empty($text)) {
|
||||
$parts[] = ['type' => 'text', 'content' => $text];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($parts)) {
|
||||
$textWithNewlines = str_ireplace(['<p>', '</p>', '<br>', '<br />'], ["", "\n", "\n", "\n"], $content);
|
||||
$text = trim(strip_tags($textWithNewlines));
|
||||
if (!empty($text)) {
|
||||
$parts[] = ['type' => 'text', 'content' => $text];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($parts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$messageIds = [];
|
||||
$allPartsSentSuccessfully = true;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if ($part['type'] === 'text') {
|
||||
$chunks = $this->splitMessage($part['content']);
|
||||
foreach ($chunks as $chunk) {
|
||||
$trimmedChunk = trim($chunk);
|
||||
if ($trimmedChunk === '') continue;
|
||||
|
||||
try {
|
||||
$response = $this->sendDiscordMessage($targetChannelId, ['content' => $trimmedChunk]);
|
||||
if (isset($response['id'])) {
|
||||
$messageIds[] = $response['id'];
|
||||
} else {
|
||||
$allPartsSentSuccessfully = false; break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error al enviar texto: " . $e->getMessage());
|
||||
$allPartsSentSuccessfully = false; break;
|
||||
}
|
||||
usleep(250000);
|
||||
}
|
||||
} elseif ($part['type'] === 'image') {
|
||||
try {
|
||||
$response = $this->sendDiscordMessage($targetChannelId, ['content' => $part['url']]);
|
||||
if (isset($response['id'])) {
|
||||
$messageIds[] = $response['id'];
|
||||
} else {
|
||||
$allPartsSentSuccessfully = false; break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error al enviar imagen como URL: " . $e->getMessage());
|
||||
$allPartsSentSuccessfully = false; break;
|
||||
}
|
||||
}
|
||||
if (!$allPartsSentSuccessfully) break;
|
||||
usleep(500000);
|
||||
}
|
||||
|
||||
return $allPartsSentSuccessfully ? $messageIds : false;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error in sendMessage: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function getTargetChannelId(string $discordId, string $recipientType): string {
|
||||
if ($recipientType === 'user') {
|
||||
return $this->createDMChannel($discordId);
|
||||
}
|
||||
return $discordId;
|
||||
}
|
||||
|
||||
private function createDMChannel(string $userId): string {
|
||||
$url = self::API_BASE_URL . '/users/@me/channels';
|
||||
$data = json_encode(['recipient_id' => $userId]);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bot ' . $this->token,
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($data)
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $data
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
throw new Exception('cURL error: ' . curl_error($ch));
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("Failed to create DM channel. HTTP code: $httpCode, Response: $response");
|
||||
}
|
||||
|
||||
$responseData = json_decode($response, true);
|
||||
return $responseData['id'];
|
||||
}
|
||||
|
||||
private function sendDiscordMessage(string $channelId, array $payload, array $files = []) {
|
||||
$url = self::API_BASE_URL . "/channels/{$channelId}/messages";
|
||||
|
||||
if (isset($payload['content'])) {
|
||||
$payload['content'] = trim($payload['content']);
|
||||
if ($payload['content'] === '') unset($payload['content']);
|
||||
}
|
||||
|
||||
if (empty($payload['content'] ?? '') && empty($payload['embeds'] ?? '') && empty($files)) {
|
||||
throw new Exception("No se puede enviar un mensaje vacío");
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
$headers = ['Authorization: Bot ' . $this->token, 'User-Agent: DiscordBot (v1.0)'] ;
|
||||
|
||||
if (empty($files)) {
|
||||
$headers[] = 'Content-Type: application/json';
|
||||
$postData = json_encode($payload);
|
||||
} else {
|
||||
// Multipart logic for files would go here if needed
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$responseData = json_decode($response, true);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
throw new Exception("Discord API error ({$httpCode}): " . ($responseData['message'] ?? 'Unknown error'));
|
||||
}
|
||||
|
||||
return $responseData;
|
||||
}
|
||||
|
||||
private function splitMessage(string $text, int $maxLength = self::MESSAGE_CHAR_LIMIT): array
|
||||
{
|
||||
$chunks = [];
|
||||
$text = str_replace(["\r\n", "\r"], "\n", $text);
|
||||
$lines = explode("\n", $text);
|
||||
$currentChunk = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (mb_strlen($currentChunk, 'UTF-8') + mb_strlen($line, 'UTF-8') + 1 > $maxLength) {
|
||||
$chunks[] = $currentChunk;
|
||||
$currentChunk = $line;
|
||||
} else {
|
||||
$currentChunk .= (empty($currentChunk) ? '' : "\n") . $line;
|
||||
}
|
||||
}
|
||||
if (!empty($currentChunk)) $chunks[] = $currentChunk;
|
||||
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
private function logMessage(string $message): void {
|
||||
$logMessage = date('[Y-m-d H:i:s] ') . $message . "\n";
|
||||
file_put_contents(self::LOG_FILE, $logMessage, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
public function sendRawMessage(string $channelId, string $content): ?array
|
||||
{
|
||||
custom_log("[DiscordSender] sendRawMessage: Called for channel ID {$channelId}.");
|
||||
try {
|
||||
return $this->sendDiscordMessage($channelId, ['content' => $content]);
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error in sendRawMessage: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía un Embed (construido como un array) a un canal de Discord.
|
||||
*
|
||||
* @param string $channelId El ID del canal de destino.
|
||||
* @param array $embedData El array que representa el embed.
|
||||
* @return array|null La respuesta de la API de Discord o null si hay un error.
|
||||
*/
|
||||
public function sendEmbedData(string $channelId, array $embedData): ?array
|
||||
{
|
||||
custom_log("[DiscordSender] sendEmbedData: Called for channel ID {$channelId}.");
|
||||
try {
|
||||
return $this->sendDiscordMessage($channelId, ['embeds' => [$embedData]]);
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error in sendEmbedData: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
discord/actions/discord_actions.php
Executable file
72
discord/actions/discord_actions.php
Executable file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/session_check.php';
|
||||
require_once __DIR__ . '/../../includes/db.php';
|
||||
require_once __DIR__ . '/../DiscordSender.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['action'])) {
|
||||
header('Location: ../sent_messages.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'delete_message') {
|
||||
if (!isset($_POST['sent_message_id'], $_POST['platform_message_id'], $_POST['channel_id'])) {
|
||||
header('Location: ../sent_messages.php?error=missing_data');
|
||||
exit();
|
||||
}
|
||||
|
||||
$sentMessageId = $_POST['sent_message_id'];
|
||||
$discordMessageIdsJson = $_POST['platform_message_id'];
|
||||
$channelId = $_POST['channel_id']; // The channel where the message was sent
|
||||
|
||||
$discordMessageIds = json_decode($discordMessageIdsJson, true);
|
||||
|
||||
// If decoding fails or the result is not an array, treat it as a single ID
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($discordMessageIds)) {
|
||||
$discordMessageIds = [$discordMessageIdsJson];
|
||||
}
|
||||
|
||||
$discordSender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
|
||||
try {
|
||||
// 1. Attempt to delete from Discord
|
||||
$all_deleted = true;
|
||||
foreach ($discordMessageIds as $discordMessageId) {
|
||||
// Skip if the ID is empty or invalid
|
||||
if (empty($discordMessageId) || !is_numeric($discordMessageId)) {
|
||||
error_log("Skipping invalid message chunk ID: " . var_export($discordMessageId, true));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
error_log("Attempting to delete message chunk ID: {$discordMessageId} in channel {$channelId}");
|
||||
$discordSender->deleteMessage($channelId, $discordMessageId);
|
||||
usleep(500000); // Wait 500ms to avoid rate limiting
|
||||
} catch (Exception $e) {
|
||||
error_log("Failed to delete message chunk {$discordMessageId}: " . $e->getMessage());
|
||||
$all_deleted = false; // Mark that at least one failed
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If all chunks were deleted (or if there was only one), delete from our database
|
||||
if ($all_deleted) {
|
||||
$stmt = $pdo->prepare("DELETE FROM sent_messages WHERE id = ?");
|
||||
$stmt->execute([$sentMessageId]);
|
||||
header('Location: ../sent_messages.php?success=deleted&platform=Discord');
|
||||
} else {
|
||||
// If some failed, we don't delete the entry, so it can be retried.
|
||||
// We could also add more sophisticated logic here, like storing partial success.
|
||||
header('Location: ../sent_messages.php?error=delete_failed_partial&platform=Discord');
|
||||
}
|
||||
exit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Discord message deletion failed: " . $e->getMessage());
|
||||
header('Location: ../sent_messages.php?error=delete_failed&platform=Discord&message=' . urlencode($e->getMessage()));
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback redirect
|
||||
header('Location: ../sent_messages.php');
|
||||
?>
|
||||
209
discord/admin/discord_translation_options.php
Executable file
209
discord/admin/discord_translation_options.php
Executable file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
// Evitar caché del navegador
|
||||
header("Cache-Control: no-cache, must-revalidate");
|
||||
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Opciones de Traducción - Discord Bot</title>
|
||||
<style>
|
||||
:root {
|
||||
--discord-blurple: #5865F2;
|
||||
--discord-green: #57F287;
|
||||
--discord-yellow: #FEE75C;
|
||||
--discord-red: #ED4245;
|
||||
--discord-dark: #2c2f33;
|
||||
--discord-gray: #99aab5;
|
||||
--discord-light-gray: #f2f3f5;
|
||||
--discord-chat-bg: #36393f;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'gg sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background: var(--discord-dark);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
}
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { text-align: center; color: white; margin-bottom: 40px; }
|
||||
.header h1 { font-size: 2.5rem; margin-bottom: 10px; }
|
||||
.header p { font-size: 1.2rem; color: var(--discord-gray); }
|
||||
.options-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 25px; margin-bottom: 30px; }
|
||||
.option-card { background: var(--discord-chat-bg); border-radius: 8px; padding: 25px; box-shadow: 0 8px 20px rgba(0,0,0,0.3); transition: transform 0.3s ease; }
|
||||
.option-card:hover { transform: translateY(-5px); }
|
||||
.option-header { display: flex; align-items: center; gap: 15px; margin-bottom: 15px; }
|
||||
.option-icon { font-size: 28px; }
|
||||
.option-title { font-size: 1.5rem; font-weight: bold; color: white; }
|
||||
.option-description { color: var(--discord-gray); font-size: 1rem; margin-bottom: 20px; }
|
||||
.demo-area { background: var(--discord-dark); border-radius: 8px; padding: 20px; border: 1px solid #40444b; }
|
||||
.discord-message { display: flex; gap: 15px; }
|
||||
.avatar { width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0; }
|
||||
.message-content { display: flex; flex-direction: column; }
|
||||
.username { font-weight: 600; font-size: 1rem; margin-bottom: 5px; color: var(--discord-green); }
|
||||
.message-text { color: #dcddde; line-height: 1.4; }
|
||||
.discord-button { padding: 10px 16px; border: none; border-radius: 3px; font-weight: 600; cursor: pointer; font-size: 0.9rem; transition: background-color 0.2s; margin-top: 10px; background-color: #4f545c; color: white; }
|
||||
.discord-button:hover { background-color: #5d636b; }
|
||||
.pros-cons { margin-top: 20px; }
|
||||
.pros, .cons { padding: 12px; border-radius: 8px; margin-bottom: 10px; font-size: 0.9rem; }
|
||||
.pros { background: rgba(87, 242, 135, 0.1); border-left: 3px solid var(--discord-green); }
|
||||
.pros strong { color: var(--discord-green); }
|
||||
.cons { background: rgba(254, 231, 92, 0.1); border-left: 3px solid var(--discord-yellow); }
|
||||
.cons strong { color: var(--discord-yellow); }
|
||||
ul { color: var(--discord-gray); margin-left: 20px; margin-top: 5px; }
|
||||
.recommendation { background: var(--discord-blurple); color: white; padding: 30px; border-radius: 8px; text-align: center; }
|
||||
|
||||
/* Modal for Ephemeral Message */
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; align-items: center; justify-content: center; }
|
||||
.modal.active { display: flex; }
|
||||
.modal-content { background: var(--discord-chat-bg); padding: 20px; border-radius: 8px; max-width: 450px; width: 90%; box-shadow: 0 5px 15px rgba(0,0,0,0.5); }
|
||||
.modal-header { color: var(--discord-green); font-weight: bold; margin-bottom: 15px; }
|
||||
.modal-body { color: #dcddde; }
|
||||
.modal-body .translation { margin-bottom: 10px; }
|
||||
.modal-body .translation strong { color: white; }
|
||||
.modal-footer { font-size: 0.8rem; color: var(--discord-gray); margin-top: 15px; text-align: center; }
|
||||
|
||||
/* Spoiler styles */
|
||||
.spoiler { background-color: #202225; color: transparent; border-radius: 3px; cursor: pointer; padding: 0 2px; }
|
||||
.spoiler.revealed { background-color: transparent; color: inherit; }
|
||||
|
||||
/* Thread styles */
|
||||
.thread-link { color: var(--discord-blurple); font-weight: 600; cursor: pointer; font-size: 0.9rem; margin-top: 8px; }
|
||||
.thread-view { display: none; margin-top: 15px; padding-top: 15px; border-top: 1px solid #40444b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><img src="https://discord.com/assets/f8389ca1a741a115313a74fbbd352562.svg" alt="Discord Logo" width="40" style="vertical-align: middle;"> Opciones de Traducción para Discord</h1>
|
||||
<p>Demostración visual de métodos no invasivos para mostrar traducciones.</p>
|
||||
</div>
|
||||
|
||||
<div class="options-grid">
|
||||
<!-- Opción 1: Botón con Respuesta Efímera -->
|
||||
<div class="option-card">
|
||||
<div class="option-header"><span class="option-icon">🤫</span> <h2 class="option-title">Botón Efímero</h2></div>
|
||||
<p class="option-description">Un botón "Traducir" bajo el mensaje original. Al pulsarlo, se muestra una traducción que sólo el usuario que ha hecho clic puede ver.</p>
|
||||
<div class="demo-area">
|
||||
<div class="discord-message">
|
||||
<img src="https://cdn.discordapp.com/embed/avatars/1.png" alt="avatar" class="avatar">
|
||||
<div class="message-content">
|
||||
<span class="username">UserFromFrance</span>
|
||||
<div class="message-text">Bonjour tout le monde! C'est une démo.</div>
|
||||
<button class="discord-button" onclick="showPopup('ephemeral-popup')">🌐 Traducir</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><strong>✅ Ventajas:</strong><ul><li>No satura el chat general.</li><li>La traducción es bajo demanda.</li><li>Experiencia de usuario moderna y limpia.</li></ul></div>
|
||||
<div class="cons"><strong>⚠️ Consideración:</strong><p style="margin-left: 20px; color: var(--discord-gray);">Requiere manejar interacciones (clicks de botón) en el backend.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opción 2: Hilos -->
|
||||
<div class="option-card">
|
||||
<div class="option-header"><span class="option-icon">🧵</span> <h2 class="option-title">Hilos (Threads)</h2></div>
|
||||
<p class="option-description">Se crea un hilo a partir del mensaje original, y la traducción se publica dentro de ese hilo, manteniendo el canal principal limpio.</p>
|
||||
<div class="demo-area">
|
||||
<div class="discord-message">
|
||||
<img src="https://cdn.discordapp.com/embed/avatars/2.png" alt="avatar" class="avatar">
|
||||
<div class="message-content">
|
||||
<span class="username" style="color: #f1c40f;">UserFromGermany</span>
|
||||
<div class="message-text">Guten Tag, dies ist eine weitere Demo.</div>
|
||||
<div class="thread-link" onclick="toggleThread('thread-view')">↪️ Ver Hilo (1 respuesta)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="thread-view" id="thread-view">
|
||||
<div class="discord-message">
|
||||
<img src="https://www.google.com/s2/favicons?domain=discord.com" alt="avatar" class="avatar">
|
||||
<div class="message-content">
|
||||
<span class="username" style="color: #5865F2;">Translation Bot</span>
|
||||
<div class="message-text"><strong>Traducción (ES):</strong><br>Buenas tardes, esta es otra demo.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><strong>✅ Ventajas:</strong><ul><li>Mantiene el canal principal 100% limpio.</li><li>Permite discusiones sobre la traducción en el hilo.</li><li>Ideal para mensajes largos.</li></ul></div>
|
||||
<div class="cons"><strong>⚠️ Consideración:</strong><p style="margin-left: 20px; color: var(--discord-gray);">Puede generar muchas notificaciones de hilos si hay mucha actividad.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opción 3: Spoilers -->
|
||||
<div class="option-card">
|
||||
<div class="option-header"><span class="option-icon">⬛</span> <h2 class="option-title">Spoilers</h2></div>
|
||||
<p class="option-description">La traducción se añade al mensaje original o en una respuesta, pero oculta tras una etiqueta de spoiler que se revela al hacer clic.</p>
|
||||
<div class="demo-area">
|
||||
<div class="discord-message">
|
||||
<img src="https://cdn.discordapp.com/embed/avatars/3.png" alt="avatar" class="avatar">
|
||||
<div class="message-content">
|
||||
<span class="username" style="color: #e91e63;">UserFromItaly</span>
|
||||
<div class="message-text">
|
||||
Ciao! Benvenuto alla demo finale.<br>
|
||||
<strong>Traducción:</strong> <span class="spoiler" onclick="this.classList.toggle('revealed')">¡Hola! Bienvenido a la demo final.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><strong>✅ Ventajas:</strong><ul><li>Muy fácil de implementar.</li><li>No requiere interacciones complejas.</li><li>La traducción está en el mismo contexto.</li></ul></div>
|
||||
<div class="cons"><strong>⚠️ Consideración:</strong><p style="margin-left: 20px; color: var(--discord-gray);">Añade "ruido" visual al chat, aunque esté oculto.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recommendation">
|
||||
<h3>💡 Nuestra Recomendación</h3>
|
||||
<p>Para la mejor experiencia de usuario y mantener los canales limpios, la **Opción 1 (Botón Efímero)** es la más recomendada. Es moderna, eficiente y le da el control total al usuario sin interrumpir a los demás.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para la demo efímera -->
|
||||
<div class="modal" id="ephemeral-popup" onclick="closePopup('ephemeral-popup')">
|
||||
<div class="modal-content" onclick="event.stopPropagation()">
|
||||
<div class="modal-header">Traducción del mensaje</div>
|
||||
<div class="modal-body">
|
||||
<div class="translation">
|
||||
<strong>Inglés:</strong><br>
|
||||
<span>Hello everyone! This is a demo.</span>
|
||||
</div>
|
||||
<div class="translation">
|
||||
<strong>Español:</strong><br>
|
||||
<span>¡Hola a todos! Esto es una demo.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">Solo tú puedes ver este mensaje.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showPopup(id) {
|
||||
document.getElementById(id).classList.add('active');
|
||||
}
|
||||
function closePopup(id) {
|
||||
document.getElementById(id).classList.remove('active');
|
||||
}
|
||||
function toggleThread(id) {
|
||||
const threadView = document.getElementById(id);
|
||||
if (threadView.style.display === 'block') {
|
||||
threadView.style.display = 'none';
|
||||
} else {
|
||||
threadView.style.display = 'block';
|
||||
}
|
||||
}
|
||||
// Cerrar modal con la tecla Escape
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === "Escape") {
|
||||
const activeModal = document.querySelector('.modal.active');
|
||||
if (activeModal) {
|
||||
closePopup(activeModal.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
765
discord/bot/discord_bot.php.backup
Executable file
765
discord/bot/discord_bot.php.backup
Executable 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()]);
|
||||
}
|
||||
}
|
||||
|
||||
209
discord/converters/HtmlToDiscordMarkdownConverter.php
Executable file
209
discord/converters/HtmlToDiscordMarkdownConverter.php
Executable file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
// Cargar configuración
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
|
||||
class HtmlToDiscordMarkdownConverter
|
||||
{
|
||||
private const DISCORD_MESSAGE_LIMIT = 2000;
|
||||
|
||||
public function convert(string $html): string
|
||||
{
|
||||
$dom = new DOMDocument();
|
||||
libxml_use_internal_errors(true); // Suppress warnings for malformed HTML
|
||||
// Use LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD to prevent adding html/body tags
|
||||
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
$markdown = '';
|
||||
|
||||
foreach ($dom->childNodes as $node) {
|
||||
$markdown .= $this->processNode($node);
|
||||
}
|
||||
|
||||
// Clean up extra newlines
|
||||
$markdown = preg_replace('/\n{3,}/', "\n\n", $markdown);
|
||||
$markdown = trim($markdown);
|
||||
|
||||
return $markdown;
|
||||
}
|
||||
|
||||
private function processNode(DOMNode $node): string
|
||||
{
|
||||
$output = '';
|
||||
|
||||
switch ($node->nodeType) {
|
||||
case XML_TEXT_NODE:
|
||||
$output .= $this->decodeHtmlEntities($node->nodeValue);
|
||||
break;
|
||||
case XML_ELEMENT_NODE:
|
||||
switch (strtolower($node->nodeName)) {
|
||||
case 'b':
|
||||
case 'strong':
|
||||
$output .= '**' . $this->processChildren($node) . '**';
|
||||
break;
|
||||
case 'i':
|
||||
case 'em':
|
||||
$output .= '*' . $this->processChildren($node) . '*';
|
||||
break;
|
||||
case 'u':
|
||||
$output .= '__' . $this->processChildren($node) . '__';
|
||||
break;
|
||||
case 'a':
|
||||
$href = $node->getAttribute('href');
|
||||
|
||||
// Ignorar nodos de texto con solo espacios en blanco para un análisis más preciso.
|
||||
$realChildNodes = [];
|
||||
foreach ($node->childNodes as $child) {
|
||||
if ($child->nodeType === XML_TEXT_NODE && trim($child->nodeValue) === '') {
|
||||
continue;
|
||||
}
|
||||
$realChildNodes[] = $child;
|
||||
}
|
||||
|
||||
// Si el único hijo real es una imagen, procesarla directamente.
|
||||
if (count($realChildNodes) === 1 && strtolower($realChildNodes[0]->nodeName) === 'img') {
|
||||
$output .= $this->processChildren($node);
|
||||
} else {
|
||||
// Si no, trátalo como un enlace de texto normal.
|
||||
$text = $this->processChildren($node);
|
||||
$output .= "[{$text}]({$href})";
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
$output .= $this->processChildren($node) . "\n\n";
|
||||
break;
|
||||
case 'br':
|
||||
$output .= "\n";
|
||||
break;
|
||||
case 'ul':
|
||||
case 'ol':
|
||||
$listContent = $this->processChildren($node);
|
||||
$listItems = explode("\n", trim($listContent));
|
||||
$formattedList = [];
|
||||
$counter = 1;
|
||||
foreach($listItems as $item) {
|
||||
if(empty(trim($item))) continue;
|
||||
if (strtolower($node->nodeName) === 'ul') {
|
||||
$formattedList[] = '- ' . trim($item);
|
||||
} else {
|
||||
$formattedList[] = ($counter++) . '. ' . trim($item);
|
||||
}
|
||||
}
|
||||
$output .= implode("\n", $formattedList) . "\n\n";
|
||||
break;
|
||||
case 'li':
|
||||
$output .= $this->processChildren($node);
|
||||
break;
|
||||
case 'img':
|
||||
$src = $node->getAttribute('src');
|
||||
if (!empty($src)) {
|
||||
$absoluteImageUrl = $src;
|
||||
// Convertir URL relativa a absoluta si es necesario
|
||||
if (strpos($src, 'http') !== 0 && strpos($src, '//') !== 0) {
|
||||
$base = rtrim(BOT_BASE_URL, '/');
|
||||
$path = ltrim($src, '/');
|
||||
$absoluteImageUrl = "{$base}/{$path}";
|
||||
}
|
||||
// Dejar solo la URL para que Discord la renderice
|
||||
$output .= "\n" . $absoluteImageUrl . "\n";
|
||||
}
|
||||
break;
|
||||
case 'div':
|
||||
$output .= $this->processChildren($node);
|
||||
break;
|
||||
default:
|
||||
// For unknown tags, just process their children
|
||||
$output .= $this->processChildren($node);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function processChildren(DOMNode $node): string
|
||||
{
|
||||
$childrenOutput = '';
|
||||
foreach ($node->childNodes as $child) {
|
||||
$childrenOutput .= $this->processNode($child);
|
||||
}
|
||||
return $childrenOutput;
|
||||
}
|
||||
|
||||
private function decodeHtmlEntities(string $encodedString): string
|
||||
{
|
||||
return html_entity_decode($encodedString, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
public function convertToArray(string $html): array
|
||||
{
|
||||
$parts = [];
|
||||
$dom = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
foreach ($dom->childNodes as $node) {
|
||||
$this->processNodeForArray($node, $parts);
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
private function processNodeForArray(DOMNode $node, array &$parts)
|
||||
{
|
||||
if ($node->nodeType === XML_TEXT_NODE) {
|
||||
$this->addTextPart($parts, $this->decodeHtmlEntities($node->nodeValue));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node->nodeType !== XML_ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (strtolower($node->nodeName)) {
|
||||
case 'img':
|
||||
$src = $node->getAttribute('src');
|
||||
if (!empty($src)) {
|
||||
$absoluteImageUrl = $src;
|
||||
if (strpos($src, 'http') !== 0 && strpos($src, '//') !== 0) {
|
||||
$base = rtrim(BOT_BASE_URL, '/');
|
||||
$path = ltrim($src, '/');
|
||||
$absoluteImageUrl = "{$base}/{$path}";
|
||||
}
|
||||
$parts[] = ['type' => 'image', 'url' => $absoluteImageUrl];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
case 'div':
|
||||
if ($node->hasChildNodes()) {
|
||||
foreach ($node->childNodes as $child) {
|
||||
$this->processNodeForArray($child, $parts);
|
||||
}
|
||||
}
|
||||
$this->addTextPart($parts, "\n\n");
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($node->hasChildNodes()) {
|
||||
foreach ($node->childNodes as $child) {
|
||||
$this->processNodeForArray($child, $parts);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function addTextPart(array &$parts, string $text)
|
||||
{
|
||||
if (empty($text)) return;
|
||||
|
||||
// Si la última parte fue texto, la unimos a ella.
|
||||
if (!empty($parts) && end($parts)['type'] === 'text') {
|
||||
$parts[key($parts)]['content'] .= $text;
|
||||
} else {
|
||||
$parts[] = ['type' => 'text', 'content' => $text];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user