Files
lastwar/discord/DiscordSender.php

415 lines
13 KiB
PHP
Executable File

<?php
namespace Discord;
class DiscordSender
{
private string $token;
private string $guildId;
private string $baseUrl = 'https://discord.com/api/v10';
public function __construct()
{
$this->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 <img>
$pattern = '/<img[^>]+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 <br>, <p>, etc. con saltos de línea
$text = preg_replace('/<br\s*\/?>/i', "\n", $html);
$text = preg_replace('/<\/p>/i', "\n", $text);
$text = preg_replace('/<p[^>]*>/i', '', $text);
$text = preg_replace('/<div[^>]*>/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('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $html, $matches);
return $matches[1] ?? [];
}
public function removeImages(string $html): string
{
return preg_replace('/<img[^>]+>/i', '', $html);
}
}