Primer commit del sistema separado falta mejorar mucho

This commit is contained in:
nickpons666
2025-12-30 01:18:46 -06:00
commit 1679c73e52
2384 changed files with 472342 additions and 0 deletions

40
shared/api/php_errors.log Executable file
View File

@@ -0,0 +1,40 @@
[30-Nov-2025 06:47:27 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29
[30-Nov-2025 06:47:40 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29
[30-Nov-2025 06:47:45 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29
[30-Nov-2025 06:47:48 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29
[30-Nov-2025 06:47:52 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29
[30-Nov-2025 06:47:58 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29
[30-Nov-2025 06:48:18 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29
[30-Nov-2025 06:48:26 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29
[30-Nov-2025 06:51:10 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29
[30-Nov-2025 21:00:05 UTC] PHP Fatal error: Uncaught Error: Call to undefined function hasPermission() in /var/www/html/bot/shared/api/stats.php:29
Stack trace:
#0 {main}
thrown in /var/www/html/bot/shared/api/stats.php on line 29

78
shared/api/stats.php Executable file
View File

@@ -0,0 +1,78 @@
<?php
/**
* API de estadísticas para el panel principal
*/
header('Content-Type: application/json');
// Cargar variables de entorno
if (file_exists(__DIR__ . '/../../.env')) {
$lines = file(__DIR__ . '/../../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
if (strpos($line, '=') === false) continue;
list($key, $value) = explode('=', $line, 2);
$_ENV[trim($key)] = trim($value);
}
}
require_once __DIR__ . '/../database/connection.php';
require_once __DIR__. '/../auth/jwt.php';
// Verificar autenticación
$userData = JWTAuth::authenticate();
if (!$userData) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'No autenticado']);
exit;
}
try {
$db = getDB();
// Estadísticas de Discord
$discordStats = [
'users' => 0,
'messages' => 0,
'templates' => 0
];
$stmt = $db->query("SELECT COUNT(*) as total FROM destinatarios_discord WHERE activo = 1 AND tipo = 'usuario'");
$discordStats['users'] = $stmt->fetch()['total'];
$stmt = $db->query("SELECT COUNT(*) as total FROM mensajes_discord WHERE estado = 'enviado'");
$discordStats['messages'] = $stmt->fetch()['total'];
$stmt = $db->query("SELECT COUNT(*) as total FROM plantillas_discord");
$discordStats['templates'] = $stmt->fetch()['total'];
// Estadísticas de Telegram
$telegramStats = [
'users' => 0,
'messages' => 0,
'templates' => 0
];
$stmt = $db->query("SELECT COUNT(*) as total FROM destinatarios_telegram WHERE activo = 1 AND tipo = 'usuario'");
$telegramStats['users'] = $stmt->fetch()['total'];
$stmt = $db->query("SELECT COUNT(*) as total FROM mensajes_telegram WHERE estado = 'enviado'");
$telegramStats['messages'] = $stmt->fetch()['total'];
$stmt = $db->query("SELECT COUNT(*) as total FROM plantillas_telegram");
$telegramStats['templates'] = $stmt->fetch()['total'];
echo json_encode([
'success' => true,
'discord' => $discordStats,
'telegram' => $telegramStats
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Error al obtener estadísticas'
]);
error_log('Error en stats.php: ' . $e->getMessage());
}

200
shared/auth/jwt.php Executable file
View File

@@ -0,0 +1,200 @@
<?php
/**
* Utilidades JWT para autenticación
* Basado en Firebase PHP-JWT
*/
require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../database/connection.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JWTAuth {
private static $secret;
private static $algorithm = 'HS256';
private static $expiration = 3600; // 1 hora por defecto
private static $userData = null; // Para almacenar los datos del usuario autenticado
private static function init() {
if (self::$secret === null) {
self::$secret = $_ENV['JWT_SECRET'] ?? getenv('JWT_SECRET');
$algo = $_ENV['JWT_ALGORITHM'] ?? getenv('JWT_ALGORITHM');
$exp = $_ENV['JWT_EXPIRATION'] ?? getenv('JWT_EXPIRATION');
if ($algo) self::$algorithm = $algo;
if ($exp) self::$expiration = (int)$exp;
}
}
/**
* Generar un token JWT
*/
public static function generateToken($userId, $username, $rol, $idioma = 'es', $permisos = []) {
self::init();
$issuedAt = time();
$expire = $issuedAt + self::$expiration;
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
'iss' => $_ENV['APP_URL'] ?? getenv('APP_URL'),
'data' => [
'userId' => $userId,
'username' => $username,
'rol' => $rol,
'idioma' => $idioma,
'permissions' => $permisos // Cambiado a 'permissions' para consistencia
]
];
return JWT::encode($payload, self::$secret, self::$algorithm);
}
/**
* Validar y decodificar un token JWT
*/
public static function validateToken($token) {
self::init();
try {
$decoded = JWT::decode($token, new Key(self::$secret, self::$algorithm));
return [
'valid' => true,
'data' => $decoded->data
];
} catch (Exception $e) {
return [
'valid' => false,
'error' => $e->getMessage()
];
}
}
/**
* Refrescar un token JWT
*/
public static function refreshToken($token) {
$result = self::validateToken($token);
if (!$result['valid']) {
return null;
}
$data = $result['data'];
return self::generateToken(
$data->userId,
$data->username,
$data->rol,
$data->idioma,
(array)$data->permissions // Cambiado a 'permissions'
);
}
/**
* Extraer el token del header Authorization
*/
public static function getTokenFromHeader() {
// Compatibilidad con todos los entornos PHP
if (function_exists('getallheaders')) {
$headers = getallheaders();
} else {
$headers = [];
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
}
if (isset($headers['Authorization'])) {
$matches = [];
if (preg_match('/Bearer\s+(.*)$/i', $headers['Authorization'], $matches)) {
return $matches[1];
}
}
return null;
}
/**
* Middleware de autenticación
* Retorna los datos del usuario si el token es válido, o false si no
*/
public static function authenticate() {
if (self::$userData !== null) {
return self::$userData; // Ya autenticado
}
// Intentar obtener el token del header
$token = self::getTokenFromHeader();
// Si no está en el header, buscar en cookie
if (!$token && isset($_COOKIE['auth_token'])) {
$token = $_COOKIE['auth_token'];
}
if (!$token) {
return false;
}
$result = self::validateToken($token);
if (!$result['valid']) {
return false;
}
self::$userData = $result['data'];
return self::$userData;
}
/**
* Obtener los datos del usuario autenticado.
* Asume que authenticate() o requireAuth() ya han sido llamados.
*/
public static function getUserData() {
return self::$userData;
}
/**
* Middleware que requiere autenticación
* Redirige al login si no está autenticado
*/
public static function requireAuth($redirectTo = '/login.php') {
$userData = self::authenticate();
if (!$userData) {
header('Location: ' . $redirectTo);
exit;
}
return $userData;
}
/**
* Cargar los permisos de un usuario desde la base de datos
*/
public static function loadUserPermissions($userId) {
try {
$db = getDB();
$stmt = $db->prepare("
SELECT p.nombre
FROM permisos p
INNER JOIN usuarios_permisos up ON p.id = up.permiso_id
WHERE up.usuario_id = ?
");
$stmt->execute([$userId]);
$permisos = $stmt->fetchAll(PDO::FETCH_COLUMN);
return $permisos;
} catch (PDOException $e) {
error_log("Error cargando permisos: " . $e->getMessage());
return [];
}
}
}

50
shared/bootstrap.php Executable file
View File

@@ -0,0 +1,50 @@
<?php
/**
* Bootstrap File
*
* Carga y configura todos los componentes esenciales de la aplicación.
*/
// Iniciar sesión si no está iniciada
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Habilitar logging de errores
ini_set('display_errors', 0); // No mostrar errores al usuario
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/../logs/php_errors.log'); // Ruta centralizada
// Cargar variables de entorno
if (file_exists(__DIR__ . '/../.env')) {
$lines = file(__DIR__ . '/../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
if (strpos($line, '=') === false) continue;
list($key, $value) = explode('=', $line, 2);
$_ENV[trim($key)] = trim($value);
}
}
// 1. Cargar la conexión a la base de datos
require_once __DIR__ . '/database/connection.php';
// 2. Cargar el helper de autenticación JWT
require_once __DIR__ . '/auth/jwt.php';
// 3. Cargar helpers generales (que ahora pueden asumir que la DB y JWT existen)
require_once __DIR__ . '/utils/helpers.php';
// 4. Realizar la autenticación y obtener los datos del usuario
// Esto se hace una sola vez y los datos se guardan en la clase JWTAuth
try {
$userData = JWTAuth::requireAuth();
} catch (Exception $e) {
// Si la autenticación falla, redirigir al login
// Esto es más seguro que mostrar un 'die()', ya que no expone la estructura de archivos.
header('Location: /login.php');
exit;
}
// 5. Cargar el gestor de traducciones (que depende de los datos del usuario para el idioma)
require_once __DIR__ . '/translations/manager.php';

76
shared/database/connection.php Executable file
View File

@@ -0,0 +1,76 @@
<?php
/**
* Conexión a Base de Datos Compartida
* Utilizada por Discord, Telegram y todos los módulos del sistema
*/
class Database {
private static $instance = null;
private $connection;
private function __construct() {
$this->connect();
}
private function connect() {
try {
$host = $_ENV['DB_HOST'] ?? getenv('DB_HOST');
$port = $_ENV['DB_PORT'] ?? getenv('DB_PORT');
$dbname = $_ENV['DB_NAME'] ?? getenv('DB_NAME');
$user = $_ENV['DB_USER'] ?? getenv('DB_USER');
$pass = $_ENV['DB_PASS'] ?? getenv('DB_PASS');
$dsn = "mysql:host={$host};port={$port};dbname={$dbname};charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
];
$this->connection = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
$logFile = __DIR__ . '/../../logs/database_errors.log';
$logMessage = date('Y-m-d H:i:s') . " - ERROR DE CONEXIÓN: " . $e->getMessage() . "\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
throw new Exception("Error de conexión a la base de datos. Consulte los logs para más detalles.");
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
// Verificar si la conexión está activa
try {
$this->connection->query('SELECT 1');
} catch (PDOException $e) {
// Reconectar si la conexión se perdió
$this->connect();
}
return $this->connection;
}
// Prevenir clonación
private function __clone() {}
// Prevenir deserialización
public function __wakeup() {
throw new Exception("No se puede deserializar un singleton.");
}
}
/**
* Función helper para obtener la conexión
*/
function getDB() {
return Database::getInstance()->getConnection();
}

363
shared/database/schema.sql Executable file
View File

@@ -0,0 +1,363 @@
-- =====================================================
-- SCHEMA DE BASE DE DATOS COMPLETO
-- Sistema de Administración de Bots Discord/Telegram
-- =====================================================
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- =====================================================
-- TABLAS COMPARTIDAS
-- =====================================================
-- Tabla de usuarios del panel
CREATE TABLE IF NOT EXISTS `usuarios` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE,
`email` VARCHAR(100) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL,
`nombre_completo` VARCHAR(100),
`rol_id` INT UNSIGNED,
`idioma_id` INT UNSIGNED DEFAULT 1,
`activo` TINYINT(1) DEFAULT 1,
`fecha_creacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`ultimo_acceso` TIMESTAMP NULL,
PRIMARY KEY (`id`),
INDEX `idx_username` (`username`),
INDEX `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabla de roles
CREATE TABLE IF NOT EXISTS `roles` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`nombre` VARCHAR(50) NOT NULL UNIQUE,
`descripcion` VARCHAR(255),
`nivel` INT DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insertar roles por defecto
INSERT INTO `roles` (`nombre`, `descripcion`, `nivel`) VALUES
('Admin', 'Administrador con acceso completo', 100),
('Editor', 'Editor con permisos limitados', 50)
ON DUPLICATE KEY UPDATE `descripcion`=VALUES(`descripcion`);
-- Tabla de permisos
CREATE TABLE IF NOT EXISTS `permisos` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`nombre` VARCHAR(50) NOT NULL UNIQUE,
`descripcion` VARCHAR(255),
`modulo` ENUM('discord', 'telegram', 'global') DEFAULT 'global',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insertar permisos por defecto
INSERT INTO `permisos` (`nombre`, `descripcion`, `modulo`) VALUES
('crear_mensajes', 'Crear y enviar mensajes', 'global'),
('editar_plantillas', 'Editar plantillas de mensajes', 'global'),
('gestionar_usuarios', 'Gestionar destinatarios', 'global'),
('ver_logs', 'Ver logs del sistema', 'global'),
('gestionar_roles', 'Gestionar roles y permisos', 'global')
ON DUPLICATE KEY UPDATE `descripcion`=VALUES(`descripcion`);
-- Tabla relacional usuarios-permisos
CREATE TABLE IF NOT EXISTS `usuarios_permisos` (
`usuario_id` INT UNSIGNED NOT NULL,
`permiso_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`usuario_id`, `permiso_id`),
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`permiso_id`) REFERENCES `permisos`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabla de idiomas
CREATE TABLE IF NOT EXISTS `idiomas` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`codigo` VARCHAR(10) NOT NULL UNIQUE,
`nombre` VARCHAR(50) NOT NULL,
`nombre_nativo` VARCHAR(50),
`activo` TINYINT(1) DEFAULT 1,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insertar idiomas por defecto
INSERT INTO `idiomas` (`codigo`, `nombre`, `nombre_nativo`, `activo`) VALUES
('es', 'Español', 'Español', 1),
('en', 'Inglés', 'English', 1),
('fr', 'Francés', 'Français', 0),
('de', 'Alemán', 'Deutsch', 0),
('pt', 'Portugués', 'Português', 0)
ON DUPLICATE KEY UPDATE `nombre`=VALUES(`nombre`);
-- Tabla de galería (compartida)
CREATE TABLE IF NOT EXISTS `gallery` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`nombre` VARCHAR(255) NOT NULL,
`nombre_original` VARCHAR(255) NOT NULL,
`ruta` VARCHAR(500) NOT NULL,
`ruta_thumbnail` VARCHAR(500),
`hash_md5` VARCHAR(32),
`tipo_mime` VARCHAR(100),
`tamano` INT UNSIGNED,
`ancho` INT UNSIGNED,
`alto` INT UNSIGNED,
`usuario_id` INT UNSIGNED,
`fecha_subida` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_hash` (`hash_md5`),
INDEX `idx_usuario` (`usuario_id`),
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- TABLAS DE DISCORD
-- =====================================================
-- Destinatarios de Discord
CREATE TABLE IF NOT EXISTS `destinatarios_discord` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`discord_id` VARCHAR(50) NOT NULL UNIQUE,
`tipo` ENUM('usuario', 'canal', 'grupo', 'rol') DEFAULT 'usuario',
`nombre` VARCHAR(255),
`username` VARCHAR(100),
`discriminador` VARCHAR(10),
`avatar_url` VARCHAR(500),
`idioma_detectado` VARCHAR(10),
`chat_mode` VARCHAR(10) DEFAULT 'bot',
`activo` TINYINT(1) DEFAULT 1,
`fecha_registro` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`ultima_interaccion` TIMESTAMP NULL,
PRIMARY KEY (`id`),
INDEX `idx_discord_id` (`discord_id`),
INDEX `idx_tipo` (`tipo`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Plantillas de Discord
CREATE TABLE IF NOT EXISTS `plantillas_discord` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`nombre` VARCHAR(100) NOT NULL,
`contenido` TEXT NOT NULL,
`usuario_id` INT UNSIGNED,
`fecha_creacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`fecha_modificacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Mensajes de Discord
CREATE TABLE IF NOT EXISTS `mensajes_discord` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`contenido` TEXT NOT NULL,
`tipo_envio` ENUM('inmediato', 'programado', 'recurrente') DEFAULT 'inmediato',
`fecha_envio` TIMESTAMP NULL,
`canal_id` VARCHAR(50),
`mensaje_discord_id` VARCHAR(50),
`usuario_id` INT UNSIGNED,
`plantilla_id` INT UNSIGNED NULL,
`estado` ENUM('pendiente', 'enviado', 'fallido', 'deshabilitado') DEFAULT 'pendiente',
`error` TEXT NULL,
`fecha_creacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_tipo_envio` (`tipo_envio`),
INDEX `idx_estado` (`estado`),
INDEX `idx_fecha_envio` (`fecha_envio`),
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`plantilla_id`) REFERENCES `plantillas_discord`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Destinatarios de mensajes Discord (relación muchos a muchos)
CREATE TABLE IF NOT EXISTS `mensajes_discord_destinatarios` (
`mensaje_id` INT UNSIGNED NOT NULL,
`destinatario_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`mensaje_id`, `destinatario_id`),
FOREIGN KEY (`mensaje_id`) REFERENCES `mensajes_discord`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`destinatario_id`) REFERENCES `destinatarios_discord`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Mensajes recurrentes de Discord
CREATE TABLE IF NOT EXISTS `recurrentes_discord` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`mensaje_id` INT UNSIGNED NOT NULL,
`frecuencia` ENUM('diario', 'semanal', 'mensual') DEFAULT 'diario',
`hora_envio` TIME DEFAULT '09:00:00',
`dia_semana` TINYINT NULL COMMENT '1=Lunes, 7=Domingo',
`dia_mes` TINYINT NULL COMMENT '1-31',
`activo` TINYINT(1) DEFAULT 1,
`ultimo_envio` TIMESTAMP NULL,
`proximo_envio` TIMESTAMP NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`mensaje_id`) REFERENCES `mensajes_discord`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Logs de Discord
CREATE TABLE IF NOT EXISTS `logs_discord` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`tipo` ENUM('mensaje', 'comando', 'error', 'interaccion', 'usuario') DEFAULT 'mensaje',
`nivel` ENUM('info', 'warning', 'error') DEFAULT 'info',
`descripcion` TEXT,
`datos_json` JSON NULL,
`usuario_id` INT UNSIGNED NULL,
`destinatario_id` INT UNSIGNED NULL,
`fecha` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_tipo` (`tipo`),
INDEX `idx_nivel` (`nivel`),
INDEX `idx_fecha` (`fecha`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Comandos de Discord
CREATE TABLE IF NOT EXISTS `comandos_discord` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`comando` VARCHAR(50) NOT NULL,
`descripcion` VARCHAR(255),
`destinatario_id` INT UNSIGNED,
`plantilla_id` INT UNSIGNED NULL,
`veces_usado` INT UNSIGNED DEFAULT 0,
`fecha_creacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`ultimo_uso` TIMESTAMP NULL,
PRIMARY KEY (`id`),
INDEX `idx_comando` (`comando`),
FOREIGN KEY (`destinatario_id`) REFERENCES `destinatarios_discord`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`plantilla_id`) REFERENCES `plantillas_discord`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Configuración de bienvenida Discord
CREATE TABLE IF NOT EXISTS `bienvenida_discord` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`canal_id` VARCHAR(50) NOT NULL,
`texto` TEXT,
`imagen_id` INT UNSIGNED NULL,
`idiomas_habilitados` JSON NULL,
`registrar_usuario` TINYINT(1) DEFAULT 1,
`activo` TINYINT(1) DEFAULT 1,
PRIMARY KEY (`id`),
FOREIGN KEY (`imagen_id`) REFERENCES `gallery`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =====================================================
-- TABLAS DE TELEGRAM
-- =====================================================
-- Destinatarios de Telegram
CREATE TABLE IF NOT EXISTS `destinatarios_telegram` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`telegram_id` VARCHAR(50) NOT NULL UNIQUE,
`tipo` ENUM('usuario', 'canal', 'grupo') DEFAULT 'usuario',
`nombre` VARCHAR(255),
`username` VARCHAR(100),
`avatar_url` VARCHAR(500),
`idioma_detectado` VARCHAR(10),
`activo` TINYINT(1) DEFAULT 1,
`fecha_registro` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`ultima_interaccion` TIMESTAMP NULL,
PRIMARY KEY (`id`),
INDEX `idx_telegram_id` (`telegram_id`),
INDEX `idx_tipo` (`tipo`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Plantillas de Telegram
CREATE TABLE IF NOT EXISTS `plantillas_telegram` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`nombre` VARCHAR(100) NOT NULL,
`comando` VARCHAR(50) UNIQUE,
`contenido` TEXT NOT NULL,
`usuario_id` INT UNSIGNED,
`fecha_creacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`fecha_modificacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_comando` (`comando`),
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Mensajes de Telegram
CREATE TABLE IF NOT EXISTS `mensajes_telegram` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`contenido` TEXT NOT NULL,
`tipo_envio` ENUM('inmediato', 'programado', 'recurrente') DEFAULT 'inmediato',
`fecha_envio` TIMESTAMP NULL,
`chat_id` VARCHAR(50),
`mensaje_telegram_id` VARCHAR(50),
`usuario_id` INT UNSIGNED,
`plantilla_id` INT UNSIGNED NULL,
`estado` ENUM('pendiente', 'enviado', 'fallido', 'deshabilitado') DEFAULT 'pendiente',
`error` TEXT NULL,
`fecha_creacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_tipo_envio` (`tipo_envio`),
INDEX `idx_estado` (`estado`),
INDEX `idx_fecha_envio` (`fecha_envio`),
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`plantilla_id`) REFERENCES `plantillas_telegram`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Destinatarios de mensajes Telegram (relación muchos a muchos)
CREATE TABLE IF NOT EXISTS `mensajes_telegram_destinatarios` (
`mensaje_id` INT UNSIGNED NOT NULL,
`destinatario_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`mensaje_id`, `destinatario_id`),
FOREIGN KEY (`mensaje_id`) REFERENCES `mensajes_telegram`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`destinatario_id`) REFERENCES `destinatarios_telegram`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Mensajes recurrentes de Telegram
CREATE TABLE IF NOT EXISTS `recurrentes_telegram` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`mensaje_id` INT UNSIGNED NOT NULL,
`frecuencia` ENUM('diario', 'semanal', 'mensual') DEFAULT 'diario',
`hora_envio` TIME DEFAULT '09:00:00',
`dia_semana` TINYINT NULL,
`dia_mes` TINYINT NULL,
`activo` TINYINT(1) DEFAULT 1,
`ultimo_envio` TIMESTAMP NULL,
`proximo_envio` TIMESTAMP NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`mensaje_id`) REFERENCES `mensajes_telegram`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Logs de Telegram
CREATE TABLE IF NOT EXISTS `logs_telegram` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`tipo` ENUM('mensaje', 'comando', 'error', 'interaccion', 'usuario') DEFAULT 'mensaje',
`nivel` ENUM('info', 'warning', 'error') DEFAULT 'info',
`descripcion` TEXT,
`datos_json` JSON NULL,
`usuario_id` INT UNSIGNED NULL,
`destinatario_id` INT UNSIGNED NULL,
`fecha` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_tipo` (`tipo`),
INDEX `idx_nivel` (`nivel`),
INDEX `idx_fecha` (`fecha`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Comandos de Telegram
CREATE TABLE IF NOT EXISTS `comandos_telegram` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`comando` VARCHAR(50) NOT NULL,
`descripcion` VARCHAR(255),
`destinatario_id` INT UNSIGNED,
`plantilla_id` INT UNSIGNED NULL,
`veces_usado` INT UNSIGNED DEFAULT 0,
`fecha_creacion` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`ultimo_uso` TIMESTAMP NULL,
PRIMARY KEY (`id`),
INDEX `idx_comando` (`comando`),
FOREIGN KEY (`destinatario_id`) REFERENCES `destinatarios_telegram`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`plantilla_id`) REFERENCES `plantillas_telegram`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Configuración de bienvenida Telegram
CREATE TABLE IF NOT EXISTS `bienvenida_telegram` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`chat_id` VARCHAR(50) NOT NULL,
`texto` TEXT,
`imagen_id` INT UNSIGNED NULL,
`idiomas_habilitados` JSON NULL,
`registrar_usuario` TINYINT(1) DEFAULT 1,
`activo` TINYINT(1) DEFAULT 1,
`boton_unirse_texto` VARCHAR(100) DEFAULT 'Únete al grupo',
`boton_unirse_url` VARCHAR(500),
PRIMARY KEY (`id`),
FOREIGN KEY (`imagen_id`) REFERENCES `gallery`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SET FOREIGN_KEY_CHECKS = 1;

41
shared/languages/get_active.php Executable file
View File

@@ -0,0 +1,41 @@
<?php
header('Content-Type: application/json');
// Habilitar logging para depuración
ini_set('display_errors', 0);
error_reporting(E_ALL);
require_once __DIR__ . '/../../shared/utils/helpers.php';
require_once __DIR__ . '/../../shared/auth/jwt.php';
require_once __DIR__ . '/../../shared/database/connection.php';
// Verificar autenticación (opcional, dependiendo de tus requisitos de seguridad)
try {
$userData = JWTAuth::authenticate();
if (!$userData) {
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
}
} catch (Exception $e) {
// Si la autenticación falla, igualmente devolvemos los idiomas (o puedes cambiar esto según tus necesidades)
}
try {
$db = getDB();
// Obtener solo los idiomas activos
$stmt = $db->query("SELECT id, nombre, codigo, bandera FROM idiomas WHERE activo = 1 ORDER BY nombre ASC");
$languages = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonResponse([
'success' => true,
'languages' => $languages
]);
} catch (Exception $e) {
error_log("Error al obtener idiomas activos: " . $e->getMessage());
jsonResponse([
'success' => false,
'error' => 'Error al cargar los idiomas',
'debug' => DEBUG_MODE ? $e->getMessage() : null
], 500);
}

604
shared/languages/manager.php Executable file
View File

@@ -0,0 +1,604 @@
<?php
session_start();
// Habilitar logging para depuración
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../shared/utils/helpers.php';
require_once __DIR__ . '/../../shared/auth/jwt.php';
require_once __DIR__ . '/../../shared/database/connection.php';
require_once __DIR__ . '/../../shared/translations/manager.php'; // Incluir helper de traducciones
$userData = JWTAuth::requireAuth();
// Verificar permiso para ver la página de gestión de idiomas
if (!hasPermission('view_languages')) {
die(__('you_do_not_have_permission_to_manage_languages'));
}
$db = getDB();
// Manejar acciones POST (Activar/Desactivar/Sincronizar/Actualizar Bandera)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Verificar permiso para realizar acciones de gestión de idiomas
if (!hasPermission('manage_languages')) {
jsonResponse(['success' => false, 'error' => __('you_do_not_have_permission_to_manage_languages')], 403);
}
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['action'])) {
try {
if ($input['action'] === 'toggle' && isset($input['id'])) {
$stmt = $db->prepare("UPDATE idiomas SET activo = NOT activo WHERE id = ?");
$stmt->execute([$input['id']]);
jsonResponse(['success' => true]);
}
elseif ($input['action'] === 'update_flag' && isset($input['id'])) {
$flag = $input['flag'] ?? null;
$stmt = $db->prepare("UPDATE idiomas SET bandera = ? WHERE id = ?");
$stmt->execute([$flag, $input['id']]);
jsonResponse(['success' => true]);
}
elseif ($input['action'] === 'sync') {
// 1. Obtener idiomas de LibreTranslate
$ltUrl = $_ENV['LIBRETRANSLATE_URL'] ?? getenv('LIBRETRANSLATE_URL') ?? 'http://10.10.4.17:5000';
$ch = curl_init("$ltUrl/languages");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) {
throw new Exception("No se pudo conectar con LibreTranslate ($ltUrl)");
}
$languages = json_decode($response, true);
if (!is_array($languages)) {
throw new Exception("Respuesta inválida de LibreTranslate");
}
// 2. Actualizar base de datos
$stmt = $db->prepare("
INSERT INTO idiomas (codigo, nombre, nombre_nativo, activo)
VALUES (?, ?, ?, 1)
ON DUPLICATE KEY UPDATE nombre = VALUES(nombre), nombre_nativo = VALUES(nombre_nativo)
");
$count = 0;
foreach ($languages as $lang) {
// LibreTranslate devuelve: code, name, targets
// No siempre devuelve nombre nativo, usaremos el nombre en inglés por defecto
$stmt->execute([
$lang['code'],
$lang['name'],
$lang['name'], // Usamos el mismo nombre ya que LT no siempre da el nativo en /languages
]);
$count++;
}
jsonResponse(['success' => true, 'message' => str_replace('{count}', $count, __('synced_languages'))]);
}
} catch (Exception $e) {
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
}
}
exit;
}
// Obtener idiomas
$stmt = $db->query("SELECT * FROM idiomas ORDER BY nombre ASC");
$idiomas = $stmt->fetchAll();
// URL de LibreTranslate desde .env
$libreTranslateUrl = $_ENV['LIBRETRANSLATE_URL'] ?? getenv('LIBRETRANSLATE_URL') ?? 'http://10.10.4.17:5000';
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo __('language_manager_title'); ?></title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="/shared/public/js/notifications.js"></script>
<style>
:root {
--primary-color: #5865F2;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f0f2f5;
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: #333;
font-size: 24px;
}
.container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
.card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.card-header {
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
font-weight: 600;
font-size: 18px;
color: #444;
display: flex;
justify-content: space-between;
align-items: center;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
font-weight: 600;
color: #555;
background: #f8f9fa;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--success-color);
}
input:checked + .slider:before {
transform: translateX(26px);
}
.btn {
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-secondary {
background: var(--secondary-color);
color: white;
}
.btn-success {
background: var(--success-color);
color: white;
}
.status-badge {
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: 600;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-inactive {
background: #f8d7da;
color: #721c24;
}
.test-area textarea {
width: 100%;
padding: 10px;
border: 2px solid #eee;
border-radius: 8px;
margin-bottom: 10px;
min-height: 100px;
resize: vertical;
}
.test-result {
margin-top: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid var(--primary-color);
display: none;
}
.btn-flag {
background: white;
border: 1px solid #ddd;
border-radius: 50%;
width: 36px;
height: 36px;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.btn-flag:hover {
background: #f0f0f0;
transform: scale(1.1);
}
/* Modal Banderas */
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
.modal-content { background: white; margin: 10% auto; padding: 25px; width: 90%; max-width: 600px; border-radius: 15px; position: relative; }
.close-modal { position: absolute; top: 15px; right: 20px; font-size: 24px; cursor: pointer; color: #aaa; }
.flag-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; padding: 5px; }
.flag-option { font-size: 24px; cursor: pointer; padding: 5px; border-radius: 5px; text-align: center; }
.flag-option:hover { background: #f0f0f0; }
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-language"></i> <?php echo __('language_manager_header'); ?></h1>
<a href="/index.php" class="btn btn-secondary">
<i class="fas fa-home"></i> <?php echo __('back_to_dashboard'); ?>
</a>
</div>
<div class="container">
<div class="card">
<div class="card-header">
<span><?php echo __('available_languages'); ?></span>
<button onclick="syncLanguages()" class="btn btn-success" style="font-size: 12px; padding: 5px 10px;">
<i class="fas fa-sync"></i> <?php echo __('sync_with_libretranslate'); ?>
</button>
</div>
<table>
<thead>
<tr>
<th><?php echo __('flag'); ?></th>
<th><?php echo __('code'); ?></th>
<th><?php echo __('name'); ?></th>
<th><?php echo __('native'); ?></th>
<th><?php echo __('status'); ?></th>
<th><?php echo __('action'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($idiomas as $lang): ?>
<tr>
<td>
<button class="btn-flag" onclick="openFlagModal(<?php echo $lang['id']; ?>, '<?php echo $lang['bandera'] ?? ''; ?>')">
<?php echo $lang['bandera'] ? $lang['bandera'] : '<i class="fas fa-plus" style="font-size: 10px; color: #ccc;"></i>'; ?>
</button>
</td>
<td><code><?php echo htmlspecialchars($lang['codigo']); ?></code></td>
<td><?php echo htmlspecialchars($lang['nombre']); ?></td>
<td><?php echo htmlspecialchars($lang['nombre_nativo']); ?></td>
<td>
<span class="status-badge <?php echo $lang['activo'] ? 'status-active' : 'status-inactive'; ?>">
<?php echo $lang['activo'] ? __('active') : __('inactive'); ?>
</span>
</td>
<td>
<label class="switch">
<input type="checkbox"
onchange="toggleLanguage(<?php echo $lang['id']; ?>)"
<?php echo $lang['activo'] ? 'checked' : ''; ?>>
<span class="slider"></span>
</label>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="card">
<div class="card-header"><?php echo __('translation_test'); ?></div>
<p style="margin-bottom: 15px; color: #666; font-size: 14px;">
<?php echo __('test_connection_with_libretranslate'); ?> (<?php echo htmlspecialchars($libreTranslateUrl); ?>).
</p>
<div class="test-area">
<textarea id="sourceText" placeholder="<?php echo __('type_something_to_translate'); ?>"></textarea>
<div style="margin-bottom: 15px;">
<label style="display:block; margin-bottom:5px; font-weight:600;"><?php echo __('translate_to'); ?></label>
<select id="targetLang" style="width:100%; padding:8px; border-radius:5px; border:1px solid #ddd;">
<?php foreach ($idiomas as $lang): ?>
<?php if ($lang['activo']): ?>
<option value="<?php echo $lang['codigo']; ?>">
<?php echo htmlspecialchars($lang['nombre']); ?>
</option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<button onclick="testTranslation()" class="btn btn-primary" style="width: 100%;">
<i class="fas fa-magic"></i> <?php echo __('translate'); ?>
</button>
<div id="translationResult" class="test-result"></div>
</div>
</div>
</div>
<!-- Modal Selección de Bandera -->
<div id="flagModal" class="modal">
<div class="modal-content">
<span class="close-modal" onclick="closeFlagModal()">&times;</span>
<h2 style="margin-bottom: 15px;"><?php echo __('select_flag'); ?></h2>
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<input type="text" id="customFlag" placeholder="<?php echo __('paste_emoji_here'); ?>" class="form-control" style="flex: 1; padding: 10px; border: 2px solid #eee; border-radius: 8px; font-size: 18px;">
<button onclick="saveCustomFlag()" class="btn btn-primary"><?php echo __('save'); ?></button>
</div>
<p style="color: #666; font-size: 14px;"><?php echo __('or_select_common_one'); ?></p>
<div class="flag-grid">
<!-- Banderas comunes -->
<div class="flag-option" onclick="selectFlag('🇺🇸')">🇺🇸</div>
<div class="flag-option" onclick="selectFlag('🇬🇧')">🇬🇧</div>
<div class="flag-option" onclick="selectFlag('🇪🇸')">🇪🇸</div>
<div class="flag-option" onclick="selectFlag('🇲🇽')">🇲🇽</div>
<div class="flag-option" onclick="selectFlag('🇫🇷')">🇫🇷</div>
<div class="flag-option" onclick="selectFlag('🇩🇪')">🇩🇪</div>
<div class="flag-option" onclick="selectFlag('🇮🇹')">🇮🇹</div>
<div class="flag-option" onclick="selectFlag('🇵🇹')">🇵🇹</div>
<div class="flag-option" onclick="selectFlag('🇧🇷')">🇧🇷</div>
<div class="flag-option" onclick="selectFlag('🇷🇺')">🇷🇺</div>
<div class="flag-option" onclick="selectFlag('🇨🇳')">🇨🇳</div>
<div class="flag-option" onclick="selectFlag('🇯🇵')">🇯🇵</div>
<div class="flag-option" onclick="selectFlag('🇰🇷')">🇰🇷</div>
<div class="flag-option" onclick="selectFlag('🇮🇳')">🇮🇳</div>
<div class="flag-option" onclick="selectFlag('🇸🇦')">🇸🇦</div>
<div class="flag-option" onclick="selectFlag('🇹🇷')">🇹🇷</div>
<div class="flag-option" onclick="selectFlag('🇳🇱')">🇳🇱</div>
<div class="flag-option" onclick="selectFlag('🇵🇱')">🇵🇱</div>
<div class="flag-option" onclick="selectFlag('🇸🇪')">🇸🇪</div>
<div class="flag-option" onclick="selectFlag('🇺🇦')">🇺🇦</div>
<div class="flag-option" onclick="selectFlag('🇨🇦')">🇨🇦</div>
<div class="flag-option" onclick="selectFlag('🇦🇺')">🇦🇺</div>
<div class="flag-option" onclick="selectFlag('🇦🇷')">🇦🇷</div>
<div class="flag-option" onclick="selectFlag('🇨🇴')">🇨🇴</div>
<div class="flag-option" onclick="selectFlag('🇨🇱')">🇨🇱</div>
<div class="flag-option" onclick="selectFlag('🇵🇪')">🇵🇪</div>
<div class="flag-option" onclick="selectFlag('🇻🇪')">🇻🇪</div>
<div class="flag-option" onclick="selectFlag('🌍')">🌍</div>
</div>
</div>
</div>
<script>
let currentLangId = null;
function openFlagModal(id, currentFlag) {
currentLangId = id;
document.getElementById('customFlag').value = currentFlag;
document.getElementById('flagModal').style.display = 'block';
}
function closeFlagModal() {
document.getElementById('flagModal').style.display = 'none';
currentLangId = null;
}
function selectFlag(flag) {
document.getElementById('customFlag').value = flag;
saveCustomFlag();
}
async function saveCustomFlag() {
const flag = document.getElementById('customFlag').value;
if (!currentLangId) return;
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'update_flag',
id: currentLangId,
flag: flag
})
});
const result = await response.json();
if (result.success) {
location.reload();
} else {
showNotification("<?php echo __('error'); ?>" + result.error, 'error');
}
} catch (error) {
console.error(error);
showNotification("<?php echo __('connection_error'); ?>", 'error');
}
}
window.onclick = function(event) {
if (event.target == document.getElementById('flagModal')) {
closeFlagModal();
}
}
async function syncLanguages() {
if (!confirm("<?php echo __('confirm_sync'); ?>")) return;
const btn = document.querySelector('.btn-success');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?php echo __('syncing'); ?>';
btn.disabled = true;
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'sync'
})
});
const result = await response.json();
if (result.success) {
showNotification(result.message, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showNotification("<?php echo __('sync_error'); ?>" + result.error, 'error');
}
} catch (error) {
console.error(error);
showNotification("<?php echo __('connection_error'); ?>", 'error');
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
}
async function toggleLanguage(id) {
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'toggle',
id: id
})
});
const result = await response.json();
if (result.success) {
// Recargar para actualizar UI visualmente (badges)
location.reload();
} else {
showNotification("<?php echo __('language_update_error'); ?>: " + result.error, 'error');
}
} catch (error) {
console.error(error);
showNotification("<?php echo __('connection_error'); ?>", 'error');
}
}
async function testTranslation() {
const text = document.getElementById('sourceText').value;
const target = document.getElementById('targetLang').value;
const resultDiv = document.getElementById('translationResult');
if (!text) return;
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?php echo __('translating'); ?>';
try {
// Usar el endpoint de LibreTranslate directamente o a través de un proxy PHP si hay CORS
// Por ahora intentamos directo, si falla implementaremos proxy
const response = await fetch('<?php echo $libreTranslateUrl; ?>/translate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
q: text,
source: 'auto',
target: target,
format: 'text'
})
});
const data = await response.json();
if (data.translatedText) {
resultDiv.innerHTML = '<strong><?php echo __('result'); ?></strong><br>' + data.translatedText;
} else {
resultDiv.innerHTML = `<span style="color:red"><?php echo __('error'); ?>${data.error || 'Desconocido'}</span>`;
}
} catch (error) {
console.error(error);
resultDiv.innerHTML = `<span style="color:red"><?php echo __('libretranslate_connection_error'); ?></span>`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,72 @@
// shared/public/js/notifications.js
/**
* Muestra una notificación tipo "toast".
* @param {string} message El mensaje a mostrar.
* @param {string} type El tipo de notificación ('success', 'error', 'info').
* @param {number} duration La duración en milisegundos.
*/
function showNotification(message, type = 'info', duration = 3000) {
// Crear el contenedor de notificaciones si no existe
let container = document.getElementById('notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-container';
document.body.appendChild(container);
// Añadir estilos al head
const style = document.createElement('style');
style.innerHTML = `
#notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast-notification {
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
opacity: 0;
transform: translateX(100%);
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.toast-notification.show {
opacity: 1;
transform: translateX(0);
}
.toast-notification.success { background-color: #28a745; }
.toast-notification.error { background-color: #dc3545; }
.toast-notification.info { background-color: #17a2b8; }
`;
document.head.appendChild(style);
}
const notification = document.createElement('div');
notification.className = `toast-notification ${type}`;
notification.textContent = message;
container.appendChild(notification);
// Trigger the animation
setTimeout(() => {
notification.classList.add('show');
}, 10);
// Hide and remove the notification after the duration
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
if (container.children.length === 0) {
container.remove();
}
}, 500); // Wait for fade out animation
}, duration);
}

75
shared/translations/en.json Executable file
View File

@@ -0,0 +1,75 @@
{
"main_panel_title": "Main Dashboard - Bot System",
"bot_admin_system_title": "Bot Administration System",
"languages": "Languages",
"logout": "Logout",
"discord": "Discord",
"discord_description": "Manage your Discord bot, send messages, manage templates, and more",
"users": "Users",
"messages": "Messages",
"templates": "Templates",
"telegram": "Telegram",
"telegram_description": "Manage your Telegram bot, send messages, manage templates, and more",
"discord_dashboard_title": "Discord Dashboard - Bot System",
"discord_dashboard_header": "Discord Dashboard",
"back_to_main_dashboard": "Back to Main Dashboard",
"templates_module_title": "Templates",
"templates_module_description": "Manage message templates",
"create_message_module_title": "Create Message",
"create_message_module_description": "Send messages to Discord",
"sent_messages_module_title": "Sent Messages",
"sent_messages_module_description": "Message history",
"recipients_module_title": "Recipients",
"recipients_module_description": "Manage users and channels",
"commands_module_title": "Commands",
"commands_module_description": "View executed commands",
"welcome_message_module_title": "Welcome Message",
"welcome_message_module_description": "Configure welcome message",
"system_logs_module_title": "System Logs",
"system_logs_module_description": "History of errors and events",
"gallery_module_title": "Gallery",
"gallery_module_description": "Manage images",
"languages_module_title": "Languages",
"languages_module_description": "Manage translations",
"connection_test_module_title": "Connection Test",
"connection_test_module_description": "Test connection with Discord",
"telegram_dashboard_title": "Telegram Dashboard - Bot System",
"telegram_dashboard_header": "Telegram Dashboard",
"recipients_module_description_telegram": "Manage users and chats",
"connection_test_module_description_telegram": "Test connection with Telegram",
"language_manager_title": "Language Management - Bot System",
"language_manager_header": "Language Management",
"back_to_dashboard": "Back to Dashboard",
"available_languages": "Available Languages",
"sync_with_libretranslate": "Sync with LibreTranslate",
"flag": "Flag",
"code": "Code",
"name": "Name",
"native": "Native",
"status": "Status",
"action": "Action",
"active": "Active",
"inactive": "Inactive",
"translation_test": "Translation Test",
"test_connection_with_libretranslate": "Test the connection with LibreTranslate",
"translate_to": "Translate to:",
"type_something_to_translate": "Type something to translate...",
"translate": "Translate",
"result": "Result:",
"error": "Error: ",
"libretranslate_connection_error": "Connection error with LibreTranslate. Verify that the service is running.",
"select_flag": "Select Flag",
"paste_emoji_here": "Paste an emoji here...",
"save": "Save",
"or_select_common_one": "Or select a common one:",
"confirm_sync": "Sync languages with LibreTranslate? This will add new available languages.",
"syncing": "Syncing...",
"synced_languages": "Synced {count} languages",
"sync_error": "Error while syncing: ",
"connection_error": "Connection error",
"confirm_toggle_language": "Activate/deactivate this language?",
"language_updated": "Language updated",
"language_update_error": "Error updating language",
"translating": "Translating...",
"json_parse_error": "Error parsing JSON"
}

75
shared/translations/es.json Executable file
View File

@@ -0,0 +1,75 @@
{
"main_panel_title": "Panel Principal - Sistema de Bots",
"bot_admin_system_title": "Sistema de Administración de Bots",
"languages": "Idiomas",
"logout": "Cerrar Sesión",
"discord": "Discord",
"discord_description": "Administra tu bot de Discord, envía mensajes, gestiona plantillas y más",
"users": "Usuarios",
"messages": "Mensajes",
"templates": "Plantillas",
"telegram": "Telegram",
"telegram_description": "Administra tu bot de Telegram, envía mensajes, gestiona plantillas y más",
"discord_dashboard_title": "Dashboard Discord - Sistema de Bots",
"discord_dashboard_header": "Dashboard Discord",
"back_to_main_dashboard": "Volver al Panel Principal",
"templates_module_title": "Plantillas",
"templates_module_description": "Gestionar plantillas de mensajes",
"create_message_module_title": "Crear Mensaje",
"create_message_module_description": "Enviar mensajes a Discord",
"sent_messages_module_title": "Mensajes Enviados",
"sent_messages_module_description": "Historial de mensajes",
"recipients_module_title": "Destinatarios",
"recipients_module_description": "Gestionar usuarios y canales",
"commands_module_title": "Comandos",
"commands_module_description": "Ver comandos ejecutados",
"welcome_message_module_title": "Mensaje de Bienvenida",
"welcome_message_module_description": "Configurar mensaje de bienvenida",
"system_logs_module_title": "Logs del Sistema",
"system_logs_module_description": "Historial de errores y eventos",
"gallery_module_title": "Galería",
"gallery_module_description": "Gestionar imágenes",
"languages_module_title": "Idiomas",
"languages_module_description": "Gestionar traducciones",
"connection_test_module_title": "Test de Conexión",
"connection_test_module_description": "Probar conexión con Discord",
"telegram_dashboard_title": "Dashboard Telegram - Sistema de Bots",
"telegram_dashboard_header": "Dashboard Telegram",
"recipients_module_description_telegram": "Gestionar usuarios y chats",
"connection_test_module_description_telegram": "Probar conexión con Telegram",
"language_manager_title": "Gestión de Idiomas - Sistema de Bots",
"language_manager_header": "Gestión de Idiomas",
"back_to_dashboard": "Volver al Panel",
"available_languages": "Idiomas Disponibles",
"sync_with_libretranslate": "Sincronizar con LibreTranslate",
"flag": "Bandera",
"code": "Código",
"name": "Nombre",
"native": "Nativo",
"status": "Estado",
"action": "Acción",
"active": "Activo",
"inactive": "Inactivo",
"translation_test": "Prueba de Traducción",
"test_connection_with_libretranslate": "Prueba la conexión con LibreTranslate",
"translate_to": "Traducir a:",
"type_something_to_translate": "Escribe algo para traducir...",
"translate": "Traducir",
"result": "Resultado:",
"error": "Error: ",
"libretranslate_connection_error": "Error de conexión con LibreTranslate. Verifica que el servicio esté corriendo.",
"select_flag": "Seleccionar Bandera",
"paste_emoji_here": "Pega un emoji aquí...",
"save": "Guardar",
"or_select_common_one": "O selecciona una común:",
"confirm_sync": "¿Sincronizar idiomas con LibreTranslate? Esto agregará nuevos idiomas disponibles.",
"syncing": "Sincronizando...",
"synced_languages": "Sincronizados {count} idiomas",
"sync_error": "Error al sincronizar: ",
"connection_error": "Error de conexión",
"confirm_toggle_language": "¿Activar/desactivar este idioma?",
"language_updated": "Idioma actualizado",
"language_update_error": "Error al actualizar idioma",
"translating": "Traduciendo...",
"json_parse_error": "Error al parsear JSON"
}

58
shared/translations/manager.php Executable file
View File

@@ -0,0 +1,58 @@
<?php
/**
* Translation Manager
*/
class TranslationManager {
private static $instance = null;
private $translations = [];
private $language = 'es'; // Idioma por defecto
private function __construct() {
// Obtener el idioma del usuario desde el JWT
if (class_exists('JWTAuth')) {
$userData = JWTAuth::getUserData();
if (isset($userData->idioma)) {
$this->language = $userData->idioma;
}
}
$this->loadTranslations();
}
public static function getInstance() {
if (self::$instance == null) {
self::$instance = new TranslationManager();
}
return self::$instance;
}
private function loadTranslations() {
$filePath = __DIR__ . '/' . $this->language . '.json';
if (file_exists($filePath)) {
$json = file_get_contents($filePath);
$this->translations = json_decode($json, true);
} else {
// Si el archivo de idioma no existe, cargar español por defecto
$defaultFilePath = __DIR__ . '/es.json';
if (file_exists($defaultFilePath)) {
$json = file_get_contents($defaultFilePath);
$this->translations = json_decode($json, true);
}
}
}
public function get($key, $default = null) {
return $this->translations[$key] ?? $default ?? $key;
}
}
/**
* Helper global para traducciones
* @param string $key La clave de traducción.
* @param string|null $default Un valor por defecto si la clave no se encuentra.
* @return string
*/
function __($key, $default = null) {
return TranslationManager::getInstance()->get($key, $default);
}

75
shared/translations/pt.json Executable file
View File

@@ -0,0 +1,75 @@
{
"main_panel_title": "Painel Principal - Sistema de Bots",
"bot_admin_system_title": "Sistema de Administração de Bots",
"languages": "Idiomas",
"logout": "Sair",
"discord": "Discord",
"discord_description": "Gerencie seu bot do Discord, envie mensagens, gerencie templates e mais",
"users": "Usuários",
"messages": "Mensagens",
"templates": "Templates",
"telegram": "Telegram",
"telegram_description": "Gerencie seu bot do Telegram, envie mensagens, gerencie templates e mais",
"discord_dashboard_title": "Dashboard do Discord - Sistema de Bots",
"discord_dashboard_header": "Dashboard do Discord",
"back_to_main_dashboard": "Voltar ao Painel Principal",
"templates_module_title": "Templates",
"templates_module_description": "Gerenciar templates de mensagens",
"create_message_module_title": "Criar Mensagem",
"create_message_module_description": "Enviar mensagens para o Discord",
"sent_messages_module_title": "Mensagens Enviadas",
"sent_messages_module_description": "Histórico de mensagens",
"recipients_module_title": "Destinatários",
"recipients_module_description": "Gerenciar usuários e canais",
"commands_module_title": "Comandos",
"commands_module_description": "Ver comandos executados",
"welcome_message_module_title": "Mensagem de Boas-Vindas",
"welcome_message_module_description": "Configurar mensagem de boas-vindas",
"system_logs_module_title": "Logs do Sistema",
"system_logs_module_description": "Histórico de erros e eventos",
"gallery_module_title": "Galeria",
"gallery_module_description": "Gerenciar imagens",
"languages_module_title": "Idiomas",
"languages_module_description": "Gerenciar traduções",
"connection_test_module_title": "Teste de Conexão",
"connection_test_module_description": "Testar conexão com o Discord",
"telegram_dashboard_title": "Dashboard do Telegram - Sistema de Bots",
"telegram_dashboard_header": "Dashboard do Telegram",
"recipients_module_description_telegram": "Gerenciar usuários e chats",
"connection_test_module_description_telegram": "Testar conexão com o Telegram",
"language_manager_title": "Gerenciamento de Idiomas - Sistema de Bots",
"language_manager_header": "Gerenciamento de Idiomas",
"back_to_dashboard": "Voltar ao Painel",
"available_languages": "Idiomas Disponíveis",
"sync_with_libretranslate": "Sincronizar com LibreTranslate",
"flag": "Bandeira",
"code": "Código",
"name": "Nome",
"native": "Nativo",
"status": "Status",
"action": "Ação",
"active": "Ativo",
"inactive": "Inativo",
"translation_test": "Teste de Tradução",
"test_connection_with_libretranslate": "Teste a conexão com o LibreTranslate",
"translate_to": "Traduzir para:",
"type_something_to_translate": "Digite algo para traduzir...",
"translate": "Traduzir",
"result": "Resultado:",
"error": "Erro: ",
"libretranslate_connection_error": "Erro de conexão com o LibreTranslate. Verifique se o serviço está em execução.",
"select_flag": "Selecionar Bandeira",
"paste_emoji_here": "Cole um emoji aqui...",
"save": "Salvar",
"or_select_common_one": "Ou selecione um comum:",
"confirm_sync": "Sincronizar idiomas com o LibreTranslate? Isso adicionará novos idiomas disponíveis.",
"syncing": "Sincronizando...",
"synced_languages": "Sincronizados {count} idiomas",
"sync_error": "Erro ao sincronizar: ",
"connection_error": "Erro de conexão",
"confirm_toggle_language": "Ativar/desativar este idioma?",
"language_updated": "Idioma atualizado",
"language_update_error": "Erro ao atualizar o idioma",
"translating": "Traduzindo...",
"json_parse_error": "Erro ao analisar JSON"
}

104
shared/translations/translate.php Executable file
View File

@@ -0,0 +1,104 @@
<?php
header('Content-Type: application/json');
// Habilitar logging para depuración
ini_set('display_errors', 0);
error_reporting(E_ALL);
define('DEBUG_MODE', true); // Cambiar a false en producción
require_once __DIR__ . '/../../shared/utils/helpers.php';
require_once __DIR__ . '/../../shared/auth/jwt.php';
// Verificar autenticación
try {
$userData = JWTAuth::authenticate();
if (!$userData) {
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
}
} catch (Exception $e) {
jsonResponse(['success' => false, 'error' => 'Error de autenticación: ' . $e->getMessage()], 401);
}
// Verificar método
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
}
// Obtener datos de la petición
$input = json_decode(file_get_contents('php://input'), true);
$text = $input['text'] ?? '';
$targetLang = $input['target'] ?? '';
$sourceLang = $input['source'] ?? 'es';
// Validar parámetros
if (empty($text) || empty($targetLang)) {
jsonResponse(['success' => false, 'error' => 'Texto o idioma de destino no especificado'], 400);
}
try {
// URL de LibreTranslate (ajusta según tu configuración)
$ltUrl = getenv('LIBRETRANSLATE_URL') ?: 'http://10.10.4.17:5000';
// Configurar la petición a LibreTranslate
$ch = curl_init("$ltUrl/translate");
$postData = [
'q' => $text,
'source' => $sourceLang,
'target' => $targetLang,
'format' => 'html', // Para mantener formato HTML si existe
'api_key' => getenv('LIBRETRANSLATE_API_KEY') ?: ''
];
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($postData),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json'
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new Exception("Error en la petición: $error");
}
$result = json_decode($response, true);
if ($httpCode !== 200 || !isset($result['translatedText'])) {
$errorMsg = $result['error'] ?? 'Error desconocido al traducir';
throw new Exception("Error en la traducción: $errorMsg");
}
// Devolver la traducción
jsonResponse([
'success' => true,
'translatedText' => $result['translatedText']
]);
} catch (Exception $e) {
error_log("Error en translate.php: " . $e->getMessage());
jsonResponse([
'success' => false,
'error' => 'Error al procesar la traducción',
'debug' => DEBUG_MODE ? $e->getMessage() : null
], 500);
}
/**
* Envía una respuesta JSON y termina la ejecución
*/
function jsonResponse($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}

159
shared/utils/helpers.php Executable file
View File

@@ -0,0 +1,159 @@
<?php
/**
* Archivo de configuración compartido
* Carga variables de entorno y proporciona funciones helper
*/
// Cargar variables de entorno si aún no están cargadas
if (!isset($_ENV['DB_HOST'])) {
if (file_exists(__DIR__ . '/../../.env')) {
$lines = file(__DIR__ . '/../../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
if (strpos($line, '=') === false) continue;
list($key, $value) = explode('=', $line, 2);
$_ENV[trim($key)] = trim($value);
}
}
}
// Timezone
date_default_timezone_set($_ENV['TIME_ZONE_ENVIOS'] ?? 'America/Mexico_City');
/**
* Obtener variable de entorno
*/
function env($key, $default = null) {
return $_ENV[$key] ?? getenv($key) ?: $default;
}
/**
* Registrar log en archivo
*/
function logToFile($filename, $message, $level = 'INFO') {
$logDir = __DIR__ . '/../../logs/';
$logFile = $logDir . $filename;
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] [{$level}] {$message}\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Sanitizar HTML para prevenir XSS
*/
function sanitizeHTML($html) {
return htmlspecialchars($html, ENT_QUOTES, 'UTF-8');
}
/**
* Validar email
*/
function isValidEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Generar token aleatorio
*/
function generateRandomToken($length = 32) {
return bin2hex(random_bytes($length / 2));
}
/**
* Formatear bytes a tamaño legible
*/
function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
/**
* Respuesta JSON
*/
function jsonResponse($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
/**
* Verifica si el usuario autenticado tiene un permiso específico.
* @param string $permissionName El nombre del permiso a verificar (ej. 'crear_plantillas', 'gestionar_usuarios').
* @param string $platform El módulo al que aplica el permiso (ej. 'discord', 'telegram', 'global').
* @return bool True si el usuario tiene el permiso, false en caso contrario.
*/
function hasPermission($permissionName, $platform = 'global') {
$userData = JWTAuth::getUserData(); // Obtener datos del usuario logueado
// Si no hay datos de usuario o no está autenticado, no tiene permisos
if (!$userData || !isset($userData->userId)) {
return false;
}
// Los administradores tienen todos los permisos
if (isset($userData->rol) && $userData->rol === 'Admin') {
return true;
}
// Si el usuario no es Admin, verificar permisos específicos
// Los permisos se cargan en el JWT al iniciar sesión
if (isset($userData->permissions) && is_array($userData->permissions)) {
foreach ($userData->permissions as $perm) {
// Un permiso 'manage_templates' global debería dar acceso a 'manage_templates' en cualquier plataforma
// Un permiso 'manage_templates_discord' solo en Discord
if ($perm === $permissionName || $perm === $permissionName . '_' . $platform) {
return true;
}
}
}
return false;
}
/**
* Dividir texto en múltiples mensajes según límite de caracteres
*/
function splitMessage($text, $maxLength = 2000) {
if (strlen($text) <= $maxLength) {
return [$text];
}
$messages = [];
$lines = explode("\n", $text);
$currentMessage = '';
foreach ($lines as $line) {
if (strlen($currentMessage) + strlen($line) + 1 > $maxLength) {
if ($currentMessage) {
$messages[] = trim($currentMessage);
$currentMessage = '';
}
// Si una línea sola es muy larga, dividirla
if (strlen($line) > $maxLength) {
$chunks = str_split($line, $maxLength);
foreach ($chunks as $chunk) {
$messages[] = $chunk;
}
} else {
$currentMessage = $line . "\n";
}
} else {
$currentMessage .= $line . "\n";
}
}
if ($currentMessage) {
$messages[] = trim($currentMessage);
}
return $messages;
}