token = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN'); $this->guildId = $_ENV['DISCORD_GUILD_ID'] ?? getenv('DISCORD_GUILD_ID'); } public function sendMessage(string $channelId, string $content, ?array $embed = null, ?array $buttons = null): array { $channelId = $this->resolveUserToDmChannel($channelId); $data = ['content' => $content]; if ($embed) { $data['embeds'] = [$embed]; } if ($buttons) { // Construir componentes correctamente $components = []; foreach ($buttons as $row) { if (isset($row['components']) && is_array($row['components'])) { $componentRow = [ 'type' => 1, 'components' => [] ]; foreach ($row['components'] as $btn) { $componentRow['components'][] = [ 'type' => 2, 'style' => isset($btn['style']) ? intval($btn['style']) : 1, 'label' => $btn['label'], 'custom_id' => $btn['custom_id'] ]; } $components[] = $componentRow; } } if (!empty($components)) { $data['components'] = $components; } } return $this->request("POST", "/channels/{$channelId}/messages", $data); } private function cleanComponents(array $components): array { $clean = []; foreach ($components as $row) { $cleanRow = []; if (isset($row['components'])) { $cleanComponents = []; foreach ($row['components'] as $btn) { $cleanBtn = [ 'type' => 2, 'style' => $btn['style'] ?? 1, 'label' => $btn['label'], 'custom_id' => $btn['custom_id'] ]; $cleanComponents[] = $cleanBtn; } $cleanRow = [ 'type' => 1, 'components' => $cleanComponents ]; } else { $cleanRow = $row; } $clean[] = $cleanRow; } return $clean; } public function sendMessageWithImages(string $channelId, string $content, array $images, ?array $buttons = null): array { $channelId = $this->resolveUserToDmChannel($channelId); $result = null; if (!empty($images)) { // Verificar si las imágenes son locales o URLs $localImages = []; $remoteImages = []; foreach ($images as $imageUrl) { if (strpos($imageUrl, 'http') === 0) { // Es una URL remota $remoteImages[] = $imageUrl; } elseif (file_exists($imageUrl)) { // Es un archivo local $localImages[] = $imageUrl; } } // Enviar imágenes locales como adjuntos if (!empty($localImages)) { $result = $this->sendMessageWithAttachments($channelId, $content, $localImages); } else { $result = $this->sendMessage($channelId, $content, null, $buttons); } // Enviar imágenes remotas como embeds foreach ($remoteImages as $imageUrl) { $embed = [ 'image' => ['url' => $imageUrl] ]; $result = $this->sendMessage($channelId, '', $embed, $buttons); $buttons = null; // Solo enviar botones en el primer mensaje } } else { $result = $this->sendMessage($channelId, $content, null, $buttons); } return $result; } /** * Enviar contenido con texto e imágenes en el orden correcto * Divide el contenido en segmentos y los envía manteniendo el orden */ public function sendContentWithOrderedImages(string $channelId, array $segments): void { $channelId = $this->resolveUserToDmChannel($channelId); foreach ($segments as $segment) { if ($segment['type'] === 'text') { // Enviar texto if (!empty(trim($segment['content']))) { $this->sendMessage($channelId, $segment['content']); } } elseif ($segment['type'] === 'image') { // Enviar imagen $imagePath = $segment['src']; if (strpos($imagePath, 'http') === 0) { // URL remota - enviar como embed $embed = ['image' => ['url' => $imagePath]]; $this->sendMessage($channelId, '', $embed); } elseif (file_exists($imagePath)) { // Archivo local - enviar como adjunto $this->sendMessageWithAttachments($channelId, '', [$imagePath]); } } } } public function sendMessageWithAttachments(string $channelId, string $content, array $files): array { $channelId = $this->resolveUserToDmChannel($channelId); $url = $this->baseUrl . "/channels/{$channelId}/messages"; // Preparar los datos multipart $postData = [ 'content' => $content, 'payload_json' => json_encode(['content' => $content]) ]; // Agregar archivos $fileIndex = 0; foreach ($files as $filePath) { if (file_exists($filePath)) { $postData["file{$fileIndex}"] = new \CURLFile($filePath, mime_content_type($filePath), basename($filePath)); $fileIndex++; } } $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bot ' . $this->token, ]); curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $result = json_decode($response, true); if ($httpCode >= 400) { throw new \Exception("Discord API Error: " . ($result['message'] ?? 'Unknown error')); } return $result; } public function editMessage(string $channelId, string $messageId, string $content, ?array $embed = null): array { $data = ['content' => $content]; if ($embed) { $data['embeds'] = [$embed]; } return $this->request("PATCH", "/channels/{$channelId}/messages/{$messageId}", $data); } public function deleteMessage(string $channelId, string $messageId): bool { $this->request("DELETE", "/channels/{$channelId}/messages/{$messageId}"); return true; } public function getChannel(string $channelId): array { return $this->request("GET", "/channels/{$channelId}"); } public function getGuildChannels(): array { return $this->request("GET", "/guilds/{$this->guildId}/channels"); } private function buildActionRow(array $buttons): array { $components = []; foreach ($buttons as $button) { $component = [ 'type' => 2, 'style' => $button['style'] ?? 1, 'label' => $button['label'], 'custom_id' => $button['custom_id'] ]; if (isset($button['url'])) { $component['url'] = $button['url']; } $components[] = $component; } return [ [ 'type' => 1, 'components' => $components ] ]; } private function request(string $method, string $endpoint, ?array $data = null): array { $url = $this->baseUrl . $endpoint; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bot ' . $this->token, 'Content-Type: application/json' ]); if ($data) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $result = json_decode($response, true); if ($httpCode >= 400) { throw new \Exception("Discord API Error: " . ($result['message'] ?? 'Unknown error')); } return $result; } private function resolveUserToDmChannel(string $userId): string { try { $response = $this->request("POST", "/users/{$userId}/channels", [ 'recipient_id' => $userId ]); if (isset($response['id'])) { return $response['id']; } } catch (\Exception $e) { error_log("Error creating DM channel: " . $e->getMessage()); } return $userId; } public function splitMessage(string $content, int $maxLength = 2000): array { if (strlen($content) <= $maxLength) { return [$content]; } $parts = []; $lines = explode("\n", $content); $currentPart = ''; foreach ($lines as $line) { if (strlen($currentPart . "\n" . $line) > $maxLength) { if (!empty($currentPart)) { $parts[] = $currentPart; $currentPart = ''; } if (strlen($line) > $maxLength) { $chunks = str_split($line, $maxLength); $parts = array_merge($parts, array_slice($chunks, 0, -1)); $currentPart = end($chunks); } else { $currentPart = $line; } } else { $currentPart .= (empty($currentPart) ? '' : "\n") . $line; } } if (!empty($currentPart)) { $parts[] = $currentPart; } return $parts; } /** * Parsear HTML y dividirlo en segmentos manteniendo el orden * Retorna array de ['type' => 'text|image', 'content' => '...', 'src' => '...'] */ public function parseContent(string $html): array { $segments = []; $currentText = ''; // Usar regex para encontrar todas las etiquetas $pattern = '/]+src=["\']([^"\']+)["\'][^>]*>/i'; $parts = preg_split($pattern, $html, -1, PREG_SPLIT_DELIM_CAPTURE); // El array parts alterna entre: [texto, src_imagen, texto, src_imagen, texto...] for ($i = 0; $i < count($parts); $i++) { if ($i % 2 === 0) { // Es texto $text = $this->htmlToPlainText($parts[$i]); if (!empty(trim($text))) { $segments[] = [ 'type' => 'text', 'content' => $text ]; } } else { // Es una imagen (el src capturado) $segments[] = [ 'type' => 'image', 'src' => $parts[$i], 'content' => '' ]; } } return $segments; } /** * Convertir HTML a texto plano manteniendo saltos de línea */ private function htmlToPlainText(string $html): string { // Reemplazar
,

, etc. con saltos de línea $text = preg_replace('//i', "\n", $html); $text = preg_replace('/<\/p>/i', "\n", $text); $text = preg_replace('/]*>/i', '', $text); $text = preg_replace('/]*>/i', '', $text); $text = preg_replace('/<\/div>/i', "\n", $text); // Eliminar otras etiquetas HTML $text = strip_tags($text); // Decodificar entidades HTML $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); // Limpiar espacios múltiples y saltos de línea $text = preg_replace('/\n{3,}/', "\n\n", $text); $text = preg_replace('/[ \t]+/', ' ', $text); return trim($text); } public function extractImages(string $html): array { preg_match_all('/]+src=["\']([^"\']+)["\'][^>]*>/i', $html, $matches); return $matches[1] ?? []; } public function removeImages(string $html): string { return preg_replace('/]+>/i', '', $html); } }