Primer commit del sistema separado falta mejorar mucho
This commit is contained in:
374
Sistema_discord/CommandLocker.php
Executable file
374
Sistema_discord/CommandLocker.php
Executable file
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
class CommandLocker {
|
||||
private $pdo;
|
||||
private $lockTimeout = 300; // 5 minutos de tiempo de espera para el bloqueo
|
||||
private $debug = true;
|
||||
private $dbConnection;
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
// Configurar PDO para que lance excepciones
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
|
||||
|
||||
// Inicializar la conexión a la base de datos
|
||||
$this->getConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene una conexión activa a la base de datos
|
||||
*
|
||||
* @return PDO La conexión a la base de datos
|
||||
*/
|
||||
private function getConnection() {
|
||||
try {
|
||||
// Verificar si la conexión actual es válida
|
||||
if ($this->pdo === null || !$this->isConnectionAlive()) {
|
||||
$this->log("Estableciendo nueva conexión a la base de datos");
|
||||
$this->pdo = $this->createNewConnection();
|
||||
}
|
||||
return $this->pdo;
|
||||
} catch (Exception $e) {
|
||||
$this->log("Error al obtener conexión a la base de datos", [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si la conexión actual está activa
|
||||
*
|
||||
* @return bool True si la conexión está activa, false en caso contrario
|
||||
*/
|
||||
private function isConnectionAlive() {
|
||||
try {
|
||||
$this->pdo->query('SELECT 1');
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->log("La conexión a la base de datos no está activa", [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea una nueva conexión a la base de datos
|
||||
*
|
||||
* @return PDO La nueva conexión
|
||||
*/
|
||||
private function createNewConnection() {
|
||||
// Obtener la configuración de la base de datos del archivo de configuración
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
|
||||
// Obtener la instancia de la conexión del archivo db.php
|
||||
global $pdo;
|
||||
|
||||
if (!($pdo instanceof PDO)) {
|
||||
throw new Exception("No se pudo establecer una conexión a la base de datos");
|
||||
}
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
private function log($message, $data = []) {
|
||||
if ($this->debug) {
|
||||
$logMessage = date('[Y-m-d H:i:s] ') . $message;
|
||||
if (!empty($data)) {
|
||||
$logMessage .= ' ' . json_encode($data);
|
||||
}
|
||||
error_log($logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intenta adquirir un bloqueo para un comando
|
||||
*
|
||||
* @param string $command El comando a bloquear
|
||||
* @param int $chatId El ID del chat donde se ejecutó el comando
|
||||
* @param string $type Tipo de bloqueo ('command' o 'translation')
|
||||
* @param array $data Datos adicionales a almacenar con el bloqueo
|
||||
* @return array|false Retorna el ID del bloqueo si se adquiere, false si ya existe un bloqueo activo
|
||||
*/
|
||||
public function acquireLock($command, $chatId, $type = 'command', $data = []) {
|
||||
$this->log("Intentando adquirir bloqueo", [
|
||||
'command' => $command,
|
||||
'chatId' => $chatId,
|
||||
'type' => $type,
|
||||
'data' => $data
|
||||
]);
|
||||
|
||||
$this->cleanupExpiredLocks();
|
||||
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
// Buscar cualquier bloqueo existente para este chat y comando (sin filtrar por estado)
|
||||
$query = "
|
||||
SELECT id, status, expires_at, data, created_at
|
||||
FROM command_locks
|
||||
WHERE command = ?
|
||||
AND chat_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1 FOR UPDATE
|
||||
";
|
||||
|
||||
$stmt = $this->pdo->prepare($query);
|
||||
|
||||
$this->log("Ejecutando consulta de bloqueo existente", [
|
||||
'query' => $stmt->queryString,
|
||||
'params' => [$command, $chatId, $type]
|
||||
]);
|
||||
|
||||
$stmt->execute([$command, $chatId]);
|
||||
$existingLock = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existingLock) {
|
||||
$this->log("Bloqueo existente encontrado", $existingLock);
|
||||
|
||||
// Si hay un bloqueo activo y reciente, bloquear nueva ejecución
|
||||
if ($existingLock['status'] === 'processing' && strtotime($existingLock['created_at']) > strtotime('-5 minutes')) {
|
||||
$this->log("Bloqueo ya en proceso, rechazando nueva solicitud");
|
||||
$this->pdo->rollBack();
|
||||
return false; // Ya hay un bloqueo activo
|
||||
}
|
||||
|
||||
// Reutilizar el mismo registro para evitar violar la única (chat_id,command)
|
||||
$expiresAt = (new DateTime('+5 minutes'))->format('Y-m-d H:i:s');
|
||||
$dataJson = !empty($data) ? json_encode($data) : null;
|
||||
$upd = $this->pdo->prepare("UPDATE command_locks SET type = ?, status='processing', data = COALESCE(?, data), message_id = NULL, expires_at = ?, updated_at = NOW() WHERE id = ?");
|
||||
$upd->execute([$type, $dataJson, $expiresAt, $existingLock['id']]);
|
||||
$this->pdo->commit();
|
||||
$this->log("Bloqueo reutilizado y pasado a processing", ['lockId' => $existingLock['id']]);
|
||||
return (int)$existingLock['id'];
|
||||
} else {
|
||||
$this->log("No se encontraron bloqueos existentes");
|
||||
}
|
||||
|
||||
// Insertar un nuevo bloqueo
|
||||
$expiresAt = (new DateTime('+5 minutes'))->format('Y-m-d H:i:s');
|
||||
$dataJson = !empty($data) ? json_encode($data) : null;
|
||||
|
||||
$query = "
|
||||
INSERT INTO command_locks
|
||||
(chat_id, command, type, data, status, expires_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'processing', ?, NOW(), NOW())
|
||||
";
|
||||
|
||||
$stmt = $this->pdo->prepare($query);
|
||||
|
||||
$this->log("Insertando nuevo bloqueo", [
|
||||
'chat_id' => $chatId,
|
||||
'command' => $command,
|
||||
'type' => $type,
|
||||
'has_data' => !empty($data),
|
||||
'expires_at' => $expiresAt
|
||||
]);
|
||||
|
||||
$stmt->execute([$chatId, $command, $type, $dataJson, $expiresAt]);
|
||||
|
||||
$lockId = $this->pdo->lastInsertId();
|
||||
$this->pdo->commit();
|
||||
|
||||
$this->log("Bloqueo adquirido exitosamente", ['lockId' => $lockId]);
|
||||
|
||||
return (int)$lockId;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->log("Error al adquirir bloqueo", [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
try {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
} catch (Exception $rollbackException) {
|
||||
$this->log("Error al hacer rollback", [
|
||||
'error' => $rollbackException->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza el estado de un bloqueo
|
||||
*
|
||||
* @param int $lockId ID del bloqueo a actualizar
|
||||
* @param string $status Nuevo estado ('processing', 'completed', 'failed')
|
||||
* @param string|null $messageId ID del mensaje asociado (opcional)
|
||||
* @return bool True si se actualizó correctamente, false en caso contrario
|
||||
*/
|
||||
public function updateLockStatus($lockId, $status, $messageId = null) {
|
||||
$this->log("Actualizando estado de bloqueo", [
|
||||
'lockId' => $lockId,
|
||||
'status' => $status,
|
||||
'messageId' => $messageId
|
||||
]);
|
||||
|
||||
try {
|
||||
// Asegurarse de que tenemos una conexión válida
|
||||
$pdo = $this->getConnection();
|
||||
|
||||
$query = "
|
||||
UPDATE command_locks
|
||||
SET status = ?,
|
||||
message_id = COALESCE(?, message_id),
|
||||
expires_at = CASE WHEN ? = 'completed' THEN DATE_ADD(NOW(), INTERVAL 1 MINUTE) ELSE expires_at END,
|
||||
updated_at = NOW()
|
||||
WHERE id = ?
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$result = $stmt->execute([$status, $messageId, $status, $lockId]);
|
||||
$rowCount = $stmt->rowCount();
|
||||
|
||||
$this->log("Resultado de actualización de estado", [
|
||||
'lockId' => $lockId,
|
||||
'status' => $status,
|
||||
'rowCount' => $rowCount,
|
||||
'result' => $result
|
||||
]);
|
||||
|
||||
// Si no se actualizó ninguna fila, verificar si el bloqueo existe
|
||||
if ($rowCount === 0) {
|
||||
$checkStmt = $pdo->prepare("SELECT id FROM command_locks WHERE id = ?");
|
||||
$checkStmt->execute([$lockId]);
|
||||
$exists = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$this->log("Verificación de existencia de bloqueo", [
|
||||
'lockId' => $lockId,
|
||||
'exists' => (bool)$exists
|
||||
]);
|
||||
|
||||
if (!$exists) {
|
||||
$this->log("Error: El bloqueo no existe", ['lockId' => $lockId]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $result && $rowCount > 0;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->log("Error al actualizar estado de bloqueo", [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Libera un bloqueo marcándolo como completado
|
||||
*
|
||||
* @param int $lockId ID del bloqueo a liberar
|
||||
* @param string|null $messageId ID del mensaje asociado (opcional)
|
||||
* @return bool True si se actualizó correctamente, false en caso contrario
|
||||
*/
|
||||
public function releaseLock($lockId, $messageId = null) {
|
||||
$this->log("Liberando bloqueo", [
|
||||
'lockId' => $lockId,
|
||||
'messageId' => $messageId
|
||||
]);
|
||||
|
||||
$result = $this->updateLockStatus($lockId, 'completed', $messageId);
|
||||
|
||||
$this->log("Resultado de liberación de bloqueo", [
|
||||
'lockId' => $lockId,
|
||||
'success' => $result
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marca un bloqueo como fallido
|
||||
*
|
||||
* @param int $lockId ID del bloqueo a marcar como fallido
|
||||
* @param string $errorMessage Mensaje de error (opcional)
|
||||
* @return bool True si se actualizó correctamente, false en caso contrario
|
||||
*/
|
||||
public function failLock($lockId, $errorMessage = '') {
|
||||
$this->log("Marcando bloqueo como fallido", [
|
||||
'lockId' => $lockId,
|
||||
'errorMessage' => $errorMessage
|
||||
]);
|
||||
|
||||
try {
|
||||
// Asegurarse de que tenemos una conexión válida
|
||||
$pdo = $this->getConnection();
|
||||
|
||||
$query = "
|
||||
UPDATE command_locks
|
||||
SET status = 'failed',
|
||||
data = JSON_MERGE_PATCH(COALESCE(data, '{}'), ?),
|
||||
expires_at = DATE_ADD(NOW(), INTERVAL 5 MINUTE),
|
||||
updated_at = NOW()
|
||||
WHERE id = ?
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$result = $stmt->execute([json_encode(['error' => $errorMessage, 'failed_at' => date('Y-m-d H:i:s')]), $lockId]);
|
||||
$rowCount = $stmt->rowCount();
|
||||
|
||||
$this->log("Resultado de marcar bloqueo como fallido", [
|
||||
'lockId' => $lockId,
|
||||
'rowCount' => $rowCount,
|
||||
'result' => $result
|
||||
]);
|
||||
|
||||
return $result && $rowCount > 0;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->log("Error al marcar bloqueo como fallido", [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia los bloqueos expirados
|
||||
*/
|
||||
private function cleanupExpiredLocks() {
|
||||
try {
|
||||
$this->pdo->exec("
|
||||
DELETE FROM command_locks
|
||||
WHERE (status = 'completed' AND expires_at <= NOW())
|
||||
OR (status = 'processing' AND created_at < DATE_SUB(NOW(), INTERVAL 1 HOUR))
|
||||
");
|
||||
} catch (Exception $e) {
|
||||
error_log("Error al limpiar bloqueos expirados: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un comando está siendo procesado actualmente
|
||||
*
|
||||
* @param string $command Comando a verificar
|
||||
* @param int $chatId ID del chat
|
||||
* @param string $type Tipo de bloqueo ('command' o 'translation')
|
||||
* @return bool True si el comando está siendo procesado, false en caso contrario
|
||||
*/
|
||||
public function isCommandProcessing($command, $chatId, $type = 'command') {
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM command_locks
|
||||
WHERE command = ?
|
||||
AND chat_id = ?
|
||||
AND type = ?
|
||||
AND status = 'processing'
|
||||
AND created_at > DATE_SUB(NOW(), INTERVAL 5 MINUTE)
|
||||
");
|
||||
$stmt->execute([$command, $chatId, $type]);
|
||||
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
}
|
||||
266
Sistema_discord/DiscordSender.php
Executable file
266
Sistema_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
209
Sistema_discord/HtmlToDiscordMarkdownConverter.php
Executable file
209
Sistema_discord/HtmlToDiscordMarkdownConverter.php
Executable file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
// Incluir el archivo de 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
172
Sistema_discord/Translate.php
Executable file
172
Sistema_discord/Translate.php
Executable file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
class Translate
|
||||
{
|
||||
private $apiUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiUrl = rtrim($_ENV['LIBRETRANSLATE_URL'], '/');
|
||||
}
|
||||
|
||||
public function detectLanguage($text)
|
||||
{
|
||||
if (empty(trim($text))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = $this->request('/detect', ['q' => $text]);
|
||||
|
||||
if (isset($response[0]['language'])) {
|
||||
return $response[0]['language'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function translateText($text, $source, $target)
|
||||
{
|
||||
if (empty(trim($text))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = $this->request('/translate', [
|
||||
'q' => $text,
|
||||
'source' => $source,
|
||||
'target' => $target,
|
||||
'format' => 'text'
|
||||
]);
|
||||
|
||||
return $response['translatedText'] ?? null;
|
||||
}
|
||||
|
||||
public function translateHtml($html, $source, $target)
|
||||
{
|
||||
if (empty(trim($html))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$maxLength = 4000; // Límite de caracteres para LibreTranslate
|
||||
$translatedHtml = '';
|
||||
|
||||
custom_log("translateHtml: Original HTML length: " . mb_strlen($html));
|
||||
// Dividir el HTML en bloques para evitar exceder el límite de LibreTranslate
|
||||
$chunks = $this->splitHtmlIntoChunks($html, $maxLength);
|
||||
custom_log("translateHtml: Number of chunks: " . count($chunks));
|
||||
|
||||
foreach ($chunks as $index => $chunk) {
|
||||
custom_log("translateHtml: Processing chunk " . ($index + 1) . "/" . count($chunks) . ", length: " . mb_strlen($chunk));
|
||||
$response = $this->request('/translate', [
|
||||
'q' => $chunk,
|
||||
'source' => $source,
|
||||
'target' => $target,
|
||||
'format' => 'html'
|
||||
]);
|
||||
|
||||
$translatedChunk = $response['translatedText'] ?? null;
|
||||
if ($translatedChunk) {
|
||||
$translatedHtml .= $translatedChunk;
|
||||
custom_log("translateHtml: Chunk " . ($index + 1) . " translated successfully.");
|
||||
} else {
|
||||
// Si una parte falla, devolver la parte original para no perder contenido
|
||||
$translatedHtml .= $chunk;
|
||||
custom_log("translateHtml: Chunk " . ($index + 1) . " failed to translate. Appending original chunk.");
|
||||
}
|
||||
}
|
||||
|
||||
return $translatedHtml;
|
||||
}
|
||||
|
||||
private function splitHtmlIntoChunks($html, $maxLength)
|
||||
{
|
||||
$chunks = [];
|
||||
$currentChunk = '';
|
||||
$dom = new DOMDocument();
|
||||
// Suprimir errores de HTML mal formado
|
||||
@$dom->loadHTML('<div>' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
$xpath = new DOMXPath($dom);
|
||||
|
||||
// Query all direct children of the body (or the implied div)
|
||||
$nodes = $xpath->query('//body/*');
|
||||
if ($nodes->length === 0) {
|
||||
// If no block-level elements, treat the whole HTML as one chunk
|
||||
$nodes = $xpath->query('//body/text()');
|
||||
if ($nodes->length === 0) {
|
||||
// Fallback if no text nodes either, just return the original HTML as one chunk
|
||||
return [$html];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$nodeHtml = $dom->saveHTML($node);
|
||||
custom_log("splitHtmlIntoChunks: Processing node, length: " . mb_strlen($nodeHtml) . ", HTML: " . substr($nodeHtml, 0, 100) . "...");
|
||||
|
||||
if (mb_strlen($currentChunk . $nodeHtml) <= $maxLength) {
|
||||
$currentChunk .= $nodeHtml;
|
||||
} else {
|
||||
if (!empty($currentChunk)) {
|
||||
$chunks[] = $currentChunk;
|
||||
custom_log("splitHtmlIntoChunks: Added chunk, length: " . mb_strlen($currentChunk));
|
||||
}
|
||||
$currentChunk = $nodeHtml;
|
||||
}
|
||||
}
|
||||
if (!empty($currentChunk)) {
|
||||
$chunks[] = $currentChunk;
|
||||
custom_log("splitHtmlIntoChunks: Added final chunk, length: " . mb_strlen($currentChunk));
|
||||
}
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
public function translateTextBatch($texts, $source, $target)
|
||||
{
|
||||
if (empty($texts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = $this->request('/translate', [
|
||||
'q' => $texts,
|
||||
'source' => $source,
|
||||
'target' => $target,
|
||||
'format' => 'text'
|
||||
]);
|
||||
|
||||
return $response['translatedText'] ?? null;
|
||||
}
|
||||
|
||||
public function getSupportedLanguages()
|
||||
{
|
||||
$response = $this->request('/languages', [], 'GET');
|
||||
return $response ?? [];
|
||||
}
|
||||
|
||||
private function request($endpoint, $data, $method = 'POST')
|
||||
{
|
||||
$url = $this->apiUrl . $endpoint;
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Considera la seguridad de esto
|
||||
|
||||
if ($method === 'POST') {
|
||||
custom_log("LibreTranslate POST Request: URL=" . $url . " | Data=" . json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
} else {
|
||||
custom_log("LibreTranslate GET Request: URL=" . $url);
|
||||
// Para GET, los datos (si los hubiera) se añadirían a la URL, pero /languages no requiere
|
||||
}
|
||||
|
||||
$response_body = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
custom_log("LibreTranslate Response: HTTP Code=" . $http_code . " | Body=" . $response_body);
|
||||
|
||||
if ($http_code >= 400) {
|
||||
return null; // Devolver null si hay un error de cliente o servidor
|
||||
}
|
||||
|
||||
return json_decode($response_body, true);
|
||||
}
|
||||
}
|
||||
125
Sistema_discord/config.php
Executable file
125
Sistema_discord/config.php
Executable file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
// config/config.php
|
||||
|
||||
// Cargar variables de entorno
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// Determinar el nombre del archivo .env basado en la variable de entorno
|
||||
// Primero revisa getenv() para la línea de comandos, luego $_SERVER para el entorno web.
|
||||
$env = getenv('APP_ENVIRONMENT') ?: ($_SERVER['APP_ENVIRONMENT'] ?? null);
|
||||
|
||||
// Por defecto, si no hay entorno definido, no cargará ningún archivo específico.
|
||||
$envFile = '.env';
|
||||
if ($env) {
|
||||
$envFile = '.env.' . $env;
|
||||
}
|
||||
|
||||
// Cargar el archivo .env correspondiente
|
||||
$dotenv = null;
|
||||
if (file_exists(dirname(__DIR__) . '/' . $envFile)) {
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__), $envFile);
|
||||
} elseif (file_exists(dirname(__DIR__) . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
} else {
|
||||
die('Error: No se pudo encontrar un archivo de configuración de entorno (.env) válido. Se buscó ' . htmlspecialchars($envFile) . ' y .env');
|
||||
}
|
||||
|
||||
try {
|
||||
$dotenv->load();
|
||||
} catch (Exception $e) {
|
||||
die('Error al cargar el archivo de entorno: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Validar variables requeridas
|
||||
$dotenv->required([
|
||||
'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS',
|
||||
'JWT_SECRET', 'APP_URL'
|
||||
]);
|
||||
|
||||
// Environment Configuration
|
||||
define('ENVIRONMENT', $_ENV['APP_ENV'] ?? 'production'); // 'development' or 'production'
|
||||
|
||||
|
||||
|
||||
// Detectar si se ejecuta desde la línea de comandos
|
||||
$is_cli = (php_sapi_name() === 'cli' || defined('STDIN'));
|
||||
|
||||
// Configurar la URL base y el protocolo
|
||||
if ($is_cli) {
|
||||
// En CLI, usar siempre la APP_URL del .env y no necesitamos protocolo
|
||||
define('BOT_BASE_URL', $_ENV['APP_URL']);
|
||||
$protocol = 'http'; // Valor por defecto, no se usa realmente
|
||||
} else {
|
||||
// En entorno web, detectar protocolo dinámicamente
|
||||
$protocol = 'http';
|
||||
if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
|
||||
(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||
|
||||
(!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') ||
|
||||
(!empty($_SERVER['HTTP_CF_VISITOR']) && strpos($_SERVER['HTTP_CF_VISITOR'], 'https' ) !== false)) {
|
||||
$protocol = 'https';
|
||||
$_SERVER['HTTPS'] = 'on';
|
||||
$_SERVER['SERVER_PORT'] = 443;
|
||||
}
|
||||
define('BOT_BASE_URL', $_ENV['APP_URL'] ?? ($protocol . '://' . $_SERVER['HTTP_HOST']));
|
||||
$_SERVER['REQUEST_SCHEME'] = $protocol;
|
||||
}
|
||||
define('BASE_PATH', dirname(__DIR__));
|
||||
|
||||
// Database Configuration
|
||||
define('DB_HOST', $_ENV['DB_HOST']);
|
||||
define('DB_USER', $_ENV['DB_USER']);
|
||||
define('DB_PASS', $_ENV['DB_PASS']);
|
||||
define('DB_NAME', $_ENV['DB_NAME']);
|
||||
define('DB_DIALECT', $_ENV['DB_DIALECT']);
|
||||
define('DB_PORT', $_ENV['DB_PORT']);
|
||||
|
||||
// Session Configuration
|
||||
define('SESSION_SECRET', $_ENV['JWT_SECRET']);
|
||||
|
||||
// Discord API Configuration
|
||||
define('DISCORD_GUILD_ID', $_ENV['DISCORD_GUILD_ID']);
|
||||
define('DISCORD_CLIENT_ID', $_ENV['DISCORD_CLIENT_ID']);
|
||||
define('DISCORD_CLIENT_SECRET', $_ENV['DISCORD_CLIENT_SECRET']);
|
||||
define('DISCORD_BOT_TOKEN', $_ENV['DISCORD_BOT_TOKEN']);
|
||||
|
||||
// Telegram API Configuration
|
||||
define('TELEGRAM_BOT_TOKEN', $_ENV['TELEGRAM_BOT_TOKEN']);
|
||||
define('TELEGRAM_WEBHOOK_TOKEN', $_ENV['TELEGRAM_WEBHOOK_TOKEN']);
|
||||
|
||||
// Error Reporting
|
||||
if (defined('ENVIRONMENT')) {
|
||||
switch (ENVIRONMENT) {
|
||||
case 'development':
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('log_errors', '1');
|
||||
ini_set('error_log', dirname(__DIR__) . '/logs/php_errors.log');
|
||||
break;
|
||||
case 'production':
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
|
||||
ini_set('display_errors', '0');
|
||||
ini_set('log_errors', '1');
|
||||
ini_set('error_log', dirname(__DIR__) . '/logs/php_errors.log');
|
||||
break;
|
||||
default:
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('log_errors', '1');
|
||||
ini_set('error_log', dirname(__DIR__) . '/logs/php_errors.log');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Helper function to get full URL
|
||||
function url($path = '') {
|
||||
$path = ltrim($path, '/');
|
||||
return BOT_BASE_URL . '/' . $path;
|
||||
}
|
||||
|
||||
// Helper function to get full asset URL
|
||||
function asset_url($path = '') {
|
||||
$path = ltrim($path, '/');
|
||||
return BOT_BASE_URL . '/assets/' . $path;
|
||||
}
|
||||
126
Sistema_discord/db.php
Executable file
126
Sistema_discord/db.php
Executable file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
// Establecer la zona horaria predeterminada
|
||||
date_default_timezone_set('America/Mexico_City');
|
||||
|
||||
/**
|
||||
* Clase para manejar la conexión a la base de datos con reconexión automática
|
||||
*/
|
||||
class DatabaseConnection {
|
||||
private static $instance = null;
|
||||
private $pdo = null;
|
||||
private $config = [];
|
||||
|
||||
private function __construct() {
|
||||
$this->config = [
|
||||
'host' => $_ENV['DB_HOST'] ?? 'localhost',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'name' => $_ENV['DB_NAME'] ?? 'bot',
|
||||
'user' => $_ENV['DB_USER'] ?? 'nickpons666',
|
||||
'pass' => $_ENV['DB_PASS'] ?? 'MiPo6425@@',
|
||||
'charset' => 'utf8mb4',
|
||||
'timeout' => 30, // Tiempo de espera de conexión en segundos
|
||||
'reconnect_attempts' => 3, // Número de intentos de reconexión
|
||||
'reconnect_delay' => 1, // Tiempo de espera entre reconexiones en segundos
|
||||
];
|
||||
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function getConnection() {
|
||||
// Verificar si la conexión sigue activa
|
||||
try {
|
||||
$this->pdo->query('SELECT 1');
|
||||
return $this->pdo;
|
||||
} catch (PDOException $e) {
|
||||
// Si la conexión se perdió, intentar reconectar
|
||||
error_log("La conexión a la base de datos se perdió. Intentando reconectar...");
|
||||
$this->connect();
|
||||
return $this->pdo;
|
||||
}
|
||||
}
|
||||
|
||||
private function connect() {
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||
$this->config['host'],
|
||||
$this->config['port'],
|
||||
$this->config['name'],
|
||||
$this->config['charset']
|
||||
);
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_TIMEOUT => $this->config['timeout'],
|
||||
PDO::ATTR_PERSISTENT => false, // No usar conexiones persistentes para evitar problemas
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
|
||||
];
|
||||
|
||||
$attempts = 0;
|
||||
$lastException = null;
|
||||
|
||||
while ($attempts < $this->config['reconnect_attempts']) {
|
||||
try {
|
||||
$this->pdo = new PDO(
|
||||
$dsn,
|
||||
$this->config['user'],
|
||||
$this->config['pass'],
|
||||
$options
|
||||
);
|
||||
|
||||
// Configuración adicional de la conexión
|
||||
$this->pdo->exec("SET time_zone = '-06:00';");
|
||||
$this->pdo->exec("SET SESSION wait_timeout=28800;"); // 8 horas
|
||||
$this->pdo->exec("SET SESSION interactive_timeout=28800;"); // 8 horas
|
||||
|
||||
error_log("Conexión a la base de datos establecida correctamente.");
|
||||
return;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$lastException = $e;
|
||||
$attempts++;
|
||||
error_log(sprintf(
|
||||
"Intento de conexión %d fallido: %s. Reintentando en %d segundos...",
|
||||
$attempts,
|
||||
$e->getMessage(),
|
||||
$this->config['reconnect_delay']
|
||||
));
|
||||
|
||||
if ($attempts < $this->config['reconnect_attempts']) {
|
||||
sleep($this->config['reconnect_delay']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si llegamos aquí, todos los intentos fallaron
|
||||
error_log("No se pudo establecer la conexión después de {$this->config['reconnect_attempts']} intentos.");
|
||||
throw $lastException;
|
||||
}
|
||||
}
|
||||
|
||||
// Crear una instancia de la conexión
|
||||
try {
|
||||
$pdo = DatabaseConnection::getInstance()->getConnection();
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error crítico de conexión a la base de datos: " . $e->getMessage());
|
||||
// No usar die() aquí porque mata a los workers - dejar que el código maneje el error
|
||||
// Para scripts web, el error se mostrará en el log y el script continuará
|
||||
// Para workers, pueden manejar la excepción y reintentar
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
// Solo para contexto web
|
||||
die("Error de conexión a la base de datos. Por favor, inténtalo de nuevo más tarde.");
|
||||
}
|
||||
// Para CLI (workers), lanzar la excepción para que el worker la maneje
|
||||
throw $e;
|
||||
}
|
||||
?>
|
||||
10
Sistema_discord/logger.php
Executable file
10
Sistema_discord/logger.php
Executable file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// Función de depuración personalizada
|
||||
if (!function_exists('custom_log')) {
|
||||
function custom_log($message) {
|
||||
$logFile = __DIR__ . '/../logs/custom_debug.log'; // Adjust path as needed
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
file_put_contents('php://stderr', "[$timestamp] $message\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user