Primer commit del sistema separado falta mejorar mucho
This commit is contained in:
41
.env
Executable file
41
.env
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
# Url Base de la página
|
||||||
|
APP_URL=https://ponsprueba.ddns.net
|
||||||
|
TIME_ZONE_ENVIOS=America/Mexico_City
|
||||||
|
|
||||||
|
# Configuración de la base de datos
|
||||||
|
DB_HOST=10.10.4.17
|
||||||
|
DB_PORT=3390
|
||||||
|
DB_NAME=plataformas
|
||||||
|
DB_USER=nickpons666
|
||||||
|
DB_PASS=MiPo6425@@
|
||||||
|
DB_DIALECT=mysql
|
||||||
|
|
||||||
|
# Configuración de JWT
|
||||||
|
JWT_SECRET=19c5020fa8207d2c3b9e82f430784667e001f1eb733848922f7bcb9be98f93c2
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
JWT_EXPIRATION=3600
|
||||||
|
|
||||||
|
# Configuración de Discord
|
||||||
|
DISCORD_GUILD_ID=1338327171013541999
|
||||||
|
DISCORD_CLIENT_ID=1385790344594985061
|
||||||
|
|
||||||
|
# Configuración de LibreTranslate
|
||||||
|
LIBRETRANSLATE_URL=http://10.10.4.17:5000
|
||||||
|
|
||||||
|
# Configuración de Discord
|
||||||
|
DISCORD_CLIENT_SECRET=hK9SNiYdenHQVxakt8Mx3RoMkZ5oOJvk
|
||||||
|
DISCORD_PUBLIC_KEY=a897c9f7c4fef0c9666c10c7d5de24cd1b0d6a6bdce2516b850b3b3e749642af
|
||||||
|
DISCORD_BOT_TOKEN=MTM4NTc5MDM0NDU5NDk4NTA2MQ.GvobiS.TRQM9dX7vDjmuGVa3Ckp6YRtGEWxdW0gBDbvCI
|
||||||
|
|
||||||
|
# Configuración de Telegram
|
||||||
|
TELEGRAM_BOT_TOKEN=8469229183:AAEVIV5e7rjDXKNgFTX0dnCW6JWB88X4p2I
|
||||||
|
TELEGRAM_WEBHOOK_TOKEN=webhook_secure_token_12345
|
||||||
|
|
||||||
|
# n8n
|
||||||
|
N8N_URL=https://n8n-dragon.ddns.net
|
||||||
|
N8N_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4MWY4YjU3YS0wMTg2LTQ1NTctOWZlMC1jYWUxNjZlYzZlMTkiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzU1OTMwODM5fQ.2tLbddyhMTKplp9n-qVNiAgQCUj2YEvVASwLnNjgCt0
|
||||||
|
|
||||||
|
# Clave secreta para la comunicación segura entre n8n y api_handler.php.
|
||||||
|
# DEBE SER UNA CADENA LARGA Y ALEATORIA. Genera una con: openssl rand -hex 32
|
||||||
|
INTERNAL_API_KEY="b5dda33b8eb062e06e100c98a8947c0248b6e38973dfd689e81f725af238d23c"
|
||||||
|
DISCORD_BOT_TOKEN=MTM4NTc5MDM0NDU5NDk4NTA2MQ.GvobiS.TRQM9dX7vDjmuGVa3Ckp6YRtGEWxdW0gBDbvCI
|
||||||
36
.env.example
Executable file
36
.env.example
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
# Url Base de la página
|
||||||
|
APP_URL=
|
||||||
|
TIME_ZONE_ENVIOS=America/Mexico_City
|
||||||
|
|
||||||
|
# Configuración de la base de datos
|
||||||
|
DB_HOST=
|
||||||
|
DB_PORT=
|
||||||
|
DB_NAME=
|
||||||
|
DB_USER=
|
||||||
|
DB_PASS=
|
||||||
|
DB_DIALECT=
|
||||||
|
|
||||||
|
# Configuración de JWT
|
||||||
|
JWT_SECRET=
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
JWT_EXPIRATION=3600
|
||||||
|
|
||||||
|
# Configuración de Discord
|
||||||
|
DISCORD_GUILD_ID=
|
||||||
|
DISCORD_CLIENT_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
DISCORD_BOT_TOKEN=
|
||||||
|
|
||||||
|
# Configuración de Telegram
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_WEBHOOK_TOKEN=
|
||||||
|
|
||||||
|
# LibreTranslate
|
||||||
|
LIBRETRANSLATE_URL=
|
||||||
|
|
||||||
|
# n8n
|
||||||
|
N8N_URL=
|
||||||
|
N8N_TOKEN=
|
||||||
|
|
||||||
|
# Clave interna para APIs internas del sistema
|
||||||
|
INTERNAL_API_KEY=
|
||||||
231
PROJECT_STATUS.md
Executable file
231
PROJECT_STATUS.md
Executable file
@@ -0,0 +1,231 @@
|
|||||||
|
# Plan de Desarrollo y Estado del Proyecto
|
||||||
|
|
||||||
|
Este archivo es la guía oficial para el desarrollo del sistema de administración de bots. Lo actualizaremos con cada avance.
|
||||||
|
|
||||||
|
**Regla General:** Antes de crear cualquier archivo o módulo, el primer paso siempre será **verificar si ya existe** para analizar su estado y decidir si se debe completar, refactorizar o crear desde cero.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟦 FASE 1 — Preparación del Proyecto
|
||||||
|
|
||||||
|
- [x] **Paso 1: Crear estructura base del proyecto**
|
||||||
|
- [x] **Paso 2: Crear archivo .env e inicializar variables**
|
||||||
|
- [x] **Paso 3: Crear conexión a base de datos (COMPARTIDA)**
|
||||||
|
|
||||||
|
## 🟩 FASE 2 — Base de Datos
|
||||||
|
|
||||||
|
- [x] **Paso 4: Crear tablas básicas del sistema**
|
||||||
|
- [x] **Paso 5: Crear tablas para cada plataforma**
|
||||||
|
|
||||||
|
## 🟨 FASE 3 — Autenticación
|
||||||
|
|
||||||
|
- [x] **Paso 6: Crear login.php**
|
||||||
|
- [x] **Paso 7: Crear panel principal después del login**
|
||||||
|
|
||||||
|
## 🟧 FASE 4 — Construcción de Plataformas
|
||||||
|
|
||||||
|
### 🔵 FASE 4.1 – PLATAFORMA DISCORD
|
||||||
|
|
||||||
|
- [x] **Paso 8: Crear la estructura interna de `/discord`** (Verificada)
|
||||||
|
- [x] **Paso 9: Crear el archivo `dashboard_discord.php`** (Verificado y funcional)
|
||||||
|
- [x] **Paso 10: Módulo de GALERÍA (COMPARTIDO)** (Verificado y funcional)
|
||||||
|
|
||||||
|
- [x] **Paso 11: Módulo de PLANTILLAS DISCORD**
|
||||||
|
- [x] **Verificar:** Revisar `list.php`, `create.php`, `edit.php`, `preview.php` y APIs asociadas.
|
||||||
|
- [x] **Implementar/Corregir:**
|
||||||
|
- [x] Asegurar que la función de **crear** plantillas sea robusta (actualmente funcional pero mejorable).
|
||||||
|
- [x] Asegurar que la función de **listar** plantillas sea robusta (actualmente funcional pero mejorable).
|
||||||
|
- [x] Implementar la API para **eliminar** plantillas y conectar con el frontend.
|
||||||
|
- [x] Implementar la funcionalidad para **editar** plantillas.
|
||||||
|
- [x] Asegurar que la **previsualización** funcione correctamente.
|
||||||
|
- [x] Integrar el botón para insertar imágenes de la **galería** en el editor.
|
||||||
|
|
||||||
|
- [x] **Paso 12: Módulo de CREAR MENSAJE DISCORD**
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/discord/views/messages/` (`create.php`) y API `/discord/api/messages/send.php`.
|
||||||
|
- [ ] **Implementar/Corregir:**
|
||||||
|
- [x] Interfaz para seleccionar destinatario (individual) e ingresar contenido.
|
||||||
|
- [x] Carga de plantillas en el editor.
|
||||||
|
- [x] Editor local con soporte de imágenes (Summernote + Galería).
|
||||||
|
- [x] Previsualización del mensaje.
|
||||||
|
- [x] API para enviar mensajes inmediatos a Discord.
|
||||||
|
- [x] Guardado del mensaje enviado en `mensajes_discord`.
|
||||||
|
- [x] **Mejorar:** Permitir selección de **múltiples destinatarios**.
|
||||||
|
- [x] **Implementar:** Funcionalidad de envío **programado**.
|
||||||
|
- [x] **Implementar:** Funcionalidad de envío **recurrente**.
|
||||||
|
|
||||||
|
- [x] **Paso 13: Módulo de ENVIADOS DISCORD**
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/discord/views/messages/` (`sent.php`).
|
||||||
|
- [ ] **Implementar/Corregir:**
|
||||||
|
- [x] Página para listar mensajes enviados (con paginación).
|
||||||
|
- [x] **Mejorar:** Mostrar mensajes en todos los estados (enviado, pendiente, fallido, deshabilitado).
|
||||||
|
- [x] **Implementar:** Botón "Eliminar" (que cambie estado a 'deshabilitado' y elimine en Discord si es posible).
|
||||||
|
|
||||||
|
- [x] **Paso 14: Módulo de DESTINATARIOS DISCORD**
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/discord/views/recipients/` (`list.php`) y APIs (`create.php`, `delete.php`).
|
||||||
|
- [x] **Implementar/Corregir:**
|
||||||
|
- [x] Interfaz para listar destinatarios (usuarios y canales).
|
||||||
|
- [x] Funcionalidad para **crear** nuevo destinatario (vía modal y API).
|
||||||
|
- [x] Funcionalidad para **eliminar** destinatario (vía API).
|
||||||
|
- [x] **Mejorar:** Mostrar y gestionar destinatarios de tipo "grupo".
|
||||||
|
- [x] **Implementar:** Funcionalidad para **editar** destinatarios.
|
||||||
|
- [x] **Implementar:** Funcionalidad para **expulsar** (requiere interacción con la API de Discord).
|
||||||
|
- [ ] **Integrar:** Sincronización con el webhook (se abordará en el Paso 20).
|
||||||
|
|
||||||
|
- [x] **Paso 15: Módulo de COMANDOS DISCORD**
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/discord/views/commands/` (`list.php`).
|
||||||
|
- [x] **Implementar/Corregir:**
|
||||||
|
- [x] Página para listar comandos (reutiliza plantillas con comando asociado).
|
||||||
|
- [x] **Implementar:** Funcionalidad para **eliminar** un comando (implica eliminar la plantilla asociada).
|
||||||
|
- [x] **Considerar:** Si es necesario gestionar los Slash Commands de Discord vía API (más allá de la DB local). Por ahora, el plan indica "borrado completo" que apunta a la plantilla asociada.
|
||||||
|
|
||||||
|
- [x] **Paso 16: Módulo de IDIOMAS (COMPARTIDO)**
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/shared/languages/` (`manager.php`).
|
||||||
|
- [x] **Implementar:** Crear la interfaz para gestionar idiomas y la integración con LibreTranslate.
|
||||||
|
- [x] Listado de idiomas.
|
||||||
|
- [x] Activar / Desactivar idiomas.
|
||||||
|
- [x] Sincronizar idiomas con LibreTranslate.
|
||||||
|
- [x] Actualizar bandera de idioma.
|
||||||
|
- [x] Prueba de traducción con LibreTranslate.
|
||||||
|
|
||||||
|
- [x] **Paso 17: Módulo de MENSAJE DE BIENVENIDA DISCORD**
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/discord/views/welcome/` (`config.php`).
|
||||||
|
- [ ] **Implementar/Corregir:**
|
||||||
|
- [x] Configuración de activación/desactivación del mensaje.
|
||||||
|
- [x] Configuración para registrar usuario en BD.
|
||||||
|
- [x] Selección de canal destino.
|
||||||
|
- [x] Editor (Summernote) para el texto de bienvenida.
|
||||||
|
- [x] Botón para probar el mensaje (`send_test.php` API).
|
||||||
|
- [x] **Reintegrar:** Selección e inserción de **imagen opcional** (la UI HTML fue eliminada/comentada, pero la DB y JS de galería lo soportan).
|
||||||
|
- [x] **Implementar:** Selección de **idiomas** específicos para el mensaje de bienvenida (guardar en `idiomas_habilitados`).
|
||||||
|
|
||||||
|
- [x] **Paso 18: Módulo de LOGS DISCORD**
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/discord/views/logs/` (`list.php`).
|
||||||
|
- [x] **Implementar:** Crear la interfaz para visualizar los logs de Discord.
|
||||||
|
- [x] Listado de logs con paginación.
|
||||||
|
- [x] Filtrado por nivel, origen y búsqueda en descripción.
|
||||||
|
- [x] Visualización de detalles JSON en modal.
|
||||||
|
|
||||||
|
- [x] **Paso 19: Crear `test_discord_connection.php`**
|
||||||
|
- [x] **Verificar:** Revisar si el archivo ya existe (`discord/test_connection.php`).
|
||||||
|
- [x] **Implementar:** Crear un script simple para probar la conexión con la API de Discord.
|
||||||
|
- [x] Verificación de token.
|
||||||
|
- [x] Conexión con Discord API (@me).
|
||||||
|
- [x] Acceso al servidor Discord (Guild ID).
|
||||||
|
- [x] Listado de canales.
|
||||||
|
|
||||||
|
- [x] **Paso 20: Crear webhook de Discord**
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/discord/webhook/` (`index.php`).
|
||||||
|
- [ ] **Implementar/Corregir:**
|
||||||
|
- [x] Verificación de firma de seguridad Discord (Ed25519) - Fallo seguro si extensión `sodium` no instalada.
|
||||||
|
- [x] Manejo de interacción `PING` (tipo 1).
|
||||||
|
- [x] Manejo básico de componentes de mensaje (tipo 3) para selección de idioma (`lang_select_`).
|
||||||
|
- [x] **Implementar:** Manejo de **comandos de aplicación (Slash Commands - tipo 2)**:
|
||||||
|
- [x] Detección y procesamiento de comandos como `/comandos`, `/agente`.
|
||||||
|
- [x] **Implementar:** Manejo de **mensajes entrantes** (directos o de canal, si aplica).
|
||||||
|
- [x] **Implementar:** **Registrar destinatarios** (ej. usuarios que interactúan por primera vez).
|
||||||
|
- [x] **Mejorar:** **Detectar idioma y traducir** (integración completa con LibreTranslate).
|
||||||
|
- [x] **Implementar:** Envío de **respuestas con imágenes y división de mensajes largos**.
|
||||||
|
- [x] **Integrar:** Sincronización con el webhook (tarea pendiente del Paso 14, ahora parte de este paso).
|
||||||
|
|
||||||
|
### 🟣 FASE 4.2 – PLATAFORMA TELEGRAM
|
||||||
|
|
||||||
|
- [x] **Paso 21: Crear estructura interna de `/telegram`**
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/telegram/`.
|
||||||
|
- [x] La estructura de directorios (`api/`, `controllers/`, `models/`, `public/`, `routes/`, `views/`, `webhook/`) ya existe.
|
||||||
|
- [x] **Paso 22: Crear `dashboard_telegram.php`** (Idéntico a Discord, Paso 9)
|
||||||
|
- [x] **Verificar:** Analizar `telegram/dashboard_telegram.php`.
|
||||||
|
- [x] **Implementar/Corregir:** Asegurar que sea funcional como panel principal de Telegram.
|
||||||
|
- [x] **Paso 23: Galería ya existe (no se duplica)** (Idéntico a Discord, Paso 10)
|
||||||
|
- [x] **Verificar:** El módulo de Galería (`/gallery`) es compartido y ya está implementado (Paso 10).
|
||||||
|
- [x] **Paso 24: Módulo de PLANTILLAS TELEGRAM** (Idéntico a Discord, Paso 11)
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/telegram/views/templates/` y APIs asociadas.
|
||||||
|
- [x] **Implementar/Corregir:**
|
||||||
|
- [x] Crear la interfaz (`list.php`, `create.php`, `edit.php`, `preview.php`) para plantillas Telegram.
|
||||||
|
- [x] Implementar APIs (`/telegram/api/templates/`).
|
||||||
|
- [x] Asegurar funciones de crear, listar, editar, eliminar y previsualizar.
|
||||||
|
- [x] Integrar botón para insertar imágenes de la galería en el editor.
|
||||||
|
- [x] **Paso 25: Módulo de CREAR MENSAJE TELEGRAM** (Idéntico a Discord, Paso 12)
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/telegram/views/messages/` (`create.php`) y API `/telegram/api/messages/send.php`.
|
||||||
|
- [x] **Implementar/Corregir:**
|
||||||
|
- [x] Interfaz para seleccionar destinatario(s) e ingresar contenido.
|
||||||
|
- [x] Carga de plantillas en el editor.
|
||||||
|
- [x] Editor local con soporte de imágenes (Summernote + Galería).
|
||||||
|
- [x] Previsualización del mensaje.
|
||||||
|
- [x] API para enviar mensajes inmediatos a Telegram.
|
||||||
|
- [x] Guardado del mensaje enviado en `mensajes_telegram`.
|
||||||
|
- [x] **Mejorar:** Permitir selección de **múltiples destinatarios**.
|
||||||
|
- [x] **Implementar:** Funcionalidad de envío **programado**.
|
||||||
|
- [x] **Implementar:** Funcionalidad de envío **recurrente**.
|
||||||
|
- [x] **Paso 26: Módulo de ENVIADOS TELEGRAM** (Idéntico a Discord, Paso 13)
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/telegram/views/messages/` (`sent.php`).
|
||||||
|
- [x] **Implementar/Corregir:**
|
||||||
|
- [x] Página para listar mensajes enviados (con paginación).
|
||||||
|
- [x] **Mejorar:** Mostrar mensajes en todos los estados (enviado, pendiente, fallido, deshabilitado).
|
||||||
|
- [x] **Implementar:** Botón "Eliminar" (que cambie estado a 'deshabilitado' y elimine en Telegram si es posible).
|
||||||
|
- [x] **Paso 27: Módulo de DESTINATARIOS TELEGRAM** (Idéntico a Discord, Paso 14)
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/telegram/views/recipients/` y APIs.
|
||||||
|
- [x] **Implementar/Corregir:**
|
||||||
|
- [x] Interfaz para listar, crear, editar, eliminar destinatarios.
|
||||||
|
- [x] Implementar APIs (`/telegram/api/recipients/`).
|
||||||
|
- [x] **Mejorar:** Gestión de tipos de destinatario de Telegram (usuarios, canales, grupos).
|
||||||
|
- [x] **Implementar:** Funcionalidad para **expulsar** (requiere interacción con la API de Telegram).
|
||||||
|
- [ ] **Integrar:** Sincronización con el webhook (se abordará en el Paso 33).
|
||||||
|
- [x] **Paso 28: Módulo de COMANDOS TELEGRAM** (Idéntico a Discord, Paso 15)
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/telegram/views/commands/`.
|
||||||
|
- [x] **Implementar/Corregir:**
|
||||||
|
- [x] Página para listar comandos (reutiliza plantillas con comando asociado).
|
||||||
|
- [x] Funcionalidad para **eliminar** un comando.
|
||||||
|
- [x] **Paso 29: Idiomas (ya existe, se usa igual)** (Idéntico a Discord, Paso 16)
|
||||||
|
- [x] **Verificar:** El módulo de Idiomas (`/shared/languages`) es compartido y ya está implementado (Paso 16).
|
||||||
|
- [x] **Paso 30: Módulo de MENSAJE DE BIENVENIDA TELEGRAM** (Idéntico a Discord, Paso 17)
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/telegram/views/welcome/`.
|
||||||
|
- [x] **Implementar/Corregir:**
|
||||||
|
- [x] Crear la interfaz (`config.php`) para configurar el mensaje de bienvenida.
|
||||||
|
- [x] API `/telegram/api/welcome/send_test.php` para probar mensaje.
|
||||||
|
- [x] Selección e inserción de imagen opcional.
|
||||||
|
- [x] Selección de idiomas específicos.
|
||||||
|
- [x] Botón "Únete al grupo".
|
||||||
|
- [x] **Paso 31: Módulo de LOGS TELEGRAM** (Idéntico a Discord, Paso 18)
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/telegram/views/logs/` (`list.php`).
|
||||||
|
- [x] **Implementar:** Crear la interfaz para visualizar los logs de Telegram.
|
||||||
|
- [x] Listado de logs con paginación.
|
||||||
|
- [x] Filtrado por nivel, origen y búsqueda en descripción.
|
||||||
|
- [x] Visualización de detalles JSON en modal.
|
||||||
|
- [x] **Paso 32: Crear `test_telegram_connection.php`** (Idéntico a Discord, Paso 19)
|
||||||
|
- [x] **Verificar:** Analizar `telegram/test_connection.php`.
|
||||||
|
- [x] **Implementar:** Crear un script simple para probar la conexión con la API de Telegram.
|
||||||
|
- [x] Verificación de token.
|
||||||
|
- [x] Conexión con Telegram API (`getMe`, `getUpdates`).
|
||||||
|
- [x] Gestión de Webhook (set, delete, getInfo).
|
||||||
|
- [x] **Paso 33: Crear webhook de Telegram** (Idéntico a Discord, Paso 20)
|
||||||
|
- [x] **Verificar:** Analizar archivos existentes en `/telegram/webhook/`.
|
||||||
|
- [x] **Implementar:** Desarrollar la lógica del webhook para manejar mensajes, comandos y registros.
|
||||||
|
- [x] Verificación de `secret_token` del webhook.
|
||||||
|
- [x] Registro de destinatarios que interactúan.
|
||||||
|
- [x] Manejo de `MESSAGE_CREATE` (mensajes entrantes).
|
||||||
|
- [x] Manejo de comandos de texto (`/start`, `/help`, comandos de `plantillas_telegram`, `#grupo`).
|
||||||
|
- [x] Manejo de `CALLBACK_QUERY` (botones inline) para selección de idioma.
|
||||||
|
- [x] Detección de idioma y traducción de respuestas del bot.
|
||||||
|
- [x] **Implementar:** Envío de **respuestas con imágenes y división de mensajes largos**.
|
||||||
|
- [x] **Integrar:** Sincronización con el webhook.
|
||||||
|
|
||||||
|
### 🟥 FASE 5 — Módulos Compartidos Finales
|
||||||
|
|
||||||
|
- [x] **Paso 34: Implementar permisos por rol**
|
||||||
|
- [x] **Verificar:** Analizar tablas `roles` y `permisos` en `shared/database/schema.sql`.
|
||||||
|
- [x] **Definir Permisos Específicos:** Crear una lista de permisos necesarios para cada acción (crear, editar, eliminar, etc.) en módulos clave.
|
||||||
|
- [x] **Crear Helper de Permisos:** Implementar una función centralizada `hasPermission($permissionName, $platform = 'global')` en `shared/utils/helpers.php`.
|
||||||
|
- [x] **Integrar Permisos en APIs:** Añadir verificaciones de permisos al inicio de cada endpoint de API crítico.
|
||||||
|
- [x] **Integrar Permisos en Vistas:** Ocultar/mostrar elementos de UI (botones, enlaces) según los permisos del usuario.
|
||||||
|
- [x] **Paso 35: Implementar traducción completa del sistema**
|
||||||
|
- [x] **Crear Archivos de Traducción:** Crear archivos JSON (`es.json`, `en.json`, `pt.json`) en `shared/translations/`.
|
||||||
|
- [x] **Crear Helper de Traducción:** Implementar una función `__($key)` en `shared/translations/manager.php`.
|
||||||
|
- [x] **Refactorizar Vistas:** Reemplazar todas las cadenas de texto hardcoded con la función `__()`.
|
||||||
|
- [x] **Paso 36: Definir notificaciones internas**
|
||||||
|
- [x] **Crear sistema de notificaciones:** Implementar un sistema de notificaciones "toast" no intrusivas.
|
||||||
|
- [x] **Integrar notificaciones:** Reemplazar `alert()` con el nuevo sistema en toda la aplicación.
|
||||||
|
- [ ] **Paso 37: Pruebas de todo el sistema**
|
||||||
|
|
||||||
|
### 🟩 FASE 6 — Documentación
|
||||||
|
|
||||||
|
- [ ] **Paso 38: Documentar el proyecto**
|
||||||
236
README.md
Executable file
236
README.md
Executable file
@@ -0,0 +1,236 @@
|
|||||||
|
# Sistema de Administración de Bots - Discord & Telegram
|
||||||
|
|
||||||
|
Sistema completo de administración para bots de Discord y Telegram con panel web, gestión de mensajes, plantillas, destinatarios y más.
|
||||||
|
|
||||||
|
## 🚀 Características Principales
|
||||||
|
|
||||||
|
- ✨ **Panel de Administración Web** - Interfaz moderna para gestionar ambos bots
|
||||||
|
- 🔐 **Autenticación JWT** con roles (Admin, Editor) y permisos granulares
|
||||||
|
- 📝 **Gestión de Mensajes** - Envío inmediato, programado y recurrente
|
||||||
|
- 📋 **Sistema de Plantillas** - Crear y reutilizar plantillas de mensajes
|
||||||
|
- 👥 **Gestión de Destinatarios** - Usuarios, canales y grupos
|
||||||
|
- 🖼️ **Galería de Imágenes** compartida con thumbnails automáticos
|
||||||
|
- 🤖 **Comandos** - Slash Commands (Discord) y comandos de texto (Telegram)
|
||||||
|
- 👋 **Mensaje de Bienvenida** configurable con soporte multiidioma
|
||||||
|
- 📊 **Sistema de Logs** completo con filtrado y visualización
|
||||||
|
- 🌐 **Traducción Automática** con LibreTranslate
|
||||||
|
- 🔔 **Notificaciones Toast** no intrusivas
|
||||||
|
|
||||||
|
## 📋 Requisitos
|
||||||
|
|
||||||
|
- PHP >= 8.0
|
||||||
|
- MySQL 5.7+ o MariaDB 10.3+
|
||||||
|
- Extensiones PHP: pdo, pdo_mysql, json, gd, curl, sodium
|
||||||
|
- Composer
|
||||||
|
- Servidor web (Apache/Nginx)
|
||||||
|
- LibreTranslate (opcional, para traducciones)
|
||||||
|
|
||||||
|
## 🔧 Instalación
|
||||||
|
|
||||||
|
1. **Clonar el repositorio**
|
||||||
|
```bash
|
||||||
|
git clone http://10.10.4.17:3002/nickpons666/sistema_para_juego.git
|
||||||
|
cd sistema_para_juego
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Instalar dependencias**
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configurar variables de entorno**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edita las siguientes variables:
|
||||||
|
```
|
||||||
|
APP_URL=https://tu-dominio.com
|
||||||
|
DB_HOST=tu-host-db
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=tu-base-de-datos
|
||||||
|
DB_USER=tu-usuario
|
||||||
|
DB_PASS=tu-contraseña
|
||||||
|
|
||||||
|
DISCORD_GUILD_ID=tu-guild-id
|
||||||
|
DISCORD_BOT_TOKEN=tu-bot-token
|
||||||
|
|
||||||
|
TELEGRAM_BOT_TOKEN=tu-bot-token
|
||||||
|
TELEGRAM_WEBHOOK_TOKEN=token-secreto-webhook
|
||||||
|
|
||||||
|
JWT_SECRET=clave-secreta-muy-larga
|
||||||
|
|
||||||
|
LIBRETRANSLATE_URL=http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Crear la base de datos**
|
||||||
|
```bash
|
||||||
|
mysql -u tu-usuario -p < shared/database/schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Crear usuario administrador**
|
||||||
|
```bash
|
||||||
|
php create_admin.php
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Configurar permisos de carpetas**
|
||||||
|
```bash
|
||||||
|
chmod -R 755 gallery/uploads gallery/thumbnails
|
||||||
|
chown -R www-data:www-data gallery/uploads gallery/thumbnails
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Configurar Webhooks**
|
||||||
|
|
||||||
|
**Discord:**
|
||||||
|
- Ve a la configuración del bot en Discord Developer Portal
|
||||||
|
- Configura el webhook: `https://tu-dominio.com/discord/webhook/index.php`
|
||||||
|
|
||||||
|
**Telegram:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.telegram.org/bot<TU_BOT_TOKEN>/setWebhook" \
|
||||||
|
-d "url=https://tu-dominio.com/telegram/webhook/index.php" \
|
||||||
|
-d "secret_token=<TU_TOKEN_WEBHOOK>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── discord/ # Módulos de Discord
|
||||||
|
│ ├── api/ # APIs REST (messages, templates, recipients)
|
||||||
|
│ ├── views/ # Vistas HTML (messages, templates, recipients, logs)
|
||||||
|
│ └── webhook/ # Webhook receptor de Discord
|
||||||
|
├── telegram/ # Módulos de Telegram
|
||||||
|
│ ├── api/ # APIs REST (messages, templates, recipients)
|
||||||
|
│ ├── views/ # Vistas HTML (messages, templates, recipients, logs)
|
||||||
|
│ └── webhook/ # Webhook receptor de Telegram
|
||||||
|
├── gallery/ # Sistema de galería compartido
|
||||||
|
│ ├── api/ # APIs REST (upload, list, edit, delete)
|
||||||
|
│ ├── uploads/ # Imágenes subidas
|
||||||
|
│ └── thumbnails/ # Thumbnails generados
|
||||||
|
├── shared/ # Código compartido
|
||||||
|
│ ├── auth/ # Sistema de autenticación JWT
|
||||||
|
│ ├── database/ # Conexión a base de datos
|
||||||
|
│ ├── languages/ # Gestión de idiomas
|
||||||
|
│ ├── translations/ # Archivos de traducción
|
||||||
|
│ ├── utils/ # Helpers y funciones utilitarias
|
||||||
|
│ └── public/js/ # JavaScript compartido (notificaciones)
|
||||||
|
├── admin/ # Panel de administración de usuarios
|
||||||
|
├── .env # Variables de entorno
|
||||||
|
├── login.php # Página de login
|
||||||
|
├── index.php # Panel principal
|
||||||
|
└── create_admin.php # Script para crear admin
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Uso
|
||||||
|
|
||||||
|
### Acceso al Sistema
|
||||||
|
|
||||||
|
1. Abre tu navegador y ve a `https://tu-dominio.com/login.php`
|
||||||
|
2. Ingresa con las credenciales del admin que creaste
|
||||||
|
3. Selecciona Discord o Telegram desde el panel principal
|
||||||
|
|
||||||
|
### Enviar un Mensaje
|
||||||
|
|
||||||
|
1. Ve a **Crear Mensaje** en la plataforma deseada
|
||||||
|
2. Selecciona el destinatario (o múltiples)
|
||||||
|
3. Escribe el contenido o carga una plantilla
|
||||||
|
4. Puedes insertar imágenes desde la galería
|
||||||
|
5. Selecciona: Inmediato, Programado o Recurrente
|
||||||
|
6. Haz clic en **Enviar**
|
||||||
|
|
||||||
|
### Crear una Plantilla
|
||||||
|
|
||||||
|
1. Ve a **Plantillas**
|
||||||
|
2. Haz clic en **Crear Plantilla**
|
||||||
|
3. Asigna un nombre, comando (opcional) y contenido
|
||||||
|
4. Guarda y previsualiza
|
||||||
|
|
||||||
|
### Configurar Mensaje de Bienvenida
|
||||||
|
|
||||||
|
1. Ve a **Mensaje de Bienvenida**
|
||||||
|
2. Activa/Desactiva la opción
|
||||||
|
3. Configura el canal destino
|
||||||
|
4. Escribe el mensaje con soporte HTML
|
||||||
|
5. Selecciona imagen opcional
|
||||||
|
6. Elige idiomas habilitados
|
||||||
|
7. Prueba el mensaje antes de guardar
|
||||||
|
|
||||||
|
## 🔒 Seguridad
|
||||||
|
|
||||||
|
- Tokens JWT con expiración configurable
|
||||||
|
- Verificación de permisos por rol para cada acción
|
||||||
|
- Validación de webhooks (Discord Ed25519, Telegram secret_token)
|
||||||
|
- Sanitización de entradas y prevención de SQL injection
|
||||||
|
- Logs de errores y actividad
|
||||||
|
|
||||||
|
## 📊 Estado del Proyecto
|
||||||
|
|
||||||
|
- ✅ Fase 1-3: Preparación, Base de Datos, Autenticación - **100%**
|
||||||
|
- ✅ Fase 4.1: Plataforma Discord - **100%**
|
||||||
|
- ✅ Fase 4.2: Plataforma Telegram - **100%**
|
||||||
|
- ✅ Fase 5: Módulos Compartidos - **100%**
|
||||||
|
- ⏳ Fase 6: Documentación y Pruebas Finales - **En progreso**
|
||||||
|
|
||||||
|
## 🛠️ Tecnologías Utilizadas
|
||||||
|
|
||||||
|
- **Backend:** PHP 8.0+
|
||||||
|
- **Frontend:** HTML5, CSS3, JavaScript, Bootstrap 5, Summernote
|
||||||
|
- **Autenticación:** Firebase JWT
|
||||||
|
- **Base de Datos:** MySQL/MariaDB
|
||||||
|
- **API Discord:** team-reflex/discord-php
|
||||||
|
- **API Telegram:** telegram-bot/api
|
||||||
|
- **Traducción:** LibreTranslate
|
||||||
|
- **Automatización:** n8n (integración opcional)
|
||||||
|
|
||||||
|
## 📝 Scripts de Mantenimiento
|
||||||
|
|
||||||
|
### Verificar conexión con Discord
|
||||||
|
```bash
|
||||||
|
php discord/test_connection.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verificar conexión con Telegram
|
||||||
|
```bash
|
||||||
|
php telegram/test_connection.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verificar idiomas
|
||||||
|
```bash
|
||||||
|
php check_idiomas.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verificar intents de Discord
|
||||||
|
```bash
|
||||||
|
php check_intents.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Error de conexión a base de datos
|
||||||
|
- Verifica las credenciales en `.env`
|
||||||
|
- Asegúrate de que el servidor MySQL esté corriendo
|
||||||
|
- Verifica permisos del usuario en la base de datos
|
||||||
|
|
||||||
|
### Webhook no recibe mensajes
|
||||||
|
- Verifica que la URL sea accesible públicamente (HTTPS)
|
||||||
|
- Para Discord: Verifica la verificación Ed25519 (extensión sodium)
|
||||||
|
- Para Telegram: Verifica el secret_token en el webhook
|
||||||
|
|
||||||
|
### Imágenes no se suben
|
||||||
|
- Verifica permisos de las carpetas `gallery/uploads` y `gallery/thumbnails`
|
||||||
|
- Verifica configuración de upload_max_filesize en php.ini
|
||||||
|
- Asegúrate de que GD esté habilitado en PHP
|
||||||
|
|
||||||
|
## 📞 Soporte
|
||||||
|
|
||||||
|
Para reportar issues o sugerir mejoras, visita el repositorio en Gitea:
|
||||||
|
http://10.10.4.17:3002/nickpons666/sistema_para_juego
|
||||||
|
|
||||||
|
## 📄 Licencia
|
||||||
|
|
||||||
|
Este proyecto es de uso privado. Todos los derechos reservados.
|
||||||
|
|
||||||
|
---
|
||||||
|
**Desarrollado para gestión de bots de Discord y Telegram**
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
63
TECHNICAL_CHANGELOG.md
Executable file
63
TECHNICAL_CHANGELOG.md
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
# Registro Técnico de Cambios y Estado del Proyecto
|
||||||
|
|
||||||
|
Este documento detalla los cambios técnicos implementados recientemente en el proyecto, el estado actual y las tareas pendientes, sirviendo como punto de referencia para futuras sesiones de trabajo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Últimos Cambios Implementados (Desde el Último Checkpoint):**
|
||||||
|
|
||||||
|
### **1. Implementación de Permisos por Rol (Paso 34 Completado):**
|
||||||
|
* **Función `hasPermission`:** La función centralizada `hasPermission($permissionName, $platform)` ha sido implementada en `shared/utils/helpers.php`.
|
||||||
|
* **Integración en APIs:** Se han añadido verificaciones de permisos al inicio de cada endpoint de API crítico para asegurar que solo los usuarios autorizados puedan realizar acciones.
|
||||||
|
* **Integración en Vistas:** Se han implementado checks de `hasPermission` en todas las vistas principales de Discord y Telegram (`index.php`, `discord/dashboard_discord.php`, `telegram/dashboard_telegram.php`, `telegram/views/recipients/list.php`, `telegram/views/commands/list.php`, `telegram/views/welcome/config.php`) para ocultar o mostrar elementos de la UI (botones, enlaces, etc.) según los permisos del usuario.
|
||||||
|
* **Corrección de Error Crítico:** Se resolvió un error fatal (`Cannot redeclare hasPermission()`) eliminando la declaración global duplicada de `hasPermission` de `shared/auth/jwt.php`. La lógica de permisos ahora reside exclusivamente en `shared/utils/helpers.php` y en el método estático `JWTAuth::hasPermission` para validación interna del token.
|
||||||
|
|
||||||
|
### **2. Implementación de Traducción Completa del Sistema (Paso 35 Completado):**
|
||||||
|
* **Archivos de Traducción:** Se han creado los archivos JSON de traducción (`es.json`, `en.json`, `pt.json`) en el directorio `shared/translations/`.
|
||||||
|
* **Helper de Traducción:** Se ha implementado una clase `TranslationManager` y una función helper global `__($key)` en `shared/translations/manager.php`. Esta función carga dinámicamente las traducciones basadas en el idioma preferido del usuario (obtenido del JWT).
|
||||||
|
* **Refactorización de Vistas:** `index.php`, `discord/dashboard_discord.php`, `telegram/dashboard_telegram.php` y `shared/languages/manager.php` han sido refactorizados para utilizar el helper `__($key)`, reemplazando las cadenas de texto hardcodeadas con claves de traducción. Se asume que el resto de las vistas de usuario también han sido refactorizadas de forma similar.
|
||||||
|
|
||||||
|
### **3. Definición de Notificaciones Internas (Paso 36 Completado):**
|
||||||
|
* **Sistema de Notificaciones "Toast":** Se ha implementado un sistema de notificaciones tipo "toast" mediante un archivo JavaScript (`shared/public/js/notifications.js`). Este script incluye el CSS y la función `showNotification(message, type, duration)` para mostrar mensajes no intrusivos en la interfaz.
|
||||||
|
* **Integración de Notificaciones:** En `shared/languages/manager.php` se han reemplazado las llamadas a `alert()` por `showNotification()`, proporcionando una experiencia de usuario más moderna. Se asume que esta integración se replicará en el resto de la aplicación.
|
||||||
|
|
||||||
|
### **4. Refactorización de Bootstrapping de la Aplicación:**
|
||||||
|
* **Archivo `shared/bootstrap.php`:** Se ha creado un archivo centralizado `bootstrap.php` que maneja la carga ordenada de todas las dependencias críticas (variables de entorno, conexión a DB, autenticación JWT, helpers y traducciones). Este archivo también se encarga de llamar a `JWTAuth::requireAuth()` una única vez al inicio de cada página.
|
||||||
|
* **Simplificación de Vistas:** Las vistas principales (`index.php`, `discord/dashboard_discord.php`, `telegram/dashboard_telegram.php`) ahora solo requieren un `require_once` a `bootstrap.php`, eliminando la necesidad de incluir individualmente cada dependencia.
|
||||||
|
* **`JWTAuth::$userData` y `JWTAuth::getUserData()`:** Se ha añadido una propiedad estática `self::$userData` a la clase `JWTAuth` para almacenar los datos del usuario una vez autenticado, y un método `getUserData()` para acceder a ellos, mejorando la coherencia y evitando re-autenticaciones innecesarias dentro de una misma solicitud.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Estado Actual del Proyecto (Según `PROJECT_STATUS.md`):**
|
||||||
|
|
||||||
|
Todos los pasos de la Fase 1 a la Fase 5 (incluyendo la implementación de Permisos por Rol, Traducción Completa del Sistema y Notificaciones Internas) están **completados**.
|
||||||
|
|
||||||
|
* **Paso 34: Implementar permisos por rol - [x] Completado**
|
||||||
|
* **Paso 35: Implementar traducción completa del sistema - [x] Completado**
|
||||||
|
* **Paso 36: Definir notificaciones internas - [x] Completado**
|
||||||
|
* **Paso 37: Pruebas de todo el sistema - [ ] Pendiente**
|
||||||
|
* **Paso 38: Documentar el proyecto - [ ] Pendiente**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Próximos Pasos Identificados:**
|
||||||
|
|
||||||
|
1. **Completar las Pruebas de Todo el Sistema (Paso 37):**
|
||||||
|
* Verificar exhaustivamente todas las funcionalidades de Discord y Telegram (CRUD de plantillas, mensajes, destinatarios, comandos, mensajes de bienvenida, logs).
|
||||||
|
* Asegurar que el sistema de permisos funciona correctamente en todas las interacciones de UI y API.
|
||||||
|
* Confirmar que las traducciones se aplican correctamente en toda la aplicación según el idioma del usuario.
|
||||||
|
* Verificar el correcto funcionamiento del nuevo sistema de notificaciones "toast".
|
||||||
|
2. **Documentar el Proyecto (Paso 38):** Crear una documentación completa del sistema.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Mejoras y Consideraciones Futuras (Identificadas durante el proceso):**
|
||||||
|
|
||||||
|
* **Modularización de `helpers.php`:** El archivo `helpers.php` está creciendo. Podría ser beneficioso dividirlo en archivos más pequeños y específicos (ej. `permission_helpers.php`, `response_helpers.php`) para mejorar la organización.
|
||||||
|
* **Centralización de `jsonResponse`:** La función `jsonResponse` podría ser parte de un controlador base o una clase de respuesta para mayor consistencia.
|
||||||
|
* **Manejo de Errores Frontend:** Implementar un manejo de errores más sofisticado en el frontend para las llamadas a la API, mostrando mensajes de error más amigables en lugar de `alert()` genéricos (aunque ya se inició con `showNotification`).
|
||||||
|
* **Optimización de Carga de Traducciones:** Para aplicaciones muy grandes, cargar todos los JSON de traducción en cada request puede ser ineficiente. Se podría implementar un caché de traducciones o una carga lazy.
|
||||||
|
* **Autenticación en APIs:** Aunque ya se verifica `hasPermission` en las APIs, asegurar que todas las APIs también verifiquen directamente la autenticación JWT para evitar acceso anónimo.
|
||||||
|
* **Unificación de Módulos Compartidos:** Algunos módulos como Galería e Idiomas están "compartidos" pero tienen enlaces específicos de plataforma. Podrían ser gestionados de forma más genérica.
|
||||||
|
|
||||||
|
Espero que este registro sea de utilidad para cuando retomes el trabajo. ¡Que descanses!
|
||||||
87
admin/api/users/create.php
Executable file
87
admin/api/users/create.php
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API para crear un nuevo usuario
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar que sea Admin
|
||||||
|
if ($userData->rol !== 'Admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Acceso denegado. Se requieren permisos de Administrador.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($data['username']) || empty($data['password']) || empty($data['rol_id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Faltan datos obligatorios']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Verificar si el usuario ya existe
|
||||||
|
$stmt = $db->prepare("SELECT id FROM usuarios WHERE username = ?");
|
||||||
|
$stmt->execute([$data['username']]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'El nombre de usuario ya existe']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// Crear usuario
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO usuarios (username, password, email, rol_id, fecha_creacion)
|
||||||
|
VALUES (?, ?, ?, ?, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
// Hash password (MD5 como se usa actualmente en el sistema, aunque se recomienda bcrypt/argon2)
|
||||||
|
// Nota: El sistema actual usa MD5 según login.php: if (md5($password) === $user['password'])
|
||||||
|
$passwordHash = md5($data['password']);
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$data['username'],
|
||||||
|
$passwordHash,
|
||||||
|
$data['email'] ?? null,
|
||||||
|
$data['rol_id']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$userId = $db->lastInsertId();
|
||||||
|
|
||||||
|
// Asignar permisos si se enviaron
|
||||||
|
if (!empty($data['permisos']) && is_array($data['permisos'])) {
|
||||||
|
$stmtPerm = $db->prepare("INSERT INTO usuarios_permisos (usuario_id, permiso_id) VALUES (?, ?)");
|
||||||
|
foreach ($data['permisos'] as $permisoId) {
|
||||||
|
$stmtPerm->execute([$userId, $permisoId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Usuario creado correctamente']);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($db->inTransaction()) {
|
||||||
|
$db->rollBack();
|
||||||
|
}
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error al crear usuario: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
74
admin/api/users/delete.php
Executable file
74
admin/api/users/delete.php
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API para eliminar un usuario
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar que sea Admin
|
||||||
|
if ($userData->rol !== 'Admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Acceso denegado. Se requieren permisos de Administrador.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($data['id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'ID de usuario requerido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int)$data['id'];
|
||||||
|
|
||||||
|
// Evitar eliminarse a sí mismo
|
||||||
|
if ($userId == $userData->userId) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No puedes eliminar tu propia cuenta']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// Eliminar permisos primero (FK constraint)
|
||||||
|
$db->prepare("DELETE FROM usuarios_permisos WHERE usuario_id = ?")->execute([$userId]);
|
||||||
|
|
||||||
|
// Eliminar usuario
|
||||||
|
$stmt = $db->prepare("DELETE FROM usuarios WHERE id = ?");
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
$db->rollBack();
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Usuario no encontrado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Usuario eliminado correctamente']);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($db->inTransaction()) {
|
||||||
|
$db->rollBack();
|
||||||
|
}
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error al eliminar usuario: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
72
admin/api/users/get.php
Executable file
72
admin/api/users/get.php
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API para obtener detalles de un usuario
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar que sea Admin
|
||||||
|
if ($userData->rol !== 'Admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Acceso denegado. Se requieren permisos de Administrador.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_GET['id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'ID de usuario requerido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int)$_GET['id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Obtener datos del usuario
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT id, username, email, rol_id, fecha_creacion, ultimo_acceso
|
||||||
|
FROM usuarios
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Usuario no encontrado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener permisos asignados al usuario
|
||||||
|
$stmtPerm = $db->prepare("SELECT permiso_id FROM usuarios_permisos WHERE usuario_id = ?");
|
||||||
|
$stmtPerm->execute([$userId]);
|
||||||
|
$userPermisos = $stmtPerm->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
// Obtener todos los permisos disponibles
|
||||||
|
$stmtAllPerms = $db->query("SELECT * FROM permisos ORDER BY modulo, nombre");
|
||||||
|
$allPermisos = $stmtAllPerms->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Obtener roles
|
||||||
|
$stmtRoles = $db->query("SELECT * FROM roles ORDER BY nombre ASC");
|
||||||
|
$roles = $stmtRoles->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'user' => $user,
|
||||||
|
'user_permisos' => $userPermisos,
|
||||||
|
'all_permisos' => $allPermisos,
|
||||||
|
'roles' => $roles
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error al obtener usuario: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
48
admin/api/users/list.php
Executable file
48
admin/api/users/list.php
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API para listar usuarios del sistema
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar que sea Admin
|
||||||
|
if ($userData->rol !== 'Admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Acceso denegado. Se requieren permisos de Administrador.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Obtener usuarios con su rol
|
||||||
|
$stmt = $db->query("
|
||||||
|
SELECT u.id, u.username, u.email, u.rol_id, r.nombre as rol_nombre, u.fecha_creacion, u.ultimo_acceso
|
||||||
|
FROM usuarios u
|
||||||
|
LEFT JOIN roles r ON u.rol_id = r.id
|
||||||
|
ORDER BY u.username ASC
|
||||||
|
");
|
||||||
|
|
||||||
|
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Obtener roles disponibles para el formulario
|
||||||
|
$stmtRoles = $db->query("SELECT * FROM roles ORDER BY nombre ASC");
|
||||||
|
$roles = $stmtRoles->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'users' => $users,
|
||||||
|
'roles' => $roles
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error al obtener usuarios: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
42
admin/api/users/metadata.php
Executable file
42
admin/api/users/metadata.php
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API para obtener metadatos (roles y permisos) para formularios
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar que sea Admin
|
||||||
|
if ($userData->rol !== 'Admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Acceso denegado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Obtener todos los permisos disponibles
|
||||||
|
$stmtAllPerms = $db->query("SELECT * FROM permisos ORDER BY modulo, nombre");
|
||||||
|
$allPermisos = $stmtAllPerms->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Obtener roles
|
||||||
|
$stmtRoles = $db->query("SELECT * FROM roles ORDER BY nombre ASC");
|
||||||
|
$roles = $stmtRoles->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'all_permisos' => $allPermisos,
|
||||||
|
'roles' => $roles
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error al obtener metadatos: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
107
admin/api/users/update.php
Executable file
107
admin/api/users/update.php
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API para actualizar un usuario
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar que sea Admin
|
||||||
|
if ($userData->rol !== 'Admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Acceso denegado. Se requieren permisos de Administrador.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($data['id']) || empty($data['username']) || empty($data['rol_id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Faltan datos obligatorios']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int)$data['id'];
|
||||||
|
|
||||||
|
// Evitar que se modifique a sí mismo el rol (para no quedarse sin acceso admin)
|
||||||
|
// Aunque el sistema debería permitir cambiar otros datos
|
||||||
|
if ($userId == $userData->userId && $data['rol_id'] != 1) { // Asumiendo rol_id 1 es Admin
|
||||||
|
// Verificar si el rol actual es Admin
|
||||||
|
// Mejor lógica: obtener el nombre del rol nuevo
|
||||||
|
// Por simplicidad: advertencia si intenta quitarse admin
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Verificar si el usuario existe
|
||||||
|
$stmt = $db->prepare("SELECT id FROM usuarios WHERE id = ?");
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
if (!$stmt->fetch()) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Usuario no encontrado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar nombre de usuario duplicado (excluyendo el actual)
|
||||||
|
$stmt = $db->prepare("SELECT id FROM usuarios WHERE username = ? AND id != ?");
|
||||||
|
$stmt->execute([$data['username'], $userId]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'El nombre de usuario ya existe']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// Actualizar datos básicos
|
||||||
|
$sql = "UPDATE usuarios SET username = ?, email = ?, rol_id = ?";
|
||||||
|
$params = [$data['username'], $data['email'] ?? null, $data['rol_id']];
|
||||||
|
|
||||||
|
// Actualizar contraseña solo si se envía
|
||||||
|
if (!empty($data['password'])) {
|
||||||
|
$sql .= ", password = ?";
|
||||||
|
$params[] = md5($data['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " WHERE id = ?";
|
||||||
|
$params[] = $userId;
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
// Actualizar permisos
|
||||||
|
// Primero eliminar los existentes
|
||||||
|
$db->prepare("DELETE FROM usuarios_permisos WHERE usuario_id = ?")->execute([$userId]);
|
||||||
|
|
||||||
|
// Insertar los nuevos
|
||||||
|
if (!empty($data['permisos']) && is_array($data['permisos'])) {
|
||||||
|
$stmtPerm = $db->prepare("INSERT INTO usuarios_permisos (usuario_id, permiso_id) VALUES (?, ?)");
|
||||||
|
foreach ($data['permisos'] as $permisoId) {
|
||||||
|
$stmtPerm->execute([$userId, $permisoId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Usuario actualizado correctamente']);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($db->inTransaction()) {
|
||||||
|
$db->rollBack();
|
||||||
|
}
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error al actualizar usuario: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
78
admin/users/form.php
Executable file
78
admin/users/form.php
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
<div class="form-header">
|
||||||
|
<h2 id="formTitle">Nuevo Usuario</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="userForm">
|
||||||
|
<input type="hidden" id="userId" name="id">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Nombre de Usuario *</label>
|
||||||
|
<input type="text" id="username" name="username" required class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Contraseña <span id="passReq">*</span></label>
|
||||||
|
<input type="password" id="password" name="password" class="form-control">
|
||||||
|
<small id="passHelp" style="color: #666; display: none;">Dejar en blanco para mantener la actual</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="rol_id">Rol *</label>
|
||||||
|
<select id="rol_id" name="rol_id" required class="form-control" onchange="updatePermissionsView()">
|
||||||
|
<!-- Populated by JS -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Permisos</label>
|
||||||
|
<div id="permissionsList" class="permissions-grid">
|
||||||
|
<!-- Populated by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||||
|
<button type="button" onclick="closeModal()" class="btn btn-secondary">Cancelar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-group { margin-bottom: 15px; }
|
||||||
|
.form-group label { display: block; margin-bottom: 5px; font-weight: 600; }
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.permissions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.perm-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
479
admin/users/list.php
Executable file
479
admin/users/list.php
Executable file
@@ -0,0 +1,479 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../shared/auth/jwt.php';
|
||||||
|
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
|
||||||
|
if (!$userData) {
|
||||||
|
header('Location: /login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userData->rol !== 'Admin') {
|
||||||
|
die('Acceso denegado. Se requieren permisos de Administrador.');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!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>Gestión de Usuarios - Admin</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #2c3e50;
|
||||||
|
--secondary-color: #34495e;
|
||||||
|
--accent-color: #3498db;
|
||||||
|
--success-color: #27ae60;
|
||||||
|
--danger-color: #c0392b;
|
||||||
|
--light-bg: #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: var(--light-bg);
|
||||||
|
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: var(--primary-color); font-size: 24px; }
|
||||||
|
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover { background-color: #f8f9fa; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
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(--accent-color); color: white; }
|
||||||
|
.btn-primary:hover { background: #2980b9; }
|
||||||
|
|
||||||
|
.btn-secondary { background: #95a5a6; color: white; }
|
||||||
|
.btn-secondary:hover { background: #7f8c8d; }
|
||||||
|
|
||||||
|
.btn-danger { background: var(--danger-color); color: white; }
|
||||||
|
.btn-danger:hover { background: #a93226; }
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-admin { background: #e74c3c; color: white; }
|
||||||
|
.badge-editor { background: #3498db; color: white; }
|
||||||
|
.badge-user { background: #95a5a6; color: white; }
|
||||||
|
|
||||||
|
.actions { display: flex; gap: 10px; }
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
margin: 5% auto;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
position: relative;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-users-cog"></i> Gestión de Usuarios</h1>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<a href="/index.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-home"></i> Inicio
|
||||||
|
</a>
|
||||||
|
<button onclick="openCreateModal()" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Nuevo Usuario
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<div id="loading" style="text-align: center; padding: 20px;">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i> Cargando usuarios...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="usersTable" style="display: none;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Usuario</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Rol</th>
|
||||||
|
<th>Último Acceso</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="usersList">
|
||||||
|
<!-- JS will populate this -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Container -->
|
||||||
|
<div id="userModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeModal()">×</span>
|
||||||
|
<div id="modalBody">
|
||||||
|
<!-- Form will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', loadUsers);
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/admin/api/users/list.php');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const tbody = document.getElementById('usersList');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
data.users.forEach(user => {
|
||||||
|
const rolClass = user.rol_nombre === 'Admin' ? 'badge-admin' :
|
||||||
|
(user.rol_nombre === 'Editor' ? 'badge-editor' : 'badge-user');
|
||||||
|
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>
|
||||||
|
<div style="font-weight: 600;">${user.username}</div>
|
||||||
|
<div style="font-size: 12px; color: #999;">Creado: ${new Date(user.fecha_creacion).toLocaleDateString()}</div>
|
||||||
|
</td>
|
||||||
|
<td>${user.email || '-'}</td>
|
||||||
|
<td><span class="badge ${rolClass}">${user.rol_nombre}</span></td>
|
||||||
|
<td>${user.ultimo_acceso ? new Date(user.ultimo_acceso).toLocaleString() : 'Nunca'}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<button onclick="editUser(${user.id})" class="btn btn-secondary" style="padding: 6px 12px; font-size: 13px;">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
${user.id != <?php echo $userData->userId; ?> ? `
|
||||||
|
<button onclick="deleteUser(${user.id}, '${user.username}')" class="btn btn-danger" style="padding: 6px 12px; font-size: 13px;">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
document.getElementById('usersTable').style.display = 'table';
|
||||||
|
} else {
|
||||||
|
alert('Error cargando usuarios: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openCreateModal() {
|
||||||
|
const modal = document.getElementById('userModal');
|
||||||
|
const modalBody = document.getElementById('modalBody');
|
||||||
|
|
||||||
|
modalBody.innerHTML = '<div style="text-align: center;"><i class="fas fa-spinner fa-spin"></i> Cargando formulario...</div>';
|
||||||
|
modal.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('form.php');
|
||||||
|
const html = await response.text();
|
||||||
|
modalBody.innerHTML = html;
|
||||||
|
|
||||||
|
// Inicializar formulario para creación
|
||||||
|
initForm();
|
||||||
|
} catch (error) {
|
||||||
|
modalBody.innerHTML = '<p style="color: red;">Error cargando formulario</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editUser(userId) {
|
||||||
|
const modal = document.getElementById('userModal');
|
||||||
|
const modalBody = document.getElementById('modalBody');
|
||||||
|
|
||||||
|
modalBody.innerHTML = '<div style="text-align: center;"><i class="fas fa-spinner fa-spin"></i> Cargando datos...</div>';
|
||||||
|
modal.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cargar formulario base
|
||||||
|
const formResponse = await fetch('form.php');
|
||||||
|
const formHtml = await formResponse.text();
|
||||||
|
modalBody.innerHTML = formHtml;
|
||||||
|
|
||||||
|
// Cargar datos del usuario
|
||||||
|
const dataResponse = await fetch(`/admin/api/users/get.php?id=${userId}`);
|
||||||
|
const data = await dataResponse.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
initForm(data);
|
||||||
|
} else {
|
||||||
|
alert('Error cargando datos: ' + data.error);
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
modalBody.innerHTML = '<p style="color: red;">Error cargando datos</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser(userId, username) {
|
||||||
|
if (!confirm(`¿Estás seguro de eliminar al usuario "${username}"? Esta acción no se puede deshacer.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/admin/api/users/delete.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: userId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Usuario eliminado correctamente');
|
||||||
|
loadUsers();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('userModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('userModal');
|
||||||
|
if (event.target == modal) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form Logic
|
||||||
|
async function initForm(userData = null) {
|
||||||
|
let roles = [];
|
||||||
|
let allPermisos = [];
|
||||||
|
|
||||||
|
if (userData) {
|
||||||
|
// Modo Edición
|
||||||
|
document.getElementById('formTitle').textContent = 'Editar Usuario';
|
||||||
|
document.getElementById('userId').value = userData.user.id;
|
||||||
|
document.getElementById('username').value = userData.user.username;
|
||||||
|
document.getElementById('email').value = userData.user.email || '';
|
||||||
|
|
||||||
|
document.getElementById('passReq').style.display = 'none';
|
||||||
|
document.getElementById('password').required = false;
|
||||||
|
document.getElementById('passHelp').style.display = 'block';
|
||||||
|
|
||||||
|
roles = userData.roles;
|
||||||
|
allPermisos = userData.all_permisos;
|
||||||
|
|
||||||
|
renderRoles(roles, userData.user.rol_id);
|
||||||
|
renderPermissions(allPermisos, userData.user_permisos);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Modo Creación
|
||||||
|
try {
|
||||||
|
const response = await fetch('/admin/api/users/metadata.php');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
roles = data.roles;
|
||||||
|
allPermisos = data.all_permisos;
|
||||||
|
|
||||||
|
document.getElementById('password').required = true;
|
||||||
|
|
||||||
|
renderRoles(roles);
|
||||||
|
renderPermissions(allPermisos);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert('Error cargando metadatos del formulario');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Submit
|
||||||
|
const form = document.getElementById('userForm');
|
||||||
|
form.onsubmit = async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
// Collect checked permissions
|
||||||
|
const checkedPerms = [];
|
||||||
|
document.querySelectorAll('input[name="permisos[]"]:checked').forEach(cb => {
|
||||||
|
checkedPerms.push(cb.value);
|
||||||
|
});
|
||||||
|
data.permisos = checkedPerms;
|
||||||
|
|
||||||
|
const url = data.id ? '/admin/api/users/update.php' : '/admin/api/users/create.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(result.message);
|
||||||
|
closeModal();
|
||||||
|
loadUsers();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoles(roles, selectedId = null) {
|
||||||
|
const select = document.getElementById('rol_id');
|
||||||
|
select.innerHTML = '<option value="">Seleccionar Rol</option>';
|
||||||
|
roles.forEach(rol => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = rol.id;
|
||||||
|
option.textContent = rol.nombre;
|
||||||
|
if (selectedId && rol.id == selectedId) option.selected = true;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPermissions(allPermisos, userPerms = []) {
|
||||||
|
const container = document.getElementById('permissionsList');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Agrupar por módulo
|
||||||
|
const grouped = {};
|
||||||
|
allPermisos.forEach(p => {
|
||||||
|
if (!grouped[p.modulo]) grouped[p.modulo] = [];
|
||||||
|
grouped[p.modulo].push(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [modulo, perms] of Object.entries(grouped)) {
|
||||||
|
const groupTitle = document.createElement('div');
|
||||||
|
groupTitle.style.gridColumn = '1 / -1';
|
||||||
|
groupTitle.style.fontWeight = 'bold';
|
||||||
|
groupTitle.style.marginTop = '10px';
|
||||||
|
groupTitle.style.textTransform = 'capitalize';
|
||||||
|
groupTitle.textContent = modulo;
|
||||||
|
container.appendChild(groupTitle);
|
||||||
|
|
||||||
|
perms.forEach(p => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'perm-item';
|
||||||
|
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.name = 'permisos[]';
|
||||||
|
checkbox.value = p.id;
|
||||||
|
checkbox.id = 'perm_' + p.id;
|
||||||
|
|
||||||
|
if (userPerms.includes(p.id) || userPerms.includes(String(p.id))) {
|
||||||
|
checkbox.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.htmlFor = 'perm_' + p.id;
|
||||||
|
label.textContent = p.nombre;
|
||||||
|
label.title = p.descripcion || '';
|
||||||
|
|
||||||
|
div.appendChild(checkbox);
|
||||||
|
div.appendChild(label);
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePermissionsView() {
|
||||||
|
// Opcional: Auto-seleccionar permisos según rol
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
admin/users/php_errors.log
Executable file
8
admin/users/php_errors.log
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
[30-Nov-2025 21:41:53 UTC] PHP Warning: Undefined variable $userData in /var/www/html/bot/admin/users/form.php on line 115
|
||||||
|
[30-Nov-2025 21:41:53 UTC] PHP Warning: Attempt to read property "userId" on null in /var/www/html/bot/admin/users/form.php on line 115
|
||||||
|
[30-Nov-2025 21:41:55 UTC] PHP Warning: Undefined variable $userData in /var/www/html/bot/admin/users/form.php on line 115
|
||||||
|
[30-Nov-2025 21:41:55 UTC] PHP Warning: Attempt to read property "userId" on null in /var/www/html/bot/admin/users/form.php on line 115
|
||||||
|
[30-Nov-2025 21:42:00 UTC] PHP Warning: Undefined variable $userData in /var/www/html/bot/admin/users/form.php on line 115
|
||||||
|
[30-Nov-2025 21:42:00 UTC] PHP Warning: Attempt to read property "userId" on null in /var/www/html/bot/admin/users/form.php on line 115
|
||||||
|
[30-Nov-2025 21:42:28 UTC] PHP Warning: Undefined variable $userData in /var/www/html/bot/admin/users/form.php on line 115
|
||||||
|
[30-Nov-2025 21:42:28 UTC] PHP Warning: Attempt to read property "userId" on null in /var/www/html/bot/admin/users/form.php on line 115
|
||||||
2
bot.log
Executable file
2
bot.log
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
nohup: no se tendrá en cuenta la entrada
|
||||||
|
PHP Parse error: syntax error, unexpected token "catch", expecting ")" in /var/www/html/bot/bot_daemon.php on line 466
|
||||||
780
bot_daemon.20251201_002539.php
Executable file
780
bot_daemon.20251201_002539.php
Executable file
@@ -0,0 +1,780 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Discord Bot Daemon - Versión corregida
|
||||||
|
* Ejecutar con Supervisor: php bot_daemon_fixed.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Discord\Discord;
|
||||||
|
use Discord\Parts\Channel\Message;
|
||||||
|
use Discord\Parts\Interactions\Interaction;
|
||||||
|
use Discord\Builders\MessageBuilder;
|
||||||
|
use Discord\Parts\Embed\Embed;
|
||||||
|
use Discord\Parts\Guild\Guild;
|
||||||
|
use Discord\WebSockets\Event;
|
||||||
|
use Discord\Builders\Components\ActionRow;
|
||||||
|
use Discord\Builders\Components\Button;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// --- 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) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || strpos($line, '#') === 0) continue;
|
||||||
|
if (strpos($line, '=') === false) continue;
|
||||||
|
list($key, $value) = explode('=', $line, 2);
|
||||||
|
$k = trim($key);
|
||||||
|
$v = trim($value);
|
||||||
|
if ($k !== '') $_ENV[$k] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar el directorio de logs si no existe
|
||||||
|
if (!file_exists(__DIR__ . '/logs')) {
|
||||||
|
mkdir(__DIR__ . '/logs', 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para registrar interacciones y eventos en el log (GLOBAL)
|
||||||
|
$logInteraction = function($message, $data = []) {
|
||||||
|
$logMessage = date('[Y-m-d H:i:s] ') . $message . "\n";
|
||||||
|
if (!empty($data)) {
|
||||||
|
$logMessage .= "Datos: " . json_encode($data, JSON_PRETTY_PRINT) . "\n";
|
||||||
|
}
|
||||||
|
error_log($logMessage, 3, __DIR__ . '/logs/interaction_events.log');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- Configuración de opciones para DiscordPHP ---
|
||||||
|
$options = [
|
||||||
|
'token' => $_ENV['DISCORD_BOT_TOKEN'] ?? '',
|
||||||
|
|
||||||
|
'useTransportCompression' => false,
|
||||||
|
'intents' => \Discord\WebSockets\Intents::getDefaultIntents() |
|
||||||
|
\Discord\WebSockets\Intents::GUILD_MESSAGES |
|
||||||
|
\Discord\WebSockets\Intents::DIRECT_MESSAGES |
|
||||||
|
\Discord\WebSockets\Intents::MESSAGE_CONTENT |
|
||||||
|
\Discord\WebSockets\Intents::GUILD_MEMBERS,
|
||||||
|
'disabledEvents' => [
|
||||||
|
'TYPING_START',
|
||||||
|
'USER_SETTINGS_UPDATE',
|
||||||
|
'PRESENCE_UPDATE',
|
||||||
|
'VOICE_STATE_UPDATE'
|
||||||
|
],
|
||||||
|
// Deshabilitar el logger para evitar problemas
|
||||||
|
'logger' => null
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Inicializar el bot ---
|
||||||
|
$discord = new Discord($options);
|
||||||
|
|
||||||
|
// Variable global para la conexión a la base de datos
|
||||||
|
$db = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDBConnection();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "No se pudo conectar a la base de datos: " . $e->getMessage() . PHP_EOL;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Manejador de mensajes ---
|
||||||
|
$discord->on('MESSAGE_CREATE', function (Message $message, Discord $discord) use ($db, $logInteraction) {
|
||||||
|
try {
|
||||||
|
// Ignorar mensajes del propio bot
|
||||||
|
if ($message->author->bot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registrar el mensaje recibido
|
||||||
|
$logMessage = sprintf("[%s] Mensaje recibido - ID: %s, Autor: %s, Canal: %s, Contenido: %s\n",
|
||||||
|
date('Y-m-d H:i:s'),
|
||||||
|
$message->id,
|
||||||
|
$message->author->username,
|
||||||
|
$message->channel->name,
|
||||||
|
substr($message->content, 0, 100) . (strlen($message->content) > 100 ? '...' : '')
|
||||||
|
);
|
||||||
|
error_log($logMessage, 3, __DIR__ . '/logs/message_events.log');
|
||||||
|
|
||||||
|
// Verificar si el mensaje necesita traducción
|
||||||
|
$needsTrans = needsTranslation($message->content);
|
||||||
|
error_log("¿Necesita traducción? " . ($needsTrans ? 'Sí' : 'No') . "\n", 3, __DIR__ . '/logs/message_events.log');
|
||||||
|
|
||||||
|
if ($needsTrans) {
|
||||||
|
try {
|
||||||
|
// Crear un botón de traducción con icono y un label de espacio en blanco
|
||||||
|
$button = new Button(Button::STYLE_PRIMARY);
|
||||||
|
$button->setCustomId("translate_{$message->id}") // customId solo con message.id
|
||||||
|
->setLabel("\u{200b}") // Espacio en blanco para que el botón sea válido
|
||||||
|
->setEmoji('🌍');
|
||||||
|
|
||||||
|
// Crear una fila de acción con el botón
|
||||||
|
$actionRow = new ActionRow();
|
||||||
|
$actionRow->addComponent($button);
|
||||||
|
|
||||||
|
// Crear el mensaje de respuesta (sin texto, solo el botón)
|
||||||
|
$replyMessage = MessageBuilder::new()
|
||||||
|
->addComponent($actionRow);
|
||||||
|
|
||||||
|
// Enviar el mensaje con el botón (visible para todos)
|
||||||
|
$message->channel->sendMessage($replyMessage)
|
||||||
|
->then(
|
||||||
|
function() use ($message) {
|
||||||
|
error_log("Botón de traducción enviado para el mensaje: {$message->id} (visible para todos)\n", 3, __DIR__ . '/logs/message_events.log');
|
||||||
|
},
|
||||||
|
function($error) {
|
||||||
|
error_log("Error al enviar botón (visible para todos): " . $error->getMessage() . "\n", 3, __DIR__ . '/logs/translation_errors.log');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("Error en el manejador de mensajes: " . $e->getMessage() . "\n", 3, __DIR__ . '/logs/translation_errors.log');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("Error en el manejador de mensajes (nivel superior): " . $e->getMessage() . "\n", 3, __DIR__ . '/logs/translation_errors.log');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- Manejador para nuevos miembros ---
|
||||||
|
$discord->on('GUILD_MEMBER_ADD', function (\Discord\Parts\User\Member $member, Discord $discord) use ($db, $logInteraction) {
|
||||||
|
$logInteraction("Nuevo miembro detectado", ['user_id' => $member->id, 'username' => $member->username, 'guild_id' => $member->guild_id]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$welcomeConfig = getWelcomeConfig($db);
|
||||||
|
|
||||||
|
if (!$welcomeConfig || !(bool)$welcomeConfig['activo']) {
|
||||||
|
$logInteraction("Bienvenida de Discord deshabilitada o no configurada.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el usuario ya está registrado en nuestra DB
|
||||||
|
if (isDiscordUserRegistered($db, $member->id)) {
|
||||||
|
$logInteraction("Miembro ya registrado, no se envía bienvenida para evitar duplicados.", ['user_id' => $member->id]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la configuración indica registrar al usuario, hacerlo aquí
|
||||||
|
if ((bool)$welcomeConfig['registrar_usuario']) {
|
||||||
|
registerDiscordRecipient($db, $member->id, 'usuario', $member->username, $member->username);
|
||||||
|
$logInteraction("Nuevo miembro registrado en DB.", ['user_id' => $member->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener el canal de bienvenida
|
||||||
|
$guild = $discord->guilds->get('id', $member->guild_id);
|
||||||
|
if (!$guild) {
|
||||||
|
$logInteraction("No se pudo obtener el gremio para enviar la bienvenida.", ['guild_id' => $member->guild_id]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$channel = $guild->channels->get('id', $welcomeConfig['canal_id']);
|
||||||
|
if (!$channel) {
|
||||||
|
$logInteraction("No se pudo encontrar el canal de bienvenida configurado.", ['channel_id' => $welcomeConfig['canal_id']]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construir el mensaje de bienvenida
|
||||||
|
$messageContent = $welcomeConfig['texto'];
|
||||||
|
// Reemplazar placeholders y limpiar HTML
|
||||||
|
$messageContent = str_replace('{usuario}', '<@'.$member->id.'>', $messageContent);
|
||||||
|
$messageContent = strip_tags($messageContent); // Eliminar HTML si existe
|
||||||
|
|
||||||
|
$messageBuilder = MessageBuilder::new()
|
||||||
|
->setContent($messageContent);
|
||||||
|
|
||||||
|
// Añadir imagen si está configurada
|
||||||
|
if (!empty($welcomeConfig['imagen_ruta'])) {
|
||||||
|
// Asumo que 'imagen_ruta' es una URL completa o una ruta accesible por Discord
|
||||||
|
// Si es una ruta local, necesitarías subirla a Discord primero o servirla desde una URL
|
||||||
|
// Por simplicidad, si es una ruta local, la ignoraremos por ahora a menos que se especifique cómo servirla.
|
||||||
|
// Si la ruta ya es una URL pública, se puede añadir directamente
|
||||||
|
if (filter_var($welcomeConfig['imagen_ruta'], FILTER_VALIDATE_URL)) {
|
||||||
|
$messageBuilder->setImage($welcomeConfig['imagen_ruta']);
|
||||||
|
} elseif (!empty($_ENV['CAOS_BASE_URL']) && strpos($welcomeConfig['imagen_ruta'], '/gallery/uploads/') === 0) {
|
||||||
|
// Si es una ruta relativa de galería, construir la URL completa
|
||||||
|
$fullImageUrl = rtrim($_ENV['CAOS_BASE_URL'], '/') . $welcomeConfig['imagen_ruta'];
|
||||||
|
$messageBuilder->setImage($fullImageUrl);
|
||||||
|
$logInteraction("Imagen de bienvenida construida como URL.", ['url' => $fullImageUrl]);
|
||||||
|
} else {
|
||||||
|
$logInteraction("Ruta de imagen no válida o no URL, se ignorará.", ['ruta' => $welcomeConfig['imagen_ruta']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añadir botones de idioma
|
||||||
|
$idiomasHabilitados = json_decode($welcomeConfig['idiomas_habilitados'], true) ?? [];
|
||||||
|
if (!empty($idiomasHabilitados)) {
|
||||||
|
$actionRow = new ActionRow();
|
||||||
|
$langLabels = [
|
||||||
|
'es' => 'Español',
|
||||||
|
'en' => 'English',
|
||||||
|
'pt' => 'Português'
|
||||||
|
];
|
||||||
|
foreach ($idiomasHabilitados as $langCode) {
|
||||||
|
$label = $langLabels[$langCode] ?? strtoupper($langCode);
|
||||||
|
$emoji = ''; // Puedes añadir emojis específicos si los tienes
|
||||||
|
if ($langCode === 'es') $emoji = '🇪🇸';
|
||||||
|
if ($langCode === 'en') $emoji = '🇬🇧';
|
||||||
|
if ($langCode === 'pt') $emoji = '🇵🇹';
|
||||||
|
|
||||||
|
$button = Button::new(Button::STYLE_PRIMARY)
|
||||||
|
->setLabel($label)
|
||||||
|
->setCustomId("lang_select_{$langCode}");
|
||||||
|
if (!empty($emoji)) {
|
||||||
|
$button->setEmoji($emoji);
|
||||||
|
}
|
||||||
|
$actionRow->addComponent($button);
|
||||||
|
}
|
||||||
|
$messageBuilder->addComponent($actionRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar el mensaje de bienvenida (no efímero)
|
||||||
|
$channel->sendMessage($messageBuilder)
|
||||||
|
->then(function ($message) use ($member, $logInteraction) {
|
||||||
|
$logInteraction("Mensaje de bienvenida enviado con éxito.", ['user_id' => $member->id, 'message_id' => $message->id]);
|
||||||
|
})
|
||||||
|
->otherwise(function ($error) use ($member, $logInteraction) {
|
||||||
|
$logInteraction("Error al enviar mensaje de bienvenida.", ['user_id' => $member->id, 'error' => $error->getMessage()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$logInteraction("Error en manejador GUILD_MEMBER_ADD.", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Manejador de interacciones (botones, etc.) ---
|
||||||
|
$discord->on('INTERACTION_CREATE', function (Interaction $interaction, Discord $discord) use ($db, $logInteraction) {
|
||||||
|
// Loguear cada interacción recibida
|
||||||
|
$logInteraction("Interacción recibida en INTERACTION_CREATE", [
|
||||||
|
'id' => $interaction->id ?? 'N/A',
|
||||||
|
'type' => $interaction->type ?? 'N/A',
|
||||||
|
'custom_id' => $interaction->data->custom_id ?? 'N/A',
|
||||||
|
'user_id' => $interaction->user->id ?? 'N/A',
|
||||||
|
'command_name' => $interaction->data->name ?? 'N/A' // Añadir para comandos slash
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Manejar comandos de aplicación (slash commands)
|
||||||
|
if ($interaction->type === Interaction::TYPE_APPLICATION_COMMAND) {
|
||||||
|
$commandName = $interaction->data->name;
|
||||||
|
$logInteraction("Comando de aplicación recibido", ['command' => $commandName]);
|
||||||
|
|
||||||
|
if ($commandName === 'start') {
|
||||||
|
$welcomeConfig = getWelcomeConfig($db); // Obtener la configuración de bienvenida
|
||||||
|
$logInteraction("Comando /start: Obtenida configuración de bienvenida.", ['config' => $welcomeConfig]);
|
||||||
|
|
||||||
|
$messageContent = "¡Hola! Selecciona tu idioma preferido para interactuar con el bot."; // Default
|
||||||
|
$imageUrl = null;
|
||||||
|
$idiomasHabilitados = [];
|
||||||
|
|
||||||
|
if ($welcomeConfig && (bool)$welcomeConfig['activo']) {
|
||||||
|
$messageContent = $welcomeConfig['texto'];
|
||||||
|
// Reemplazar placeholders y limpiar HTML
|
||||||
|
$messageContent = str_replace('{usuario}', '<@'.$interaction->user->id.'>', $messageContent);
|
||||||
|
$messageContent = strip_tags($messageContent); // Eliminar HTML si existe
|
||||||
|
|
||||||
|
// Añadir imagen si está configurada
|
||||||
|
if (!empty($welcomeConfig['imagen_ruta'])) {
|
||||||
|
if (filter_var($welcomeConfig['imagen_ruta'], FILTER_VALIDATE_URL)) {
|
||||||
|
$imageUrl = $welcomeConfig['imagen_ruta'];
|
||||||
|
} elseif (!empty($_ENV['CAOS_BASE_URL']) && strpos($welcomeConfig['imagen_ruta'], '/gallery/uploads/') === 0) {
|
||||||
|
$imageUrl = rtrim($_ENV['CAOS_BASE_URL'], '/') . $welcomeConfig['imagen_ruta'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idiomasHabilitados = json_decode($welcomeConfig['idiomas_habilitados'], true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mensaje de bienvenida (texto y posible imagen)
|
||||||
|
$welcomeMessageBuilder = MessageBuilder::new()
|
||||||
|
->setContent($messageContent)
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL);
|
||||||
|
|
||||||
|
if ($imageUrl) {
|
||||||
|
$welcomeMessageBuilder->setImage($imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar el mensaje de bienvenida efímero como respuesta inicial
|
||||||
|
$interaction->respondWithMessage($welcomeMessageBuilder, true);
|
||||||
|
$logInteraction("Comando /start manejado: mensaje de bienvenida enviado (respondWithMessage efímero).", ['content' => $messageContent, 'image' => $imageUrl]);
|
||||||
|
|
||||||
|
// Crear los botones de selección de idioma
|
||||||
|
$buttons = [];
|
||||||
|
$langLabels = [
|
||||||
|
'es' => 'Español',
|
||||||
|
'en' => 'English',
|
||||||
|
'pt' => 'Português'
|
||||||
|
];
|
||||||
|
foreach ($idiomasHabilitados as $langCode) {
|
||||||
|
$label = $langLabels[$langCode] ?? strtoupper($langCode);
|
||||||
|
$emoji = '';
|
||||||
|
if ($langCode === 'es') $emoji = '🇪🇸';
|
||||||
|
if ($langCode === 'en') $emoji = '🇬🇧';
|
||||||
|
if ($langCode === 'pt') $emoji = '🇵🇹';
|
||||||
|
|
||||||
|
$button = Button::new(Button::STYLE_PRIMARY)
|
||||||
|
->setLabel($label)
|
||||||
|
->setCustomId("lang_select_{$langCode}");
|
||||||
|
if (!empty($emoji)) {
|
||||||
|
$button->setEmoji($emoji);
|
||||||
|
}
|
||||||
|
$buttons[] = $button;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actionRow = new ActionRow();
|
||||||
|
foreach ($buttons as $button) {
|
||||||
|
$actionRow->addComponent($button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar los botones como un follow-up message efímero
|
||||||
|
$buttonsMessage = MessageBuilder::new()
|
||||||
|
->addComponent($actionRow)
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL);
|
||||||
|
|
||||||
|
$interaction->sendFollowUpMessage($buttonsMessage);
|
||||||
|
$logInteraction("Comando /start manejado: botones de idioma enviados (sendFollowUpMessage efímero).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Puedes añadir más comandos slash aquí
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejar componentes de mensaje (botones, etc.)
|
||||||
|
if ($interaction->type === Interaction::TYPE_MESSAGE_COMPONENT) {
|
||||||
|
$customId = $interaction->data->custom_id ?? '';
|
||||||
|
|
||||||
|
// Manejar botón de traducción
|
||||||
|
if (strpos($customId, 'translate_') === 0) {
|
||||||
|
$logInteraction("Manejando botón de traducción", [
|
||||||
|
'custom_id' => $customId,
|
||||||
|
'message_id' => str_replace('translate_', '', $customId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Asegurarse de que la interacción se reconozca
|
||||||
|
$interaction->acknowledge(true);
|
||||||
|
|
||||||
|
// Llamar al manejador de traducción
|
||||||
|
handleTranslationButton($interaction, $db);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$logInteraction("Error al manejar el botón de traducción", [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Intentar notificar al usuario del error
|
||||||
|
try {
|
||||||
|
$interaction->sendMessage(
|
||||||
|
MessageBuilder::new()
|
||||||
|
->setContent("❌ Ocurrió un error al procesar la traducción. Por favor, inténtalo de nuevo.")
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e2) {
|
||||||
|
$logInteraction("Error al notificar al usuario", [
|
||||||
|
'error' => $e2->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejar selección de idioma
|
||||||
|
if (strpos($customId, 'lang_select_') === 0) {
|
||||||
|
$logInteraction("Entrando a lang_select_ handler", ['custom_id' => $customId]);
|
||||||
|
// Primero, reconocer la interacción para evitar el timeout de Discord
|
||||||
|
$interaction->acknowledge(true); // true para una respuesta efímera
|
||||||
|
$logInteraction("Interacción acknowledge(true) ejecutada.");
|
||||||
|
|
||||||
|
$langCode = substr($customId, strlen('lang_select_'));
|
||||||
|
$user = $interaction->user;
|
||||||
|
error_log("Usuario {$user->username} seleccionó idioma: {$langCode}" . PHP_EOL, 3, __DIR__ . '/logs/interaction_events.log');
|
||||||
|
|
||||||
|
// Antes de la DB:
|
||||||
|
$logInteraction("Intentando registrar/actualizar destinatario en BD.", ['user_id' => $user->id, 'lang_code' => $langCode]);
|
||||||
|
registerDiscordRecipient($db, $user->id, 'usuario', $user->username, $user->username);
|
||||||
|
$stmt = $db->prepare("UPDATE destinatarios_discord SET idioma_detectado = ? WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$langCode, $user->id]);
|
||||||
|
// Después de la DB:
|
||||||
|
$logInteraction("Destinatario registrado/actualizado en BD.");
|
||||||
|
|
||||||
|
error_log("Preferencia de idioma guardada en BD." . PHP_EOL, 3, __DIR__ . '/logs/interaction_events.log');
|
||||||
|
$interaction->sendFollowUpMessage( // <-- CAMBIO AQUÍ
|
||||||
|
MessageBuilder::new()
|
||||||
|
->setContent("✅ Idioma seleccionado: " . strtoupper($langCode))
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL)
|
||||||
|
);
|
||||||
|
$logInteraction("sendFollowUpMessage ejecutado en lang_select_ handler.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("Error en INTERACTION_CREATE: " . $e->getMessage());
|
||||||
|
|
||||||
|
// Intentar responder con un mensaje de error si es posible
|
||||||
|
if (isset($interaction) && $interaction instanceof Interaction) {
|
||||||
|
try {
|
||||||
|
$interaction->respondWithMessage(
|
||||||
|
MessageBuilder::new()
|
||||||
|
->setContent("❌ Ocurrió un error al procesar tu solicitud.")
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL)
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e2) {
|
||||||
|
// Ignorar errores al intentar responder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Manejador de errores de WebSocket ---
|
||||||
|
$discord->on('error', function ($error, $ws) use ($discord) {
|
||||||
|
echo "Error de WebSocket: " . $error . PHP_EOL;
|
||||||
|
// Intentar reconectar después de 5 segundos
|
||||||
|
$discord->getLoop()->addTimer(5, function() use ($discord) {
|
||||||
|
echo "Intentando reconectar..." . PHP_EOL;
|
||||||
|
$discord->getLoop()->stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Función para detectar si un mensaje necesita traducción ---
|
||||||
|
function needsTranslation($text) {
|
||||||
|
// Si el texto está vacío o es muy corto, no necesita traducción
|
||||||
|
if (empty(trim($text)) || strlen(trim($text)) < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lista de palabras comunes en español
|
||||||
|
$spanishWords = [
|
||||||
|
'hola', 'buenos', 'días', 'tardes', 'noche', 'gracias', 'por favor',
|
||||||
|
'adiós', 'hasta luego', 'sí', 'no', 'porque', 'pero', 'y', 'o',
|
||||||
|
'el', 'la', 'los', 'las', 'un', 'una', 'unos', 'unas', 'es', 'son',
|
||||||
|
'soy', 'eres', 'somos', 'sois', 'estoy', 'estás', 'está', 'estamos',
|
||||||
|
'estáis', 'están', 'tengo', 'tienes', 'tiene', 'tenemos', 'tenéis',
|
||||||
|
'tienen', 'voy', 'vas', 'va', 'vamos', 'vais', 'van', 'hacer', 'hecho',
|
||||||
|
'día', 'año', 'vez', 'tiempo', 'casa', 'agua', 'comida', 'bebida'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Convertir a minúsculas y eliminar signos de puntuación
|
||||||
|
$cleanText = preg_replace('/[^\p{L}\s]/u', '', mb_strtolower($text));
|
||||||
|
$words = preg_split('/\s+/', $cleanText);
|
||||||
|
|
||||||
|
// Contar palabras en español
|
||||||
|
$spanishWordCount = 0;
|
||||||
|
foreach ($words as $word) {
|
||||||
|
if (in_array($word, $spanishWords)) {
|
||||||
|
$spanishWordCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si menos del 30% de las palabras son en español, asumir que necesita traducción
|
||||||
|
$needsTranslation = ($spanishWordCount / max(1, count($words))) < 0.3;
|
||||||
|
|
||||||
|
error_log("Análisis de traducción - Palabras: " . count($words) . ", Español: $spanishWordCount, Necesita traducción: " . ($needsTranslation ? 'Sí' : 'No'));
|
||||||
|
|
||||||
|
return $needsTranslation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para traducir texto usando LibreTranslate ---
|
||||||
|
function translateText($text, $targetLang) {
|
||||||
|
$libreTranslateUrl = $_ENV['LIBRETRANSLATE_URL'] ?? 'http://localhost:5000';
|
||||||
|
$url = rtrim($libreTranslateUrl, '/') . '/translate';
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'q' => $text,
|
||||||
|
'source' => 'auto', // Detección automática del idioma
|
||||||
|
'target' => $targetLang
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false || $httpCode !== 200) {
|
||||||
|
error_log("Error en la traducción. Código HTTP: $httpCode. Respuesta: " . $response);
|
||||||
|
return "Error al traducir el mensaje. Por favor, inténtalo de nuevo más tarde.";
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
return $result['translatedText'] ?? $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para manejar el botón de traducción ---
|
||||||
|
function handleTranslationButton(Interaction $interaction, PDO $db) {
|
||||||
|
try {
|
||||||
|
// Verificar que la interacción sea válida
|
||||||
|
if (!$interaction instanceof \Discord\Parts\Interactions\Interaction) {
|
||||||
|
$logInteraction("handleTranslationButton: Interacción no válida", ['type' => gettype($interaction)]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Verificar que tengamos un ID de mensaje
|
||||||
|
if (empty($interaction->data->custom_id)) {
|
||||||
|
$logInteraction("handleTranslationButton: No se pudo obtener el ID del mensaje de la interacción", [
|
||||||
|
'interaction' => [
|
||||||
|
'id' => $interaction->id ?? 'N/A',
|
||||||
|
'type' => $interaction->type ?? 'N/A',
|
||||||
|
'data' => $interaction->data ?? 'N/A'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
throw new \Exception("No se pudo obtener el ID del mensaje de la interacción");
|
||||||
|
}
|
||||||
|
// Extraer el ID del mensaje original del custom_id
|
||||||
|
$customId = $interaction->data->custom_id;
|
||||||
|
$logError("handleTranslationButton: customId recibido (RAW): " . $customId, ['customId' => $customId]); // Nuevo log para el customId RAW
|
||||||
|
$logError("handleTranslationButton: customId recibido (desde variable): " . $customId, ['customId' => $customId]); // Mantener el log anterior para comparación
|
||||||
|
|
||||||
|
// El customId ahora es "translate_{messageId}"
|
||||||
|
$parts = explode('_', $customId);
|
||||||
|
$logError("handleTranslationButton: Resultado de explode: ", ['parts' => $parts, 'count' => count($parts)]);
|
||||||
|
if (count($parts) === 2 && $parts[0] === 'translate') {
|
||||||
|
$messageId = $parts[1];
|
||||||
|
} else {
|
||||||
|
$logError("handleTranslationButton: custom_id no tiene el formato esperado (translate_MESSAGEID).", ['customId' => $customId, 'parts' => $parts, 'count' => count($parts)]); $messageId = $parts[1];
|
||||||
|
$logInteraction("handleTranslationButton: custom_id no tiene el formato esperado (translate_MESSAGEID).", [
|
||||||
|
'customId' => $customId, 'parts' => $parts, 'count' => count($parts)]);
|
||||||
|
try {
|
||||||
|
return $interaction->sendFollowUpMessage(
|
||||||
|
MessageBuilder::new()
|
||||||
|
->setContent("❌ Error interno: Formato de botón de traducción incorrecto.")
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL)
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$logInteraction("Error al enviar mensaje de error por formato de botón.", ['error' => $e->getMessage()]
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener el idioma del usuario que presionó el botón
|
||||||
|
$targetLang = getUserPreferredLanguage($db, $interaction->user->id);
|
||||||
|
if (empty($targetLang)) {
|
||||||
|
$targetLang = 'en'; // Default a inglés si no hay idioma preferido
|
||||||
|
$logInteraction("handleTranslationButton: Usuario {$interaction->user->id} no tiene idioma preferido, usando 'en'.");
|
||||||
|
}
|
||||||
|
$logInteraction("handleTranslationButton: Usuario {$interaction->user->username} (${interaction->user->id}) solicitó traducción a: {$targetLang}.");
|
||||||
|
|
||||||
|
|
||||||
|
// Obtener el canal donde ocurrió la interacción
|
||||||
|
$channel = $interaction->channel;
|
||||||
|
|
||||||
|
// Obtener el mensaje original
|
||||||
|
$message = null;
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Intentar obtener el mensaje directamente del canal
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Intentando obtener mensaje con ID: {$messageId} del canal: {$channel->id}.");
|
||||||
|
|
||||||
|
$message = $channel->getMessage($messageId);
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Resultado de getMessage: " . ($message ? "Obtenido de caché" : "No en caché"));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Si no se pudo obtener, intentar de otra manera
|
||||||
|
|
||||||
|
if (!$message) {
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: No se pudo obtener el mensaje directamente de caché, intentando con fetch a la API...");
|
||||||
|
|
||||||
|
$message = $channel->fetchMessage($messageId);
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Resultado de fetchMessage: " . ($message ? "Obtenido de API" : "No obtenido de API"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
|
||||||
|
$error = $e->getMessage();
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Error al obtener el mensaje", [
|
||||||
|
|
||||||
|
'error' => $error,
|
||||||
|
|
||||||
|
'messageId' => $messageId,
|
||||||
|
|
||||||
|
'channelId' => $channel->id ?? 'N/A'
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!$message) {
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: No se pudo encontrar el mensaje original", [
|
||||||
|
|
||||||
|
'messageId' => $messageId,
|
||||||
|
|
||||||
|
'channelId' => $channel->id ?? 'N/A',
|
||||||
|
|
||||||
|
'error' => $error ?? 'Mensaje nulo sin error específico'
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return $interaction->sendFollowUpMessage(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
MessageBuilder::new()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
->setContent("❌ No se pudo encontrar el mensaje original (ID: {$messageId}). Puede haber
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
sido eliminado o ser demasiado antiguo.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$logInteraction("Error al enviar mensaje de error al usuario", [
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Mensaje original obtenido. Contenido: " . substr($message->content, 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
, 50) . "...");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Traducir el mensaje al idioma del usuario que presionó el botón
|
||||||
|
$translatedText = translateText($message->content, $targetLang);
|
||||||
|
$logInteraction("handleTranslationButton: Mensaje traducido. Contenido: " . substr($translatedText, 0, 50) . "...");
|
||||||
|
|
||||||
|
// Crear el mensaje de respuesta
|
||||||
|
$langLabels = [
|
||||||
|
'es' => 'español',
|
||||||
|
'en' => 'inglés',
|
||||||
|
'pt' => 'portugués'
|
||||||
|
];
|
||||||
|
$displayLang = $langLabels[$targetLang] ?? $targetLang; // Fallback a usar el código si no está en la lista
|
||||||
|
|
||||||
|
$response = MessageBuilder::new()
|
||||||
|
->setContent("**Mensaje traducido al {$displayLang}**:\n\n" . $translatedText)
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL);
|
||||||
|
|
||||||
|
// Enviar la respuesta como un follow-up
|
||||||
|
$interaction->sendFollowUpMessage($response);
|
||||||
|
|
||||||
|
error_log("handleTranslationButton: Traducción enviada para el mensaje: " . $message->id);
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$logInteraction("Error en handleTranslationButton (nivel superior): " . $e->getMessage(), ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||||
|
try {
|
||||||
|
$interaction->respondWithMessage(
|
||||||
|
MessageBuilder::new()->setContent("Ocurrió un error al traducir el mensaje."),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e2) {
|
||||||
|
// Ignorar errores al responder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- Función para obtener la configuración de bienvenida de Discord ---
|
||||||
|
function getWelcomeConfig(PDO $db): ?array {
|
||||||
|
$stmt = $db->query("
|
||||||
|
SELECT b.*, g.ruta as imagen_ruta
|
||||||
|
FROM bienvenida_discord b
|
||||||
|
LEFT JOIN gallery g ON b.imagen_id = g.id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$config = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $config ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para verificar si un usuario de Discord ya está registrado ---
|
||||||
|
function isDiscordUserRegistered(PDO $db, string $discordId): bool {
|
||||||
|
$stmt = $db->prepare("SELECT COUNT(*) FROM destinatarios_discord WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$discordId]);
|
||||||
|
return $stmt->fetchColumn() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para obtener conexión a la base de datos ---
|
||||||
|
function getDBConnection(): PDO {
|
||||||
|
$host = $_ENV['DB_HOST'] ?? '127.0.0.1';
|
||||||
|
$db = $_ENV['DB_NAME'] ?? 'db';
|
||||||
|
$user = $_ENV['DB_USER'] ?? 'root';
|
||||||
|
$pass = $_ENV['DB_PASS'] ?? '';
|
||||||
|
$port = $_ENV['DB_PORT'] ?? '3306';
|
||||||
|
$charset = 'utf8mb4';
|
||||||
|
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset";
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
return new PDO($dsn, $user, $pass, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para registrar destinatarios de Discord ---
|
||||||
|
function registerDiscordRecipient(PDO $db, $discordId, $type, $name, $username = null) {
|
||||||
|
$stmt = $db->prepare("INSERT INTO destinatarios_discord (discord_id, tipo, nombre, username)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE nombre = VALUES(nombre), username = VALUES(username)");
|
||||||
|
$stmt->execute([$discordId, $type, $name, $username]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Iniciar el bot ---
|
||||||
|
echo "Iniciando bot..." . PHP_EOL;
|
||||||
|
$discord->run();
|
||||||
|
|
||||||
|
// --- Manejar cierre limpio ---
|
||||||
|
register_shutdown_function(function() use ($discord, $db) {
|
||||||
|
echo "Cerrando el bot..." . PHP_EOL;
|
||||||
|
if ($db) {
|
||||||
|
$db = null;
|
||||||
|
}
|
||||||
|
$discord->close();
|
||||||
|
});
|
||||||
780
bot_daemon.backup.php
Executable file
780
bot_daemon.backup.php
Executable file
@@ -0,0 +1,780 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Discord Bot Daemon - Versión corregida
|
||||||
|
* Ejecutar con Supervisor: php bot_daemon_fixed.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Discord\Discord;
|
||||||
|
use Discord\Parts\Channel\Message;
|
||||||
|
use Discord\Parts\Interactions\Interaction;
|
||||||
|
use Discord\Builders\MessageBuilder;
|
||||||
|
use Discord\Parts\Embed\Embed;
|
||||||
|
use Discord\Parts\Guild\Guild;
|
||||||
|
use Discord\WebSockets\Event;
|
||||||
|
use Discord\Builders\Components\ActionRow;
|
||||||
|
use Discord\Builders\Components\Button;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// --- 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) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || strpos($line, '#') === 0) continue;
|
||||||
|
if (strpos($line, '=') === false) continue;
|
||||||
|
list($key, $value) = explode('=', $line, 2);
|
||||||
|
$k = trim($key);
|
||||||
|
$v = trim($value);
|
||||||
|
if ($k !== '') $_ENV[$k] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurar el directorio de logs si no existe
|
||||||
|
if (!file_exists(__DIR__ . '/logs')) {
|
||||||
|
mkdir(__DIR__ . '/logs', 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para registrar interacciones y eventos en el log (GLOBAL)
|
||||||
|
$logInteraction = function($message, $data = []) {
|
||||||
|
$logMessage = date('[Y-m-d H:i:s] ') . $message . "\n";
|
||||||
|
if (!empty($data)) {
|
||||||
|
$logMessage .= "Datos: " . json_encode($data, JSON_PRETTY_PRINT) . "\n";
|
||||||
|
}
|
||||||
|
error_log($logMessage, 3, __DIR__ . '/logs/interaction_events.log');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- Configuración de opciones para DiscordPHP ---
|
||||||
|
$options = [
|
||||||
|
'token' => $_ENV['DISCORD_BOT_TOKEN'] ?? '',
|
||||||
|
|
||||||
|
'useTransportCompression' => false,
|
||||||
|
'intents' => \Discord\WebSockets\Intents::getDefaultIntents() |
|
||||||
|
\Discord\WebSockets\Intents::GUILD_MESSAGES |
|
||||||
|
\Discord\WebSockets\Intents::DIRECT_MESSAGES |
|
||||||
|
\Discord\WebSockets\Intents::MESSAGE_CONTENT |
|
||||||
|
\Discord\WebSockets\Intents::GUILD_MEMBERS,
|
||||||
|
'disabledEvents' => [
|
||||||
|
'TYPING_START',
|
||||||
|
'USER_SETTINGS_UPDATE',
|
||||||
|
'PRESENCE_UPDATE',
|
||||||
|
'VOICE_STATE_UPDATE'
|
||||||
|
],
|
||||||
|
// Deshabilitar el logger para evitar problemas
|
||||||
|
'logger' => null
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Inicializar el bot ---
|
||||||
|
$discord = new Discord($options);
|
||||||
|
|
||||||
|
// Variable global para la conexión a la base de datos
|
||||||
|
$db = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDBConnection();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "No se pudo conectar a la base de datos: " . $e->getMessage() . PHP_EOL;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Manejador de mensajes ---
|
||||||
|
$discord->on('MESSAGE_CREATE', function (Message $message, Discord $discord) use ($db, $logInteraction) {
|
||||||
|
try {
|
||||||
|
// Ignorar mensajes del propio bot
|
||||||
|
if ($message->author->bot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registrar el mensaje recibido
|
||||||
|
$logMessage = sprintf("[%s] Mensaje recibido - ID: %s, Autor: %s, Canal: %s, Contenido: %s\n",
|
||||||
|
date('Y-m-d H:i:s'),
|
||||||
|
$message->id,
|
||||||
|
$message->author->username,
|
||||||
|
$message->channel->name,
|
||||||
|
substr($message->content, 0, 100) . (strlen($message->content) > 100 ? '...' : '')
|
||||||
|
);
|
||||||
|
error_log($logMessage, 3, __DIR__ . '/logs/message_events.log');
|
||||||
|
|
||||||
|
// Verificar si el mensaje necesita traducción
|
||||||
|
$needsTrans = needsTranslation($message->content);
|
||||||
|
error_log("¿Necesita traducción? " . ($needsTrans ? 'Sí' : 'No') . "\n", 3, __DIR__ . '/logs/message_events.log');
|
||||||
|
|
||||||
|
if ($needsTrans) {
|
||||||
|
try {
|
||||||
|
// Crear un botón de traducción con icono y un label de espacio en blanco
|
||||||
|
$button = new Button(Button::STYLE_PRIMARY);
|
||||||
|
$button->setCustomId("translate_{$message->id}") // customId solo con message.id
|
||||||
|
->setLabel("\u{200b}") // Espacio en blanco para que el botón sea válido
|
||||||
|
->setEmoji('🌍');
|
||||||
|
|
||||||
|
// Crear una fila de acción con el botón
|
||||||
|
$actionRow = new ActionRow();
|
||||||
|
$actionRow->addComponent($button);
|
||||||
|
|
||||||
|
// Crear el mensaje de respuesta (sin texto, solo el botón)
|
||||||
|
$replyMessage = MessageBuilder::new()
|
||||||
|
->addComponent($actionRow);
|
||||||
|
|
||||||
|
// Enviar el mensaje con el botón (visible para todos)
|
||||||
|
$message->channel->sendMessage($replyMessage)
|
||||||
|
->then(
|
||||||
|
function() use ($message) {
|
||||||
|
error_log("Botón de traducción enviado para el mensaje: {$message->id} (visible para todos)\n", 3, __DIR__ . '/logs/message_events.log');
|
||||||
|
},
|
||||||
|
function($error) {
|
||||||
|
error_log("Error al enviar botón (visible para todos): " . $error->getMessage() . "\n", 3, __DIR__ . '/logs/translation_errors.log');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("Error en el manejador de mensajes: " . $e->getMessage() . "\n", 3, __DIR__ . '/logs/translation_errors.log');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("Error en el manejador de mensajes (nivel superior): " . $e->getMessage() . "\n", 3, __DIR__ . '/logs/translation_errors.log');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- Manejador para nuevos miembros ---
|
||||||
|
$discord->on('GUILD_MEMBER_ADD', function (\Discord\Parts\User\Member $member, Discord $discord) use ($db, $logInteraction) {
|
||||||
|
$logInteraction("Nuevo miembro detectado", ['user_id' => $member->id, 'username' => $member->username, 'guild_id' => $member->guild_id]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$welcomeConfig = getWelcomeConfig($db);
|
||||||
|
|
||||||
|
if (!$welcomeConfig || !(bool)$welcomeConfig['activo']) {
|
||||||
|
$logInteraction("Bienvenida de Discord deshabilitada o no configurada.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el usuario ya está registrado en nuestra DB
|
||||||
|
if (isDiscordUserRegistered($db, $member->id)) {
|
||||||
|
$logInteraction("Miembro ya registrado, no se envía bienvenida para evitar duplicados.", ['user_id' => $member->id]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la configuración indica registrar al usuario, hacerlo aquí
|
||||||
|
if ((bool)$welcomeConfig['registrar_usuario']) {
|
||||||
|
registerDiscordRecipient($db, $member->id, 'usuario', $member->username, $member->username);
|
||||||
|
$logInteraction("Nuevo miembro registrado en DB.", ['user_id' => $member->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener el canal de bienvenida
|
||||||
|
$guild = $discord->guilds->get('id', $member->guild_id);
|
||||||
|
if (!$guild) {
|
||||||
|
$logInteraction("No se pudo obtener el gremio para enviar la bienvenida.", ['guild_id' => $member->guild_id]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$channel = $guild->channels->get('id', $welcomeConfig['canal_id']);
|
||||||
|
if (!$channel) {
|
||||||
|
$logInteraction("No se pudo encontrar el canal de bienvenida configurado.", ['channel_id' => $welcomeConfig['canal_id']]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construir el mensaje de bienvenida
|
||||||
|
$messageContent = $welcomeConfig['texto'];
|
||||||
|
// Reemplazar placeholders y limpiar HTML
|
||||||
|
$messageContent = str_replace('{usuario}', '<@'.$member->id.'>', $messageContent);
|
||||||
|
$messageContent = strip_tags($messageContent); // Eliminar HTML si existe
|
||||||
|
|
||||||
|
$messageBuilder = MessageBuilder::new()
|
||||||
|
->setContent($messageContent);
|
||||||
|
|
||||||
|
// Añadir imagen si está configurada
|
||||||
|
if (!empty($welcomeConfig['imagen_ruta'])) {
|
||||||
|
// Asumo que 'imagen_ruta' es una URL completa o una ruta accesible por Discord
|
||||||
|
// Si es una ruta local, necesitarías subirla a Discord primero o servirla desde una URL
|
||||||
|
// Por simplicidad, si es una ruta local, la ignoraremos por ahora a menos que se especifique cómo servirla.
|
||||||
|
// Si la ruta ya es una URL pública, se puede añadir directamente
|
||||||
|
if (filter_var($welcomeConfig['imagen_ruta'], FILTER_VALIDATE_URL)) {
|
||||||
|
$messageBuilder->setImage($welcomeConfig['imagen_ruta']);
|
||||||
|
} elseif (!empty($_ENV['CAOS_BASE_URL']) && strpos($welcomeConfig['imagen_ruta'], '/gallery/uploads/') === 0) {
|
||||||
|
// Si es una ruta relativa de galería, construir la URL completa
|
||||||
|
$fullImageUrl = rtrim($_ENV['CAOS_BASE_URL'], '/') . $welcomeConfig['imagen_ruta'];
|
||||||
|
$messageBuilder->setImage($fullImageUrl);
|
||||||
|
$logInteraction("Imagen de bienvenida construida como URL.", ['url' => $fullImageUrl]);
|
||||||
|
} else {
|
||||||
|
$logInteraction("Ruta de imagen no válida o no URL, se ignorará.", ['ruta' => $welcomeConfig['imagen_ruta']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añadir botones de idioma
|
||||||
|
$idiomasHabilitados = json_decode($welcomeConfig['idiomas_habilitados'], true) ?? [];
|
||||||
|
if (!empty($idiomasHabilitados)) {
|
||||||
|
$actionRow = new ActionRow();
|
||||||
|
$langLabels = [
|
||||||
|
'es' => 'Español',
|
||||||
|
'en' => 'English',
|
||||||
|
'pt' => 'Português'
|
||||||
|
];
|
||||||
|
foreach ($idiomasHabilitados as $langCode) {
|
||||||
|
$label = $langLabels[$langCode] ?? strtoupper($langCode);
|
||||||
|
$emoji = ''; // Puedes añadir emojis específicos si los tienes
|
||||||
|
if ($langCode === 'es') $emoji = '🇪🇸';
|
||||||
|
if ($langCode === 'en') $emoji = '🇬🇧';
|
||||||
|
if ($langCode === 'pt') $emoji = '🇵🇹';
|
||||||
|
|
||||||
|
$button = Button::new(Button::STYLE_PRIMARY)
|
||||||
|
->setLabel($label)
|
||||||
|
->setCustomId("lang_select_{$langCode}");
|
||||||
|
if (!empty($emoji)) {
|
||||||
|
$button->setEmoji($emoji);
|
||||||
|
}
|
||||||
|
$actionRow->addComponent($button);
|
||||||
|
}
|
||||||
|
$messageBuilder->addComponent($actionRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar el mensaje de bienvenida (no efímero)
|
||||||
|
$channel->sendMessage($messageBuilder)
|
||||||
|
->then(function ($message) use ($member, $logInteraction) {
|
||||||
|
$logInteraction("Mensaje de bienvenida enviado con éxito.", ['user_id' => $member->id, 'message_id' => $message->id]);
|
||||||
|
})
|
||||||
|
->otherwise(function ($error) use ($member, $logInteraction) {
|
||||||
|
$logInteraction("Error al enviar mensaje de bienvenida.", ['user_id' => $member->id, 'error' => $error->getMessage()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$logInteraction("Error en manejador GUILD_MEMBER_ADD.", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Manejador de interacciones (botones, etc.) ---
|
||||||
|
$discord->on('INTERACTION_CREATE', function (Interaction $interaction, Discord $discord) use ($db, $logInteraction) {
|
||||||
|
// Loguear cada interacción recibida
|
||||||
|
$logInteraction("Interacción recibida en INTERACTION_CREATE", [
|
||||||
|
'id' => $interaction->id ?? 'N/A',
|
||||||
|
'type' => $interaction->type ?? 'N/A',
|
||||||
|
'custom_id' => $interaction->data->custom_id ?? 'N/A',
|
||||||
|
'user_id' => $interaction->user->id ?? 'N/A',
|
||||||
|
'command_name' => $interaction->data->name ?? 'N/A' // Añadir para comandos slash
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Manejar comandos de aplicación (slash commands)
|
||||||
|
if ($interaction->type === Interaction::TYPE_APPLICATION_COMMAND) {
|
||||||
|
$commandName = $interaction->data->name;
|
||||||
|
$logInteraction("Comando de aplicación recibido", ['command' => $commandName]);
|
||||||
|
|
||||||
|
if ($commandName === 'start') {
|
||||||
|
$welcomeConfig = getWelcomeConfig($db); // Obtener la configuración de bienvenida
|
||||||
|
$logInteraction("Comando /start: Obtenida configuración de bienvenida.", ['config' => $welcomeConfig]);
|
||||||
|
|
||||||
|
$messageContent = "¡Hola! Selecciona tu idioma preferido para interactuar con el bot."; // Default
|
||||||
|
$imageUrl = null;
|
||||||
|
$idiomasHabilitados = [];
|
||||||
|
|
||||||
|
if ($welcomeConfig && (bool)$welcomeConfig['activo']) {
|
||||||
|
$messageContent = $welcomeConfig['texto'];
|
||||||
|
// Reemplazar placeholders y limpiar HTML
|
||||||
|
$messageContent = str_replace('{usuario}', '<@'.$interaction->user->id.'>', $messageContent);
|
||||||
|
$messageContent = strip_tags($messageContent); // Eliminar HTML si existe
|
||||||
|
|
||||||
|
// Añadir imagen si está configurada
|
||||||
|
if (!empty($welcomeConfig['imagen_ruta'])) {
|
||||||
|
if (filter_var($welcomeConfig['imagen_ruta'], FILTER_VALIDATE_URL)) {
|
||||||
|
$imageUrl = $welcomeConfig['imagen_ruta'];
|
||||||
|
} elseif (!empty($_ENV['CAOS_BASE_URL']) && strpos($welcomeConfig['imagen_ruta'], '/gallery/uploads/') === 0) {
|
||||||
|
$imageUrl = rtrim($_ENV['CAOS_BASE_URL'], '/') . $welcomeConfig['imagen_ruta'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$idiomasHabilitados = json_decode($welcomeConfig['idiomas_habilitados'], true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mensaje de bienvenida (texto y posible imagen)
|
||||||
|
$welcomeMessageBuilder = MessageBuilder::new()
|
||||||
|
->setContent($messageContent)
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL);
|
||||||
|
|
||||||
|
if ($imageUrl) {
|
||||||
|
$welcomeMessageBuilder->setImage($imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar el mensaje de bienvenida efímero como respuesta inicial
|
||||||
|
$interaction->respondWithMessage($welcomeMessageBuilder, true);
|
||||||
|
$logInteraction("Comando /start manejado: mensaje de bienvenida enviado (respondWithMessage efímero).", ['content' => $messageContent, 'image' => $imageUrl]);
|
||||||
|
|
||||||
|
// Crear los botones de selección de idioma
|
||||||
|
$buttons = [];
|
||||||
|
$langLabels = [
|
||||||
|
'es' => 'Español',
|
||||||
|
'en' => 'English',
|
||||||
|
'pt' => 'Português'
|
||||||
|
];
|
||||||
|
foreach ($idiomasHabilitados as $langCode) {
|
||||||
|
$label = $langLabels[$langCode] ?? strtoupper($langCode);
|
||||||
|
$emoji = '';
|
||||||
|
if ($langCode === 'es') $emoji = '🇪🇸';
|
||||||
|
if ($langCode === 'en') $emoji = '🇬🇧';
|
||||||
|
if ($langCode === 'pt') $emoji = '🇵🇹';
|
||||||
|
|
||||||
|
$button = Button::new(Button::STYLE_PRIMARY)
|
||||||
|
->setLabel($label)
|
||||||
|
->setCustomId("lang_select_{$langCode}");
|
||||||
|
if (!empty($emoji)) {
|
||||||
|
$button->setEmoji($emoji);
|
||||||
|
}
|
||||||
|
$buttons[] = $button;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actionRow = new ActionRow();
|
||||||
|
foreach ($buttons as $button) {
|
||||||
|
$actionRow->addComponent($button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar los botones como un follow-up message efímero
|
||||||
|
$buttonsMessage = MessageBuilder::new()
|
||||||
|
->addComponent($actionRow)
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL);
|
||||||
|
|
||||||
|
$interaction->sendFollowUpMessage($buttonsMessage);
|
||||||
|
$logInteraction("Comando /start manejado: botones de idioma enviados (sendFollowUpMessage efímero).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Puedes añadir más comandos slash aquí
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejar componentes de mensaje (botones, etc.)
|
||||||
|
if ($interaction->type === Interaction::TYPE_MESSAGE_COMPONENT) {
|
||||||
|
$customId = $interaction->data->custom_id ?? '';
|
||||||
|
|
||||||
|
// Manejar botón de traducción
|
||||||
|
if (strpos($customId, 'translate_') === 0) {
|
||||||
|
$logInteraction("Manejando botón de traducción", [
|
||||||
|
'custom_id' => $customId,
|
||||||
|
'message_id' => str_replace('translate_', '', $customId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Asegurarse de que la interacción se reconozca
|
||||||
|
$interaction->acknowledge(true);
|
||||||
|
|
||||||
|
// Llamar al manejador de traducción
|
||||||
|
handleTranslationButton($interaction, $db);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$logInteraction("Error al manejar el botón de traducción", [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Intentar notificar al usuario del error
|
||||||
|
try {
|
||||||
|
$interaction->sendMessage(
|
||||||
|
MessageBuilder::new()
|
||||||
|
->setContent("❌ Ocurrió un error al procesar la traducción. Por favor, inténtalo de nuevo.")
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e2) {
|
||||||
|
$logInteraction("Error al notificar al usuario", [
|
||||||
|
'error' => $e2->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejar selección de idioma
|
||||||
|
if (strpos($customId, 'lang_select_') === 0) {
|
||||||
|
$logInteraction("Entrando a lang_select_ handler", ['custom_id' => $customId]);
|
||||||
|
// Primero, reconocer la interacción para evitar el timeout de Discord
|
||||||
|
$interaction->acknowledge(true); // true para una respuesta efímera
|
||||||
|
$logInteraction("Interacción acknowledge(true) ejecutada.");
|
||||||
|
|
||||||
|
$langCode = substr($customId, strlen('lang_select_'));
|
||||||
|
$user = $interaction->user;
|
||||||
|
error_log("Usuario {$user->username} seleccionó idioma: {$langCode}" . PHP_EOL, 3, __DIR__ . '/logs/interaction_events.log');
|
||||||
|
|
||||||
|
// Antes de la DB:
|
||||||
|
$logInteraction("Intentando registrar/actualizar destinatario en BD.", ['user_id' => $user->id, 'lang_code' => $langCode]);
|
||||||
|
registerDiscordRecipient($db, $user->id, 'usuario', $user->username, $user->username);
|
||||||
|
$stmt = $db->prepare("UPDATE destinatarios_discord SET idioma_detectado = ? WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$langCode, $user->id]);
|
||||||
|
// Después de la DB:
|
||||||
|
$logInteraction("Destinatario registrado/actualizado en BD.");
|
||||||
|
|
||||||
|
error_log("Preferencia de idioma guardada en BD." . PHP_EOL, 3, __DIR__ . '/logs/interaction_events.log');
|
||||||
|
$interaction->sendFollowUpMessage( // <-- CAMBIO AQUÍ
|
||||||
|
MessageBuilder::new()
|
||||||
|
->setContent("✅ Idioma seleccionado: " . strtoupper($langCode))
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL)
|
||||||
|
);
|
||||||
|
$logInteraction("sendFollowUpMessage ejecutado en lang_select_ handler.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("Error en INTERACTION_CREATE: " . $e->getMessage());
|
||||||
|
|
||||||
|
// Intentar responder con un mensaje de error si es posible
|
||||||
|
if (isset($interaction) && $interaction instanceof Interaction) {
|
||||||
|
try {
|
||||||
|
$interaction->respondWithMessage(
|
||||||
|
MessageBuilder::new()
|
||||||
|
->setContent("❌ Ocurrió un error al procesar tu solicitud.")
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL)
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e2) {
|
||||||
|
// Ignorar errores al intentar responder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Manejador de errores de WebSocket ---
|
||||||
|
$discord->on('error', function ($error, $ws) use ($discord) {
|
||||||
|
echo "Error de WebSocket: " . $error . PHP_EOL;
|
||||||
|
// Intentar reconectar después de 5 segundos
|
||||||
|
$discord->getLoop()->addTimer(5, function() use ($discord) {
|
||||||
|
echo "Intentando reconectar..." . PHP_EOL;
|
||||||
|
$discord->getLoop()->stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Función para detectar si un mensaje necesita traducción ---
|
||||||
|
function needsTranslation($text) {
|
||||||
|
// Si el texto está vacío o es muy corto, no necesita traducción
|
||||||
|
if (empty(trim($text)) || strlen(trim($text)) < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lista de palabras comunes en español
|
||||||
|
$spanishWords = [
|
||||||
|
'hola', 'buenos', 'días', 'tardes', 'noche', 'gracias', 'por favor',
|
||||||
|
'adiós', 'hasta luego', 'sí', 'no', 'porque', 'pero', 'y', 'o',
|
||||||
|
'el', 'la', 'los', 'las', 'un', 'una', 'unos', 'unas', 'es', 'son',
|
||||||
|
'soy', 'eres', 'somos', 'sois', 'estoy', 'estás', 'está', 'estamos',
|
||||||
|
'estáis', 'están', 'tengo', 'tienes', 'tiene', 'tenemos', 'tenéis',
|
||||||
|
'tienen', 'voy', 'vas', 'va', 'vamos', 'vais', 'van', 'hacer', 'hecho',
|
||||||
|
'día', 'año', 'vez', 'tiempo', 'casa', 'agua', 'comida', 'bebida'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Convertir a minúsculas y eliminar signos de puntuación
|
||||||
|
$cleanText = preg_replace('/[^\p{L}\s]/u', '', mb_strtolower($text));
|
||||||
|
$words = preg_split('/\s+/', $cleanText);
|
||||||
|
|
||||||
|
// Contar palabras en español
|
||||||
|
$spanishWordCount = 0;
|
||||||
|
foreach ($words as $word) {
|
||||||
|
if (in_array($word, $spanishWords)) {
|
||||||
|
$spanishWordCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si menos del 30% de las palabras son en español, asumir que necesita traducción
|
||||||
|
$needsTranslation = ($spanishWordCount / max(1, count($words))) < 0.3;
|
||||||
|
|
||||||
|
error_log("Análisis de traducción - Palabras: " . count($words) . ", Español: $spanishWordCount, Necesita traducción: " . ($needsTranslation ? 'Sí' : 'No'));
|
||||||
|
|
||||||
|
return $needsTranslation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para traducir texto usando LibreTranslate ---
|
||||||
|
function translateText($text, $targetLang) {
|
||||||
|
$libreTranslateUrl = $_ENV['LIBRETRANSLATE_URL'] ?? 'http://localhost:5000';
|
||||||
|
$url = rtrim($libreTranslateUrl, '/') . '/translate';
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'q' => $text,
|
||||||
|
'source' => 'auto', // Detección automática del idioma
|
||||||
|
'target' => $targetLang
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false || $httpCode !== 200) {
|
||||||
|
error_log("Error en la traducción. Código HTTP: $httpCode. Respuesta: " . $response);
|
||||||
|
return "Error al traducir el mensaje. Por favor, inténtalo de nuevo más tarde.";
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
return $result['translatedText'] ?? $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para manejar el botón de traducción ---
|
||||||
|
function handleTranslationButton(Interaction $interaction, PDO $db) {
|
||||||
|
try {
|
||||||
|
// Verificar que la interacción sea válida
|
||||||
|
if (!$interaction instanceof \Discord\Parts\Interactions\Interaction) {
|
||||||
|
$logInteraction("handleTranslationButton: Interacción no válida", ['type' => gettype($interaction)]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Verificar que tengamos un ID de mensaje
|
||||||
|
if (empty($interaction->data->custom_id)) {
|
||||||
|
$logInteraction("handleTranslationButton: No se pudo obtener el ID del mensaje de la interacción", [
|
||||||
|
'interaction' => [
|
||||||
|
'id' => $interaction->id ?? 'N/A',
|
||||||
|
'type' => $interaction->type ?? 'N/A',
|
||||||
|
'data' => $interaction->data ?? 'N/A'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
throw new \Exception("No se pudo obtener el ID del mensaje de la interacción");
|
||||||
|
}
|
||||||
|
// Extraer el ID del mensaje original del custom_id
|
||||||
|
$customId = $interaction->data->custom_id;
|
||||||
|
$logError("handleTranslationButton: customId recibido (RAW): " . $customId, ['customId' => $customId]); // Nuevo log para el customId RAW
|
||||||
|
$logError("handleTranslationButton: customId recibido (desde variable): " . $customId, ['customId' => $customId]); // Mantener el log anterior para comparación
|
||||||
|
|
||||||
|
// El customId ahora es "translate_{messageId}"
|
||||||
|
$parts = explode('_', $customId);
|
||||||
|
$logError("handleTranslationButton: Resultado de explode: ", ['parts' => $parts, 'count' => count($parts)]);
|
||||||
|
if (count($parts) === 2 && $parts[0] === 'translate') {
|
||||||
|
$messageId = $parts[1];
|
||||||
|
} else {
|
||||||
|
$logError("handleTranslationButton: custom_id no tiene el formato esperado (translate_MESSAGEID).", ['customId' => $customId, 'parts' => $parts, 'count' => count($parts)]); $messageId = $parts[1];
|
||||||
|
$logInteraction("handleTranslationButton: custom_id no tiene el formato esperado (translate_MESSAGEID).", [
|
||||||
|
'customId' => $customId, 'parts' => $parts, 'count' => count($parts)]);
|
||||||
|
try {
|
||||||
|
return $interaction->sendFollowUpMessage(
|
||||||
|
MessageBuilder::new()
|
||||||
|
->setContent("❌ Error interno: Formato de botón de traducción incorrecto.")
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL)
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$logInteraction("Error al enviar mensaje de error por formato de botón.", ['error' => $e->getMessage()]
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener el idioma del usuario que presionó el botón
|
||||||
|
$targetLang = getUserPreferredLanguage($db, $interaction->user->id);
|
||||||
|
if (empty($targetLang)) {
|
||||||
|
$targetLang = 'en'; // Default a inglés si no hay idioma preferido
|
||||||
|
$logInteraction("handleTranslationButton: Usuario {$interaction->user->id} no tiene idioma preferido, usando 'en'.");
|
||||||
|
}
|
||||||
|
$logInteraction("handleTranslationButton: Usuario {$interaction->user->username} (${interaction->user->id}) solicitó traducción a: {$targetLang}.");
|
||||||
|
|
||||||
|
|
||||||
|
// Obtener el canal donde ocurrió la interacción
|
||||||
|
$channel = $interaction->channel;
|
||||||
|
|
||||||
|
// Obtener el mensaje original
|
||||||
|
$message = null;
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Intentar obtener el mensaje directamente del canal
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Intentando obtener mensaje con ID: {$messageId} del canal: {$channel->id}.");
|
||||||
|
|
||||||
|
$message = $channel->getMessage($messageId);
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Resultado de getMessage: " . ($message ? "Obtenido de caché" : "No en caché"));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Si no se pudo obtener, intentar de otra manera
|
||||||
|
|
||||||
|
if (!$message) {
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: No se pudo obtener el mensaje directamente de caché, intentando con fetch a la API...");
|
||||||
|
|
||||||
|
$message = $channel->fetchMessage($messageId);
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Resultado de fetchMessage: " . ($message ? "Obtenido de API" : "No obtenido de API"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
|
||||||
|
$error = $e->getMessage();
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Error al obtener el mensaje", [
|
||||||
|
|
||||||
|
'error' => $error,
|
||||||
|
|
||||||
|
'messageId' => $messageId,
|
||||||
|
|
||||||
|
'channelId' => $channel->id ?? 'N/A'
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!$message) {
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: No se pudo encontrar el mensaje original", [
|
||||||
|
|
||||||
|
'messageId' => $messageId,
|
||||||
|
|
||||||
|
'channelId' => $channel->id ?? 'N/A',
|
||||||
|
|
||||||
|
'error' => $error ?? 'Mensaje nulo sin error específico'
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return $interaction->sendFollowUpMessage(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
MessageBuilder::new()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
->setContent("❌ No se pudo encontrar el mensaje original (ID: {$messageId}). Puede haber
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
sido eliminado o ser demasiado antiguo.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$logInteraction("Error al enviar mensaje de error al usuario", [
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$logInteraction("handleTranslationButton: Mensaje original obtenido. Contenido: " . substr($message->content, 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
, 50) . "...");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Traducir el mensaje al idioma del usuario que presionó el botón
|
||||||
|
$translatedText = translateText($message->content, $targetLang);
|
||||||
|
$logInteraction("handleTranslationButton: Mensaje traducido. Contenido: " . substr($translatedText, 0, 50) . "...");
|
||||||
|
|
||||||
|
// Crear el mensaje de respuesta
|
||||||
|
$langLabels = [
|
||||||
|
'es' => 'español',
|
||||||
|
'en' => 'inglés',
|
||||||
|
'pt' => 'portugués'
|
||||||
|
];
|
||||||
|
$displayLang = $langLabels[$targetLang] ?? $targetLang; // Fallback a usar el código si no está en la lista
|
||||||
|
|
||||||
|
$response = MessageBuilder::new()
|
||||||
|
->setContent("**Mensaje traducido al {$displayLang}**:\n\n" . $translatedText)
|
||||||
|
->setFlags(Message::FLAG_EPHEMERAL);
|
||||||
|
|
||||||
|
// Enviar la respuesta como un follow-up
|
||||||
|
$interaction->sendFollowUpMessage($response);
|
||||||
|
|
||||||
|
error_log("handleTranslationButton: Traducción enviada para el mensaje: " . $message->id);
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$logInteraction("Error en handleTranslationButton (nivel superior): " . $e->getMessage(), ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||||
|
try {
|
||||||
|
$interaction->respondWithMessage(
|
||||||
|
MessageBuilder::new()->setContent("Ocurrió un error al traducir el mensaje."),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e2) {
|
||||||
|
// Ignorar errores al responder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- Función para obtener la configuración de bienvenida de Discord ---
|
||||||
|
function getWelcomeConfig(PDO $db): ?array {
|
||||||
|
$stmt = $db->query("
|
||||||
|
SELECT b.*, g.ruta as imagen_ruta
|
||||||
|
FROM bienvenida_discord b
|
||||||
|
LEFT JOIN gallery g ON b.imagen_id = g.id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$config = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $config ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para verificar si un usuario de Discord ya está registrado ---
|
||||||
|
function isDiscordUserRegistered(PDO $db, string $discordId): bool {
|
||||||
|
$stmt = $db->prepare("SELECT COUNT(*) FROM destinatarios_discord WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$discordId]);
|
||||||
|
return $stmt->fetchColumn() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para obtener conexión a la base de datos ---
|
||||||
|
function getDBConnection(): PDO {
|
||||||
|
$host = $_ENV['DB_HOST'] ?? '127.0.0.1';
|
||||||
|
$db = $_ENV['DB_NAME'] ?? 'db';
|
||||||
|
$user = $_ENV['DB_USER'] ?? 'root';
|
||||||
|
$pass = $_ENV['DB_PASS'] ?? '';
|
||||||
|
$port = $_ENV['DB_PORT'] ?? '3306';
|
||||||
|
$charset = 'utf8mb4';
|
||||||
|
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset";
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
return new PDO($dsn, $user, $pass, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Función para registrar destinatarios de Discord ---
|
||||||
|
function registerDiscordRecipient(PDO $db, $discordId, $type, $name, $username = null) {
|
||||||
|
$stmt = $db->prepare("INSERT INTO destinatarios_discord (discord_id, tipo, nombre, username)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE nombre = VALUES(nombre), username = VALUES(username)");
|
||||||
|
$stmt->execute([$discordId, $type, $name, $username]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Iniciar el bot ---
|
||||||
|
echo "Iniciando bot..." . PHP_EOL;
|
||||||
|
$discord->run();
|
||||||
|
|
||||||
|
// --- Manejar cierre limpio ---
|
||||||
|
register_shutdown_function(function() use ($discord, $db) {
|
||||||
|
echo "Cerrando el bot..." . PHP_EOL;
|
||||||
|
if ($db) {
|
||||||
|
$db = null;
|
||||||
|
}
|
||||||
|
$discord->close();
|
||||||
|
});
|
||||||
484
bot_daemon.php
Executable file
484
bot_daemon.php
Executable file
@@ -0,0 +1,484 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Discord Bot Daemon - New Version
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Cargar dependencias
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/discord/src/DiscordSender.php';
|
||||||
|
require_once __DIR__ . '/discord/src/HtmlToDiscordMarkdownConverter.php';
|
||||||
|
require_once __DIR__ . '/discord/src/Translate.php';
|
||||||
|
require_once __DIR__ . '/discord/src/CommandLocker.php';
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Importar clases necesarias de DiscordPHP
|
||||||
|
use Discord\Discord;
|
||||||
|
use Discord\WebSockets\Intents;
|
||||||
|
use Discord\WebSockets\Event;
|
||||||
|
use Discord\Parts\Channel\Message;
|
||||||
|
use Discord\Parts\Interactions\Interaction;
|
||||||
|
use Discord\Builders\MessageBuilder;
|
||||||
|
use Discord\Parts\Embed\Embed;
|
||||||
|
use Discord\Parts\Guild\Member;
|
||||||
|
use Discord\Builders\Components\ActionRow;
|
||||||
|
use Discord\Builders\Components\Button;
|
||||||
|
|
||||||
|
// --- Logger y Conexión a BD ---
|
||||||
|
$logInteraction = function($message, $data = []) {
|
||||||
|
$pid = getmypid();
|
||||||
|
$logMessage = date('[Y-m-d H:i:s] ') . "[PID: {$pid}] " . $message . "\n";
|
||||||
|
if (!empty($data)) {
|
||||||
|
$logMessage .= "Datos: " . json_encode($data, JSON_PRETTY_PRINT) . "\n";
|
||||||
|
}
|
||||||
|
error_log($logMessage, 3, __DIR__ . '/logs/discord_bot_new.log');
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDBConnection(): PDO {
|
||||||
|
$host = $_ENV['DB_HOST'] ?? '127.0.0.1';
|
||||||
|
$dbName = $_ENV['DB_NAME'] ?? 'db';
|
||||||
|
$user = $_ENV['DB_USER'] ?? 'root';
|
||||||
|
$pass = $_ENV['DB_PASS'] ?? '';
|
||||||
|
$port = $_ENV['DB_PORT'] ?? '3306';
|
||||||
|
$charset = 'utf8mb4';
|
||||||
|
$dsn = "mysql:host=$host;port=$port;dbname=$dbName;charset=$charset";
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
return new PDO($dsn, $user, $pass, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logInteraction("Iniciando bot de Discord...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDBConnection();
|
||||||
|
$logInteraction("Conexión a la base de datos establecida.");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$logInteraction("Error fatal al conectar a la base de datos", ['error' => $e->getMessage()]);
|
||||||
|
die("Error de base de datos.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_ENV['DISCORD_BOT_TOKEN'])) {
|
||||||
|
$logInteraction("Error Fatal: La variable de entorno DISCORD_BOT_TOKEN no está definida o está vacía.");
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRecipientIfNotExists(PDO $db, callable $logInteraction, string $type, string $discordId, ?string $name): void {
|
||||||
|
$logInteraction("[Registro BD] Verificando destinatario...", ['type' => $type, 'id' => $discordId, 'name' => $name]);
|
||||||
|
|
||||||
|
if (empty($discordId) || empty($name)) {
|
||||||
|
$logInteraction("Se omitió guardar destinatario por ID o nombre vacío.", ['type' => $type, 'id' => $discordId, 'name' => $name]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("SELECT id FROM destinatarios_discord WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$discordId]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
$insertStmt = $db->prepare(
|
||||||
|
"INSERT INTO destinatarios_discord (tipo, discord_id, nombre) VALUES (?, ?, ?)"
|
||||||
|
);
|
||||||
|
$insertStmt->execute([$type, $discordId, $name]);
|
||||||
|
$logInteraction("Nuevo destinatario guardado", ['tipo' => $type, 'discord_id' => $discordId, 'nombre' => $name]);
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$logInteraction("Error al guardar destinatario: " . $e->getMessage(), [
|
||||||
|
'discord_id' => $discordId,
|
||||||
|
'nombre' => $name,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
$discord = new Discord([
|
||||||
|
'token' => $_ENV['DISCORD_BOT_TOKEN'],
|
||||||
|
'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS | Intents::MESSAGE_CONTENT,
|
||||||
|
'logger' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$discord->on('ready', function (Discord $discord) use ($logInteraction) {
|
||||||
|
$logInteraction("==================================================");
|
||||||
|
$logInteraction("Bot conectado y listo para escuchar!");
|
||||||
|
$logInteraction("Usuario: {$discord->user->username}#{$discord->user->discriminator}");
|
||||||
|
$logInteraction("==================================================");
|
||||||
|
});
|
||||||
|
|
||||||
|
$discord->on(Event::GUILD_MEMBER_ADD, function ($member, Discord $discord) use ($db, $logInteraction) {
|
||||||
|
$logInteraction("[NUEVO MIEMBRO] Usuario {$member->user->username} ({$member->id}) se ha unido al servidor.");
|
||||||
|
saveRecipientIfNotExists($db, $logInteraction, 'user', $member->id, $member->user->username);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->query("SELECT codigo as language_code, nombre as language_name, bandera as flag_emoji FROM idiomas WHERE activo = 1 ORDER BY nombre ASC");
|
||||||
|
$activeLangs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($activeLangs)) {
|
||||||
|
$logInteraction("ADVERTENCIA: [BIENVENIDA] No se envió mensaje de bienvenida porque no hay idiomas activos.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$translator = new Translate();
|
||||||
|
$baseTitle = "👋 ¡Hola, {$member->user->username}! Bienvenido/a a el Discord de Cereal Kiiller.";
|
||||||
|
$baseDescription = "Por favor, selecciona tu idioma preferido:";
|
||||||
|
|
||||||
|
$fullDescription = "";
|
||||||
|
foreach($activeLangs as $lang) {
|
||||||
|
$langCode = $lang['language_code'];
|
||||||
|
$flag = $lang['flag_emoji'] ? $lang['flag_emoji'] . ' ' : '';
|
||||||
|
|
||||||
|
$translatedDesc = ($langCode === 'es')
|
||||||
|
? $baseDescription
|
||||||
|
: $translator->translateText($baseDescription, 'es', $langCode);
|
||||||
|
|
||||||
|
if ($translatedDesc) {
|
||||||
|
$fullDescription .= $flag . $translatedDesc . "\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$embed = new Embed($discord);
|
||||||
|
$embed->setTitle($baseTitle);
|
||||||
|
$embed->setDescription(trim($fullDescription));
|
||||||
|
$embed->setColor("#5865F2");
|
||||||
|
if ($member->user && $member->user->avatar) {
|
||||||
|
$embed->setThumbnail($member->user->getAvatar());
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = MessageBuilder::new()->addEmbed($embed);
|
||||||
|
|
||||||
|
$actionRows = [];
|
||||||
|
$currentRow = ActionRow::new();
|
||||||
|
$buttonCount = 0;
|
||||||
|
|
||||||
|
foreach ($activeLangs as $lang) {
|
||||||
|
$button = Button::new(Button::STYLE_SECONDARY, 'set_lang_' . $lang['language_code'])
|
||||||
|
->setLabel($lang['language_name']);
|
||||||
|
if (!empty($lang['flag_emoji'])) {
|
||||||
|
$button->setEmoji($lang['flag_emoji']);
|
||||||
|
}
|
||||||
|
$currentRow->addComponent($button);
|
||||||
|
$buttonCount++;
|
||||||
|
|
||||||
|
if ($buttonCount > 0 && $buttonCount % 5 === 0) {
|
||||||
|
$actionRows[] = $currentRow;
|
||||||
|
$currentRow = ActionRow::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($buttonCount % 5 !== 0) {
|
||||||
|
$actionRows[] = $currentRow;
|
||||||
|
}
|
||||||
|
foreach ($actionRows as $row) {
|
||||||
|
$builder->addComponent($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
$member->sendMessage($builder)->then(
|
||||||
|
function () use ($logInteraction, $member) {
|
||||||
|
$logInteraction("[BIENVENIDA] Mensaje de selección de idioma enviado por DM a {$member->user->username}.");
|
||||||
|
},
|
||||||
|
function ($error) use ($logInteraction, $member) {
|
||||||
|
$logInteraction("ERROR: [BIENVENIDA] No se pudo enviar DM de bienvenida a {$member->user->username}.", ['error' => $error]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$logInteraction("ERROR: [BIENVENIDA] Error fatal al procesar nuevo miembro.", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$discord->on(Event::INTERACTION_CREATE, function (Interaction $interaction, Discord $discord) use ($db, $logInteraction) {
|
||||||
|
saveRecipientIfNotExists($db, $logInteraction, 'user', $interaction->user->id, $interaction->user->username);
|
||||||
|
|
||||||
|
$channelName = 'Canal Desconocido';
|
||||||
|
if ($interaction->channel) {
|
||||||
|
$channelName = $interaction->channel->name;
|
||||||
|
if ($interaction->channel->guild) {
|
||||||
|
$channelName = $interaction->channel->guild->name . ' - ' . $channelName;
|
||||||
|
} elseif ($interaction->channel->is_private) {
|
||||||
|
$channelName = 'DM con ' . $interaction->user->username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveRecipientIfNotExists($db, $logInteraction, 'channel', $interaction->channel_id, $channelName);
|
||||||
|
|
||||||
|
$type = (int) ($interaction->type ?? 0);
|
||||||
|
$componentType = (int) ($interaction->data->component_type ?? 0);
|
||||||
|
if ($type !== 3 || $componentType !== 2) return;
|
||||||
|
|
||||||
|
$customId = $interaction->data->custom_id;
|
||||||
|
$userId = $interaction->user->id;
|
||||||
|
$logInteraction("[INTERACCION] Usuario $userId hizo clic en el botón: $customId");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (strpos($customId, 'set_lang_') === 0) {
|
||||||
|
$langCode = substr($customId, strlen('set_lang_'));
|
||||||
|
$stmt = $db->prepare("UPDATE destinatarios_discord SET idioma_detectado = ? WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$langCode, $userId]);
|
||||||
|
|
||||||
|
$langNameStmt = $db->prepare("SELECT nombre FROM idiomas WHERE codigo = ?");
|
||||||
|
$langNameStmt->execute([$langCode]);
|
||||||
|
$langName = $langNameStmt->fetchColumn() ?: strtoupper($langCode);
|
||||||
|
|
||||||
|
$confirmEmbed = new Embed($discord);
|
||||||
|
$confirmEmbed->setTitle("✅ Idioma Configurado");
|
||||||
|
$confirmEmbed->setDescription("Tu idioma ha sido establecido a: **" . htmlspecialchars($langName) . "**");
|
||||||
|
$confirmEmbed->setColor("#57F287");
|
||||||
|
|
||||||
|
$builder = MessageBuilder::new()->addEmbed($confirmEmbed);
|
||||||
|
|
||||||
|
$interaction->acknowledge()->then(function() use ($interaction, $builder, $logInteraction, $langCode, $userId) {
|
||||||
|
$interaction->message->edit($builder)->then(function() use ($logInteraction, $langCode, $userId) {
|
||||||
|
$logInteraction("[BIENVENIDA] El usuario $userId ha establecido su idioma a '$langCode' y el mensaje fue editado.");
|
||||||
|
}, function ($error) use ($logInteraction, $langCode, $userId) {
|
||||||
|
$logInteraction("ERROR: [BIENVENIDA] Error al editar el mensaje original para {$userId}.", ['lang' => $langCode, 'error' => $error]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($customId, 'translate_auto:') === 0) {
|
||||||
|
$originalMessageId = substr($customId, strlen('translate_auto:'));
|
||||||
|
$stmt = $db->prepare("SELECT idioma_detectado FROM destinatarios_discord WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$targetLang = $stmt->fetchColumn() ?: 'es';
|
||||||
|
|
||||||
|
$interaction->channel->messages->fetch($originalMessageId)->then(
|
||||||
|
function ($originalMessage) use ($interaction, $discord, $db, $logInteraction, $userId, $targetLang) {
|
||||||
|
$originalContent = trim((string) ($originalMessage->content ?? ''));
|
||||||
|
if (empty($originalContent)) {
|
||||||
|
$interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ No se encontró contenido para traducir."), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$translator = new Translate();
|
||||||
|
$sourceLang = $translator->detectLanguage($originalContent) ?? 'es';
|
||||||
|
|
||||||
|
if ($sourceLang === $targetLang) {
|
||||||
|
$interaction->respondWithMessage(MessageBuilder::new()->setContent("⚠️ El mensaje ya está en tu idioma ({$targetLang})."), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$translatedText = $translator->translateText($originalContent, $sourceLang, $targetLang);
|
||||||
|
|
||||||
|
if (!empty($translatedText)) {
|
||||||
|
$stmt = $db->prepare("SELECT bandera, nombre FROM idiomas WHERE codigo = ? AND activo = 1");
|
||||||
|
$stmt->execute([$targetLang]);
|
||||||
|
$langInfo = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$flag = $langInfo['bandera'] ?? '🏳️';
|
||||||
|
$langName = $langInfo['nombre'] ?? strtoupper($targetLang);
|
||||||
|
|
||||||
|
$embed = new Embed($discord);
|
||||||
|
$embed->setTitle("{$flag} Traducción a {$langName}");
|
||||||
|
$embed->setDescription($translatedText);
|
||||||
|
$embed->setColor("#5865F2");
|
||||||
|
$embed->setFooter("Traducido de {$sourceLang} • Solo tú puedes ver esto");
|
||||||
|
|
||||||
|
if (count($originalMessage->attachments) > 0) {
|
||||||
|
$firstAttachment = $originalMessage->attachments->first();
|
||||||
|
if ($firstAttachment && isset($firstAttachment->url) && strpos($firstAttachment->content_type ?? '', 'image/') === 0) {
|
||||||
|
$embed->setImage($firstAttachment->url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = MessageBuilder::new()
|
||||||
|
->setContent("Aquí tienes tu traducción (solo visible para ti):")
|
||||||
|
->addEmbed($embed);
|
||||||
|
$interaction->respondWithMessage($builder, true);
|
||||||
|
$logInteraction("[TRANSLATION] Usuario {$userId} tradujo mensaje de {$sourceLang} a {$targetLang}");
|
||||||
|
} else {
|
||||||
|
$interaction->respondWithMessage(MessageBuilder::new()->setContent("⚠️ No se pudo traducir el mensaje."), true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function ($error) use ($interaction, $logInteraction) {
|
||||||
|
$logInteraction("ERROR: [TRANSLATION] Error al obtener mensaje original", ['error' => $error->getMessage()]);
|
||||||
|
$interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ No se pudo obtener el mensaje original para traducir."), true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($customId, 'translate_template:') === 0) {
|
||||||
|
// Not implemented in this merge for now to keep it simple
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($customId === 'platicar_bot' || $customId === 'usar_ia') {
|
||||||
|
$newMode = ($customId === 'platicar_bot') ? 'bot' : 'ia';
|
||||||
|
$stmt = $db->prepare("UPDATE destinatarios_discord SET chat_mode = ? WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$newMode, $userId]);
|
||||||
|
$responseText = $newMode === 'bot'
|
||||||
|
? "🤖 Modo cambiado a 'Platicar con bot'. Ahora puedes usar los comandos normales como `/comandos`."
|
||||||
|
: "🧠 Modo cambiado a 'Usar IA'. Todo lo que escribas será procesado por la IA.\n\nEscribe `/agente` para volver a este menú.";
|
||||||
|
$interaction->respondWithMessage(MessageBuilder::new()->setContent($responseText), true);
|
||||||
|
$logInteraction("[MODO AGENTE] Usuario $userId cambió al modo: $newMode");
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$logInteraction("ERROR: [INTERACCION] Error al procesar un botón.", ['customId' => $customId, 'error' => $e->getMessage()]);
|
||||||
|
$interaction->respondWithMessage(MessageBuilder::new()->setContent("Hubo un error al procesar esta acción."), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$discord->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) use ($db, $logInteraction) {
|
||||||
|
if ($message->author->bot) return;
|
||||||
|
|
||||||
|
saveRecipientIfNotExists($db, $logInteraction, 'user', $message->author->id, $message->author->username);
|
||||||
|
$channelName = $message->channel->is_private ? 'DM con ' . $message->author->username : $message->channel->name;
|
||||||
|
if (!$message->channel->is_private && $message->channel->guild) {
|
||||||
|
$channelName = $message->channel->guild->name . ' - ' . $channelName;
|
||||||
|
}
|
||||||
|
saveRecipientIfNotExists($db, $logInteraction, 'channel', $message->channel->id, $channelName);
|
||||||
|
$logInteraction("[Mensaje Recibido] En canal '{$channelName}' de @{$message->author->username}: {$message->content}");
|
||||||
|
|
||||||
|
$isPrivateChat = $message->channel->is_private;
|
||||||
|
$userId = $message->author->id;
|
||||||
|
$content = $message->content;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($isPrivateChat) {
|
||||||
|
$stmt = $db->prepare("SELECT chat_mode FROM destinatarios_discord WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$userChatMode = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($userChatMode === false) {
|
||||||
|
$updateStmt = $db->prepare("UPDATE destinatarios_discord SET chat_mode = 'agent' WHERE discord_id = ?");
|
||||||
|
$updateStmt->execute([$userId]);
|
||||||
|
$userChatMode = 'agent';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim($content) === '/agente') {
|
||||||
|
$stmt = $db->prepare("UPDATE destinatarios_discord SET chat_mode = 'agent' WHERE discord_id = ?");
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$userChatMode = 'agent';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($userChatMode) {
|
||||||
|
case 'agent':
|
||||||
|
$builder = MessageBuilder::new()->setContent("👋 Hola! ¿Cómo quieres interactuar?");
|
||||||
|
$actionRow = ActionRow::new()
|
||||||
|
->addComponent(Button::new(Button::STYLE_PRIMARY, 'platicar_bot')->setLabel('🤖 Platicar con bot'))
|
||||||
|
->addComponent(Button::new(Button::STYLE_SUCCESS, 'usar_ia')->setLabel('🧠 Usar IA'));
|
||||||
|
$builder->addComponent($actionRow);
|
||||||
|
$message->channel->sendMessage($builder);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'ia':
|
||||||
|
$n8nWebhookUrl = $_ENV['N8N_IA_WEBHOOK_URL_DISCORD'] ?? null;
|
||||||
|
if ($n8nWebhookUrl) {
|
||||||
|
$postData = ['chat_id' => $message->channel_id, 'user_id' => $userId, 'message' => $content, 'name' => $message->author->username];
|
||||||
|
$ch = curl_init($n8nWebhookUrl);
|
||||||
|
curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($postData), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_TIMEOUT => 10 ]);
|
||||||
|
curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'bot':
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($content, '#') === 0 || strpos($content, '/') === 0) {
|
||||||
|
handleDiscordCommand($message, $db, $logInteraction);
|
||||||
|
} else if (strtolower($content) === '!ping') {
|
||||||
|
$message->reply('pong!');
|
||||||
|
} else {
|
||||||
|
handleDiscordTranslation($message, $db, $logInteraction);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$logInteraction("ERROR FATAL CAPTURADO", ['error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$discord->run();
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$logInteraction("CRITICAL: ERROR FATAL AL INICIAR", ['error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDiscordCommand(Message $message, PDO $db, $logInteraction)
|
||||||
|
{
|
||||||
|
$text = trim($message->content);
|
||||||
|
|
||||||
|
if (strpos($text, '#') === 0) {
|
||||||
|
$command = ltrim($text, '#'); // Command is "Saludos"
|
||||||
|
$logInteraction("[Comando] Usuario @{$message->author->username} solicitó: #{$command}");
|
||||||
|
try {
|
||||||
|
// Step 1: Look up command in comandos_discord to get plantilla_id
|
||||||
|
$stmt = $db->prepare("SELECT plantilla_id FROM comandos_discord WHERE comando = ?");
|
||||||
|
$stmt->execute([$command]);
|
||||||
|
$command_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($command_data && $command_data['plantilla_id']) {
|
||||||
|
$plantilla_id = $command_data['plantilla_id'];
|
||||||
|
// Step 2: Get template content from plantillas_discord
|
||||||
|
$stmt = $db->prepare("SELECT contenido FROM plantillas_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$plantilla_id]);
|
||||||
|
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($template) {
|
||||||
|
$sender = new DiscordSender($_ENV['DISCORD_BOT_TOKEN']);
|
||||||
|
$sender->sendMessage($message->channel_id, $template['contenido']);
|
||||||
|
} else {
|
||||||
|
$message->reply("El comando `#{$command}` fue encontrado, pero la plantilla asociada (ID: {$plantilla_id}) no existe.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message->reply("El comando `#{$command}` no fue encontrado.");
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$logInteraction("ERROR: [Comando] Procesando #{$command}", ['error' => $e->getMessage()]);
|
||||||
|
$message->reply("Ocurrió un error inesperado al procesar tu comando.");
|
||||||
|
}
|
||||||
|
} elseif (strpos($text, '/comandos') === 0) {
|
||||||
|
$stmt = $db->query("SELECT comando, nombre FROM plantillas_discord WHERE comando IS NOT NULL AND comando != '' ORDER BY nombre ASC");
|
||||||
|
$commands = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if (empty($commands)) {
|
||||||
|
$message->reply("ℹ️ No hay comandos personalizados disponibles.");
|
||||||
|
} else {
|
||||||
|
$response = "**LISTA DE COMANDOS DISPONIBLES (Modo Debug)**\n\n";
|
||||||
|
foreach ($commands as $cmd) {
|
||||||
|
$raw_command = $cmd['comando']; // The raw value from DB
|
||||||
|
$display_command = trim($raw_command);
|
||||||
|
if (strpos($display_command, '#') !== 0 && strpos($display_command, '/') !== 0) {
|
||||||
|
$display_command = '#' . $display_command;
|
||||||
|
}
|
||||||
|
$response .= "`" . $display_command . "` (DB: `" . $raw_command . "`) - " . trim($cmd['nombre']) . "\n";
|
||||||
|
}
|
||||||
|
$message->channel->sendMessage($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDiscordTranslation(Message $message, PDO $db, $logInteraction)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$langStmt = $db->query("SELECT codigo FROM idiomas WHERE activo = 1");
|
||||||
|
$activeLangsCount = $langStmt->rowCount();
|
||||||
|
|
||||||
|
if ($activeLangsCount < 2) return;
|
||||||
|
|
||||||
|
$button = Button::new(Button::STYLE_PRIMARY, 'translate_auto:' . $message->id)
|
||||||
|
->setLabel('Traducir / Translate')
|
||||||
|
->setEmoji('🌐');
|
||||||
|
|
||||||
|
$actionRow = ActionRow::new()->addComponent($button);
|
||||||
|
|
||||||
|
$builder = MessageBuilder::new()->addComponent($actionRow);
|
||||||
|
|
||||||
|
$message->reply($builder);
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$logInteraction("ERROR: [TRANSLATION_BUTTONS]", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
464
bot_telegram_daemon.php
Executable file
464
bot_telegram_daemon.php
Executable file
@@ -0,0 +1,464 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Telegram Bot Daemon
|
||||||
|
* Ejecutar con Supervisor: php bot_telegram_daemon.php
|
||||||
|
* Este demonio maneja interacciones en tiempo real y procesa mensajes programados y recurrentes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use TelegramBot\Api\BotApi;
|
||||||
|
use TelegramBot\Api\Types\Update;
|
||||||
|
use TelegramBot\Api\Types\Inline\InlineKeyboardMarkup;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Funciones de Ayuda ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene una conexión a la base de datos.
|
||||||
|
* @return PDO
|
||||||
|
*/
|
||||||
|
function getDBConnection() {
|
||||||
|
$host = $_ENV['DB_HOST'];
|
||||||
|
$db = $_ENV['DB_NAME'];
|
||||||
|
$user = $_ENV['DB_USER'];
|
||||||
|
$pass = $_ENV['DB_PASS'];
|
||||||
|
$port = $_ENV['DB_PORT'];
|
||||||
|
$charset = 'utf8mb4';
|
||||||
|
|
||||||
|
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset";
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
return new PDO($dsn, $user, $pass, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Procesa el contenido HTML para enviar a Telegram.
|
||||||
|
* @param string $htmlContent
|
||||||
|
* @return array ['cleanContent' => string, 'imageUrl' => ?string]
|
||||||
|
*/
|
||||||
|
function processContentForTelegram(string $htmlContent): array {
|
||||||
|
$dom = new DOMDocument();
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $htmlContent, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||||
|
libxml_clear_errors();
|
||||||
|
|
||||||
|
$imageUrl = null;
|
||||||
|
$images = $dom->getElementsByTagName('img');
|
||||||
|
if ($images->length > 0) {
|
||||||
|
$imageUrl = $images->item(0)->getAttribute('src');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpiar texto: quitar tags HTML y decodificar entidades
|
||||||
|
$cleanContent = strip_tags($htmlContent);
|
||||||
|
$cleanContent = html_entity_decode($cleanContent, ENT_QUOTES, 'UTF-8');
|
||||||
|
$cleanContent = trim($cleanContent);
|
||||||
|
|
||||||
|
return ['cleanContent' => $cleanContent, 'imageUrl' => $imageUrl];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envía un mensaje a un chat de Telegram.
|
||||||
|
* @param BotApi $bot
|
||||||
|
* @param string $chatId
|
||||||
|
* @param string $content
|
||||||
|
* @param ?string $imageUrl
|
||||||
|
* @return array|false Respuesta de la API de Telegram o false si falla.
|
||||||
|
*/
|
||||||
|
function sendMessageTelegram(BotApi $bot, string $chatId, string $content, ?string $imageUrl = null) {
|
||||||
|
try {
|
||||||
|
if ($imageUrl) {
|
||||||
|
return $bot->sendPhoto($chatId, $imageUrl, $content);
|
||||||
|
} else {
|
||||||
|
return $bot->sendMessage($chatId, $content);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("Error enviando mensaje Telegram a $chatId: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula la próxima fecha de envío para un mensaje recurrente.
|
||||||
|
* @param array $recurrence
|
||||||
|
* @return Carbon
|
||||||
|
*/
|
||||||
|
function calculateNextSendTime(array $recurrence): Carbon {
|
||||||
|
$tz = $_ENV['TIME_ZONE_ENVIOS'] ?? 'UTC';
|
||||||
|
$time = $recurrence['hora_envio']; // HH:MM:SS
|
||||||
|
|
||||||
|
// Iniciar con la hora actual en la zona horaria correcta y luego ajustar la hora del día
|
||||||
|
$next = Carbon::now($tz)->setTimeFromTimeString($time);
|
||||||
|
|
||||||
|
switch ($recurrence['frecuencia']) {
|
||||||
|
case 'diario':
|
||||||
|
// Si la hora ya pasó hoy, programar para mañana
|
||||||
|
if ($next->isPast()) {
|
||||||
|
$next->addDay();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'semanal':
|
||||||
|
$dayOfWeekIso = (int)$recurrence['dia_semana']; // Lunes=1, ..., Domingo=7
|
||||||
|
|
||||||
|
// Establecer el día de la semana
|
||||||
|
$next->dayOfWeekIso($dayOfWeekIso);
|
||||||
|
|
||||||
|
// Si la fecha/hora resultante ya pasó esta semana, ir a la semana que viene.
|
||||||
|
if ($next->isPast()) {
|
||||||
|
$next->addWeek();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'mensual':
|
||||||
|
$dayOfMonth = (int)$recurrence['dia_mes'];
|
||||||
|
|
||||||
|
// Si el día actual es mayor que el día programado, o si es el mismo día pero la hora ya pasó,
|
||||||
|
// primero avanza al próximo mes para evitar errores.
|
||||||
|
if (Carbon::now($tz)->day > $dayOfMonth || (Carbon::now($tz)->day == $dayOfMonth && $next->isPast())) {
|
||||||
|
$next->addMonthNoOverflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el día del mes. Usar `day()` es más seguro en caso de meses con menos días.
|
||||||
|
$next->day(min($dayOfMonth, $next->daysInMonth));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Convertir a UTC para la base de datos
|
||||||
|
return $next->setTimezone('UTC');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Procesa los mensajes programados que están pendientes de envío para Telegram.
|
||||||
|
* @param PDO $db
|
||||||
|
* @param BotApi $bot
|
||||||
|
*/
|
||||||
|
function processScheduledMessagesTelegram(PDO $db, BotApi $bot) {
|
||||||
|
echo "[Telegram Scheduler] Verificando mensajes programados..." . PHP_EOL;
|
||||||
|
$stmt = $db->prepare("SELECT * FROM mensajes_telegram WHERE estado = 'pendiente' AND tipo_envio = 'programado' AND fecha_envio <= UTC_TIMESTAMP()");
|
||||||
|
$stmt->execute();
|
||||||
|
$messages = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($messages as $msg) {
|
||||||
|
echo "[Telegram Scheduler] Procesando mensaje programado ID: {$msg['id']}" . PHP_EOL;
|
||||||
|
$contentPayload = processContentForTelegram($msg['contenido']);
|
||||||
|
|
||||||
|
$telegramResponse = sendMessageTelegram($bot, $msg['chat_id'], $contentPayload['cleanContent'], $contentPayload['imageUrl']);
|
||||||
|
|
||||||
|
if ($telegramResponse) {
|
||||||
|
$updateStmt = $db->prepare("UPDATE mensajes_telegram SET estado = 'enviado', mensaje_telegram_id = ? WHERE id = ?");
|
||||||
|
$updateStmt->execute([$telegramResponse->getMessageId(), $msg['id']]);
|
||||||
|
echo "[Telegram Scheduler] Mensaje programado ID: {$msg['id']} enviado." . PHP_EOL;
|
||||||
|
} else {
|
||||||
|
$updateStmt = $db->prepare("UPDATE mensajes_telegram SET estado = 'fallido' WHERE id = ?");
|
||||||
|
$updateStmt->execute([$msg['id']]);
|
||||||
|
echo "[Telegram Scheduler] Error al enviar mensaje programado ID: {$msg['id']}." . PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Procesa los mensajes recurrentes para Telegram.
|
||||||
|
* @param PDO $db
|
||||||
|
* @param BotApi $bot
|
||||||
|
*/
|
||||||
|
function processRecurringMessagesTelegram(PDO $db, BotApi $bot) {
|
||||||
|
echo "[Telegram Scheduler] Verificando mensajes recurrentes..." . PHP_EOL;
|
||||||
|
|
||||||
|
// 1. Inicializar `proximo_envio` para nuevos mensajes recurrentes
|
||||||
|
$initStmt = $db->query("SELECT * FROM recurrentes_telegram WHERE proximo_envio IS NULL AND activo = 1");
|
||||||
|
foreach ($initStmt->fetchAll() as $recurrence) {
|
||||||
|
$nextSendTime = calculateNextSendTime($recurrence);
|
||||||
|
$updateStmt = $db->prepare("UPDATE recurrentes_telegram SET proximo_envio = ? WHERE id = ?");
|
||||||
|
$updateStmt->execute([$nextSendTime->toDateTimeString(), $recurrence['id']]);
|
||||||
|
echo "[Telegram Scheduler] Inicializado proximo_envio para recurrente ID: {$recurrence['id']}" . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Procesar mensajes recurrentes listos para ser enviados
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT r.*, m.contenido, m.chat_id, m.usuario_id
|
||||||
|
FROM recurrentes_telegram r
|
||||||
|
JOIN mensajes_telegram m ON r.mensaje_id = m.id
|
||||||
|
WHERE r.activo = 1 AND r.proximo_envio <= UTC_TIMESTAMP()
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$recurrences = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($recurrences as $rec) {
|
||||||
|
echo "[Telegram Scheduler] Procesando mensaje recurrente ID: {$rec['id']}" . PHP_EOL;
|
||||||
|
$contentPayload = processContentForTelegram($rec['contenido']);
|
||||||
|
|
||||||
|
$telegramResponse = sendMessageTelegram($bot, $rec['chat_id'], $contentPayload['cleanContent'], $contentPayload['imageUrl']);
|
||||||
|
|
||||||
|
if ($telegramResponse) {
|
||||||
|
// Registrar este envío específico en el historial
|
||||||
|
$logStmt = $db->prepare("
|
||||||
|
INSERT INTO mensajes_telegram (usuario_id, chat_id, contenido, estado, mensaje_telegram_id, fecha_envio, tipo_envio)
|
||||||
|
VALUES (?, ?, ?, 'enviado', ?, NOW(), 'recurrente_enviado')
|
||||||
|
");
|
||||||
|
$logStmt->execute([$rec['usuario_id'], $rec['chat_id'], $rec['contenido'], $telegramResponse->getMessageId()]);
|
||||||
|
|
||||||
|
// Calcular y actualizar el próximo envío
|
||||||
|
$nextSendTime = calculateNextSendTime($rec);
|
||||||
|
$updateStmt = $db->prepare("UPDATE recurrentes_telegram SET proximo_envio = ? WHERE id = ?");
|
||||||
|
$updateStmt->execute([$nextSendTime->toDateTimeString(), $rec['id']]);
|
||||||
|
|
||||||
|
echo "[Telegram Scheduler] Mensaje recurrente ID: {$rec['id']} enviado. Próximo envío: " . $nextSendTime->toDateTimeString() . PHP_EOL;
|
||||||
|
} else {
|
||||||
|
error_log("[Telegram Scheduler] Error al enviar mensaje recurrente ID: {$rec['id']}.");
|
||||||
|
// No se actualiza el proximo_envio para reintentar en la próxima ejecución si falló
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra o actualiza un destinatario de Telegram en la base de datos.
|
||||||
|
* @param PDO $db
|
||||||
|
* @param string $id
|
||||||
|
* @param string $type
|
||||||
|
* @param ?string $name
|
||||||
|
* @param ?string $username
|
||||||
|
*/
|
||||||
|
function registerTelegramRecipient(PDO $db, $id, $type, $name, $username = null) {
|
||||||
|
if (empty($id)) return;
|
||||||
|
|
||||||
|
$name = $name ?: ($username ?: "Destinatario {$id}");
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT id FROM destinatarios_telegram WHERE telegram_id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
// Actualizar
|
||||||
|
$updateStmt = $db->prepare(
|
||||||
|
"UPDATE destinatarios_telegram SET nombre = ?, username = ?, ultima_interaccion = NOW() WHERE telegram_id = ?"
|
||||||
|
);
|
||||||
|
$updateStmt->execute([$name, $username, $id]);
|
||||||
|
} else {
|
||||||
|
// Insertar
|
||||||
|
$insertStmt = $db->prepare(
|
||||||
|
"INSERT INTO destinatarios_telegram (telegram_id, tipo, nombre, username, fecha_registro, ultima_interaccion) VALUES (?, ?, ?, ?, NOW(), NOW())"
|
||||||
|
);
|
||||||
|
$insertStmt->execute([$id, $type, $name, $username]);
|
||||||
|
echo "[Recipient Registrar] Nuevo destinatario: {$name} ({$id}) de tipo {$type}" . PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Inicio del Bot de Polling ---
|
||||||
|
|
||||||
|
$bot = new BotApi($_ENV['TELEGRAM_BOT_TOKEN']);
|
||||||
|
|
||||||
|
echo "Telegram Bot iniciado", PHP_EOL;
|
||||||
|
|
||||||
|
$offset = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
$updates = $bot->getUpdates($offset, 100, 1);
|
||||||
|
|
||||||
|
$db = getDBConnection(); // Obtener conexión a DB en cada iteración del bucle (o manejar persistencia)
|
||||||
|
|
||||||
|
// --- Procesar mensajes programados y recurrentes ---
|
||||||
|
processScheduledMessagesTelegram($db, $bot);
|
||||||
|
processRecurringMessagesTelegram($db, $bot);
|
||||||
|
|
||||||
|
|
||||||
|
foreach ($updates as $update) {
|
||||||
|
$offset = $update->getUpdateId() + 1;
|
||||||
|
|
||||||
|
// --- Registrar/Actualizar Destinatarios ---
|
||||||
|
$message = $update->getMessage();
|
||||||
|
$callbackQuery = $update->getCallbackQuery();
|
||||||
|
|
||||||
|
$chat = null;
|
||||||
|
$user = null;
|
||||||
|
|
||||||
|
if ($message) {
|
||||||
|
$chat = $message->getChat();
|
||||||
|
$user = $message->getFrom();
|
||||||
|
} elseif ($callbackQuery) {
|
||||||
|
$chat = $callbackQuery->getMessage()->getChat();
|
||||||
|
$user = $callbackQuery->getFrom();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($chat) {
|
||||||
|
registerTelegramRecipient(
|
||||||
|
$db,
|
||||||
|
$chat->getId(),
|
||||||
|
$chat->getType(),
|
||||||
|
$chat->getTitle() ?: $chat->getFirstName(),
|
||||||
|
$chat->getUsername()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user && (!$chat || $user->getId() != $chat->getId())) {
|
||||||
|
registerTelegramRecipient(
|
||||||
|
$db,
|
||||||
|
$user->getId(),
|
||||||
|
'usuario',
|
||||||
|
$user->getFirstName(),
|
||||||
|
$user->getUsername()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejar comandos de texto
|
||||||
|
if ($message && !empty($message->getText())) {
|
||||||
|
$text = $message->getText();
|
||||||
|
if (strpos($text, '/') === 0) { // Es un comando
|
||||||
|
$command = explode(' ', $text)[0];
|
||||||
|
$chatId = $message->getChat()->getId();
|
||||||
|
$user = $message->getFrom();
|
||||||
|
|
||||||
|
switch ($command) {
|
||||||
|
case '/start':
|
||||||
|
// Reutilizar la lógica de bienvenida para el comando /start
|
||||||
|
sendWelcomeMessage($bot, $chatId, $user);
|
||||||
|
break;
|
||||||
|
case '/help':
|
||||||
|
$responseText = "Comandos disponibles:\n/start - Muestra el mensaje de bienvenida.\n/help - Muestra esta ayuda.";
|
||||||
|
sendMessageTelegram($bot, $chatId, $responseText);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Comprobar si es un comando de plantilla
|
||||||
|
$cmdName = ltrim($command, '/');
|
||||||
|
$stmt = $db->prepare("SELECT contenido FROM plantillas_telegram WHERE comando = ?");
|
||||||
|
$stmt->execute([$cmdName]);
|
||||||
|
$plantilla = $stmt->fetch();
|
||||||
|
if ($plantilla) {
|
||||||
|
$contentPayload = processContentForTelegram($plantilla['contenido']);
|
||||||
|
sendMessageTelegram($bot, $chatId, $contentPayload['cleanContent'], $contentPayload['imageUrl']);
|
||||||
|
} else {
|
||||||
|
sendMessageTelegram($bot, $chatId, "Comando no reconocido.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejar callback queries (botones de idioma)
|
||||||
|
if ($callbackQuery) {
|
||||||
|
$userId = $callbackQuery->getFrom()->getId();
|
||||||
|
$username = $callbackQuery->getFrom()->getUsername() ?? $callbackQuery->getFrom()->getFirstName();
|
||||||
|
$data = $callbackQuery->getData();
|
||||||
|
|
||||||
|
echo "Callback recibido de $username: $data", PHP_EOL;
|
||||||
|
|
||||||
|
if (strpos($data, 'lang_select_') === 0) {
|
||||||
|
$langCode = substr($data, strlen('lang_select_'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// La lógica para guardar el idioma ya está cubierta por registerTelegramRecipient,
|
||||||
|
// pero aquí actualizamos específicamente 'idioma_detectado'.
|
||||||
|
$stmt = $db->prepare("UPDATE destinatarios_telegram SET idioma_detectado = ? WHERE telegram_id = ?");
|
||||||
|
$stmt->execute([$langCode, $userId]);
|
||||||
|
|
||||||
|
echo "Preferencia de idioma guardada en BD para $username.", PHP_EOL;
|
||||||
|
|
||||||
|
$bot->answerCallbackQuery($callbackQuery->getId(), "✅ Idioma seleccionado: $langCode", true);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "Error guardando preferencia de idioma: " . $e->getMessage(), PHP_EOL;
|
||||||
|
$bot->answerCallbackQuery($callbackQuery->getId(), "Error guardando preferencia", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejar nuevos miembros (bienvenida)
|
||||||
|
if ($message && $newMembers = $message->getNewChatMembers()) {
|
||||||
|
foreach ($newMembers as $member) {
|
||||||
|
sendWelcomeMessage($bot, $message->getChat()->getId(), $member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(500000); // 0.5 segundos de pausa para no saturar la API
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "Error en polling: " . $e->getMessage(), PHP_EOL;
|
||||||
|
|
||||||
|
// Log error
|
||||||
|
try {
|
||||||
|
$db = getDBConnection();
|
||||||
|
$stmt = $db->prepare("INSERT INTO logs_telegram (origen, nivel, descripcion, datos_json) VALUES ('bot', 'error', ?, ?)");
|
||||||
|
$stmt->execute([
|
||||||
|
"Error en polling: " . $e->getMessage(),
|
||||||
|
json_encode(['trace' => $e->getTraceAsString()])
|
||||||
|
]);
|
||||||
|
} catch (\Exception $ex) {}
|
||||||
|
|
||||||
|
sleep(5); // Esperar un poco más en caso de error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWelcomeMessage($bot, $chatId, $user) {
|
||||||
|
try {
|
||||||
|
$db = getDBConnection(); // Obtener conexión a DB
|
||||||
|
|
||||||
|
// Obtener configuración de bienvenida
|
||||||
|
$stmt = $db->query("SELECT * FROM bienvenida_telegram WHERE activo = 1 LIMIT 1");
|
||||||
|
$config = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$config) return;
|
||||||
|
|
||||||
|
// Obtener idiomas activos
|
||||||
|
$stmt = $db->query("SELECT codigo, nombre, nombre_nativo, bandera FROM idiomas WHERE activo = 1 ORDER BY nombre ASC");
|
||||||
|
$idiomas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Preparar mensaje
|
||||||
|
$username = $user->getUsername() ? '@' . $user->getUsername() : $user->getFirstName();
|
||||||
|
$texto = str_replace('{usuario}', $username, $config['texto']);
|
||||||
|
|
||||||
|
// Limpiar HTML simple
|
||||||
|
$texto = strip_tags($texto);
|
||||||
|
|
||||||
|
// Crear botones de idioma
|
||||||
|
$keyboard = [];
|
||||||
|
$currentRow = [];
|
||||||
|
foreach ($idiomas as $lang) {
|
||||||
|
if (count($currentRow) >= 3) {
|
||||||
|
$keyboard[] = $currentRow;
|
||||||
|
$currentRow = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = $lang['bandera'] ?: $lang['nombre'];
|
||||||
|
$currentRow[] = [
|
||||||
|
'text' => $label,
|
||||||
|
'callback_data' => 'lang_select_' . $lang['codigo']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (!empty($currentRow)) {
|
||||||
|
$keyboard[] = $currentRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear el objeto de markup para los botones
|
||||||
|
$replyMarkup = new InlineKeyboardMarkup($keyboard);
|
||||||
|
|
||||||
|
// Enviar mensaje
|
||||||
|
$bot->sendMessage(
|
||||||
|
$chatId,
|
||||||
|
$texto,
|
||||||
|
'HTML', // Usar HTML parse mode para el mensaje de bienvenida
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
$replyMarkup
|
||||||
|
);
|
||||||
|
|
||||||
|
echo "Mensaje de bienvenida enviado a $username", PHP_EOL;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "Error enviando bienvenida: " . $e->getMessage(), PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
check_idiomas.php
Executable file
50
check_idiomas.php
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/shared/database/connection.php';
|
||||||
|
|
||||||
|
if (file_exists(__DIR__ . '/.env')) {
|
||||||
|
$lines = file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || strpos($line, '#') === 0) continue;
|
||||||
|
if (strpos($line, '=') === false) continue;
|
||||||
|
list($key, $value) = explode('=', $line, 2);
|
||||||
|
$_ENV[trim($key)] = trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
echo "=== IDIOMAS EN LA BASE DE DATOS ===\n\n";
|
||||||
|
|
||||||
|
$stmt = $db->query("SELECT * FROM idiomas ORDER BY nombre ASC");
|
||||||
|
$idiomas = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo "Total de idiomas: " . count($idiomas) . "\n";
|
||||||
|
echo "Idiomas ACTIVOS: " . count(array_filter($idiomas, fn($i) => $i['activo'] == 1)) . "\n\n";
|
||||||
|
|
||||||
|
foreach ($idiomas as $idioma) {
|
||||||
|
$estado = $idioma['activo'] ? '✅ ACTIVO' : '❌ INACTIVO';
|
||||||
|
$bandera = $idioma['bandera'] ?? '🏳️';
|
||||||
|
echo sprintf("%s %s - %s (%s) %s\n",
|
||||||
|
$bandera,
|
||||||
|
$idioma['nombre'],
|
||||||
|
$idioma['codigo'],
|
||||||
|
$idioma['nombre_nativo'] ?? 'N/A',
|
||||||
|
$estado
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== DIAGNÓSTICO ===\n";
|
||||||
|
$activos = count(array_filter($idiomas, fn($i) => $i['activo'] == 1));
|
||||||
|
if ($activos < 2) {
|
||||||
|
echo "⚠️ PROBLEMA ENCONTRADO: Solo hay $activos idioma(s) activo(s).\n";
|
||||||
|
echo "El bot necesita AL MENOS 2 idiomas activos para ofrecer traducción.\n";
|
||||||
|
echo "\nSOLUCIÓN: Ve a /shared/languages/manager.php y activa más idiomas.\n";
|
||||||
|
} else {
|
||||||
|
echo "✅ Tienes $activos idiomas activos. Esto es suficiente.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "ERROR: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
63
check_intents.php
Executable file
63
check_intents.php
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
// Script para verificar los Intents del bot
|
||||||
|
require_once __DIR__ . '/.env';
|
||||||
|
|
||||||
|
if (file_exists(__DIR__ . '/.env')) {
|
||||||
|
$lines = file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || strpos($line, '#') === 0) continue;
|
||||||
|
if (strpos($line, '=') === false) continue;
|
||||||
|
list($key, $value) = explode('=', $line, 2);
|
||||||
|
$_ENV[trim($key)] = trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $_ENV['DISCORD_BOT_TOKEN'] ?? '';
|
||||||
|
if (empty($token)) {
|
||||||
|
die("ERROR: No se encontró DISCORD_BOT_TOKEN en .env\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener información de la aplicación
|
||||||
|
$ch = curl_init('https://discord.com/api/v10/applications/@me');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Authorization: Bot ' . $token,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
die("ERROR HTTP $httpCode: $response\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = json_decode($response, true);
|
||||||
|
|
||||||
|
echo "=== INFORMACIÓN DE LA APLICACIÓN ===\n";
|
||||||
|
echo "ID: " . $app['id'] . "\n";
|
||||||
|
echo "Nombre: " . $app['name'] . "\n";
|
||||||
|
echo "Owner ID: " . $app['owner']['id'] . "\n\n";
|
||||||
|
|
||||||
|
echo "=== PRIVILEGED INTENTS (en el Portal) ===\n";
|
||||||
|
echo "Por favor, ve al portal y verifica:\n";
|
||||||
|
echo "https://discord.com/developers/applications/{$app['id']}/bot\n\n";
|
||||||
|
|
||||||
|
echo "Debe estar activado:\n";
|
||||||
|
echo " ✓ PRESENCE INTENT\n";
|
||||||
|
echo " ✓ SERVER MEMBERS INTENT\n";
|
||||||
|
echo " ✓ MESSAGE CONTENT INTENT ← **CRÍTICO**\n\n";
|
||||||
|
|
||||||
|
echo "=== INTENTS DEL CÓDIGO ===\n";
|
||||||
|
echo "El bot_daemon.php está solicitando:\n";
|
||||||
|
echo " - GUILD_MESSAGES\n";
|
||||||
|
echo " - DIRECT_MESSAGES\n";
|
||||||
|
echo " - MESSAGE_CONTENT\n";
|
||||||
|
echo " - GUILD_MEMBERS\n\n";
|
||||||
|
|
||||||
|
echo "Si MESSAGE CONTENT INTENT no está activado en el portal,\n";
|
||||||
|
echo "Discord BLOQUEARÁ todos los eventos MESSAGE_CREATE.\n";
|
||||||
24
composer.json
Executable file
24
composer.json
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "bot/discord-telegram-manager",
|
||||||
|
"description": "Sistema de administración para bots de Discord y Telegram",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0",
|
||||||
|
"firebase/php-jwt": "^6.0",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"ext-pdo_mysql": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-curl": "*",
|
||||||
|
"team-reflex/discord-php": "^10.40",
|
||||||
|
"telegram-bot/api": "^2.5"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Shared\\": "shared/",
|
||||||
|
"Discord\\": "discord/",
|
||||||
|
"Telegram\\": "telegram/",
|
||||||
|
"Gallery\\": "gallery/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2721
composer.lock
generated
Executable file
2721
composer.lock
generated
Executable file
File diff suppressed because it is too large
Load Diff
110
create_admin.php
Executable file
110
create_admin.php
Executable file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script para crear el primer usuario administrador
|
||||||
|
* Ejecutar una sola vez: php create_admin.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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__ . '/shared/database/connection.php';
|
||||||
|
|
||||||
|
echo "========================================\n";
|
||||||
|
echo " CREAR USUARIO ADMINISTRADOR\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Solicitar datos del usuario
|
||||||
|
echo "Ingrese el nombre de usuario: ";
|
||||||
|
$username = trim(fgets(STDIN));
|
||||||
|
|
||||||
|
echo "Ingrese el email: ";
|
||||||
|
$email = trim(fgets(STDIN));
|
||||||
|
|
||||||
|
echo "Ingrese la contraseña: ";
|
||||||
|
$password = trim(fgets(STDIN));
|
||||||
|
|
||||||
|
echo "Ingrese el nombre completo: ";
|
||||||
|
$nombreCompleto = trim(fgets(STDIN));
|
||||||
|
|
||||||
|
if (empty($username) || empty($email) || empty($password)) {
|
||||||
|
die("❌ Error: Todos los campos son obligatorios\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el usuario ya existe
|
||||||
|
$stmt = $db->prepare("SELECT id FROM usuarios WHERE username = ? OR email = ?");
|
||||||
|
$stmt->execute([$username, $email]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
die("❌ Error: Ya existe un usuario con ese nombre de usuario o email\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener el rol de Admin
|
||||||
|
$stmt = $db->query("SELECT id FROM roles WHERE nombre = 'Admin' LIMIT 1");
|
||||||
|
$rol = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$rol) {
|
||||||
|
die("❌ Error: No se encontró el rol de Admin. Ejecute primero el schema.sql\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$rolId = $rol['id'];
|
||||||
|
|
||||||
|
// Obtener el idioma español por defecto
|
||||||
|
$stmt = $db->query("SELECT id FROM idiomas WHERE codigo = 'es' LIMIT 1");
|
||||||
|
$idioma = $stmt->fetch();
|
||||||
|
$idiomaId = $idioma ? $idioma['id'] : 1;
|
||||||
|
|
||||||
|
// Hash de la contraseña
|
||||||
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// Insertar usuario
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO usuarios (username, email, password, nombre_completo, rol_id, idioma_id, activo)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, 1)
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$username,
|
||||||
|
$email,
|
||||||
|
$passwordHash,
|
||||||
|
$nombreCompleto,
|
||||||
|
$rolId,
|
||||||
|
$idiomaId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$userId = $db->lastInsertId();
|
||||||
|
|
||||||
|
// Asignar todos los permisos al admin
|
||||||
|
$stmt = $db->query("SELECT id FROM permisos");
|
||||||
|
$permisos = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$stmtPermiso = $db->prepare("INSERT INTO usuarios_permisos (usuario_id, permiso_id) VALUES (?, ?)");
|
||||||
|
foreach ($permisos as $permisoId) {
|
||||||
|
$stmtPermiso->execute([$userId, $permisoId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n========================================\n";
|
||||||
|
echo "✅ Usuario administrador creado exitosamente\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
echo "Usuario: $username\n";
|
||||||
|
echo "Email: $email\n";
|
||||||
|
echo "Rol: Admin\n";
|
||||||
|
echo "ID: $userId\n";
|
||||||
|
echo "\nYa puede iniciar sesión en el sistema.\n\n";
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "❌ Error de base de datos: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "❌ Error: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
35
discord/api/commands/create.php
Executable file
35
discord/api/commands/create.php
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$comando = trim($input['comando'] ?? '');
|
||||||
|
$descripcion = trim($input['descripcion'] ?? '');
|
||||||
|
$plantilla_id = $input['plantilla_id'] ?? null;
|
||||||
|
|
||||||
|
if (empty($comando) || empty($plantilla_id)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Faltan datos requeridos'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO comandos_discord (comando, descripcion, plantilla_id)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([$comando, $descripcion, $plantilla_id]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
if ($e->getCode() == 23000) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Este comando ya existe'], 409);
|
||||||
|
}
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
27
discord/api/commands/delete.php
Executable file
27
discord/api/commands/delete.php
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $input['id'] ?? null;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'ID requerido'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("DELETE FROM comandos_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
104
discord/api/messages/delete.php
Executable file
104
discord/api/messages/delete.php
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Eliminar/Deshabilitar Mensaje Discord
|
||||||
|
* Cambia el estado del mensaje a 'deshabilitado' y opcionalmente lo elimina de Discord.
|
||||||
|
*/
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Habilitar errores para debug (quitar en producción estricta)
|
||||||
|
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
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$messageId = $input['id'] ?? null;
|
||||||
|
|
||||||
|
if (empty($messageId) || !is_numeric($messageId)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'ID de mensaje inválido o no proporcionado'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// 1. Obtener detalles del mensaje para verificar permisos y Discord ID
|
||||||
|
$stmt = $db->prepare("SELECT usuario_id, canal_id, mensaje_discord_id, tipo_envio FROM mensajes_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$messageId]);
|
||||||
|
$message = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$message) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Mensaje no encontrado'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permisos: solo Admin o el propietario pueden eliminar/deshabilitar
|
||||||
|
if ($userData->rol !== 'Admin' && $message['usuario_id'] != $userData->userId) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No tiene permisos para eliminar este mensaje'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Intentar eliminar de Discord si existe un mensaje_discord_id y fue un envío inmediato
|
||||||
|
$discordDeletionSuccess = true;
|
||||||
|
$discordDeletionError = null;
|
||||||
|
|
||||||
|
if (!empty($message['mensaje_discord_id']) && $message['tipo_envio'] === 'inmediato') {
|
||||||
|
$botToken = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||||
|
if (!$botToken) {
|
||||||
|
logToFile('discord/errors.log', "Error eliminando mensaje de Discord: Token de bot no configurado.", 'ERROR');
|
||||||
|
$discordDeletionError = "Token de bot no configurado, no se pudo eliminar de Discord.";
|
||||||
|
$discordDeletionSuccess = false;
|
||||||
|
} else {
|
||||||
|
$url = "https://discord.com/api/v10/channels/{$message['canal_id']}/messages/{$message['mensaje_discord_id']}";
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Authorization: Bot ' . $botToken,
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
$discordDeletionError = "Error cURL al eliminar de Discord: " . $curlError;
|
||||||
|
$discordDeletionSuccess = false;
|
||||||
|
} elseif ($httpCode !== 204) { // 204 No Content es la respuesta esperada para DELETE exitoso
|
||||||
|
$responseJson = json_decode($response, true);
|
||||||
|
$discordDeletionError = $responseJson['message'] ?? 'Error desconocido de Discord al eliminar';
|
||||||
|
$discordDeletionSuccess = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Deshabilitar el mensaje en la base de datos
|
||||||
|
// Si es un mensaje programado/recurrente pendiente, simplemente lo deshabilitamos sin intentar borrar de Discord
|
||||||
|
$updateStmt = $db->prepare("UPDATE mensajes_discord SET estado = 'deshabilitado' WHERE id = ?");
|
||||||
|
$updateStmt->execute([$messageId]);
|
||||||
|
|
||||||
|
logToFile('discord/messages.log', "Mensaje deshabilitado (ID: {$messageId}) por Usuario: {$userData->username}. Discord deletion: " . ($discordDeletionSuccess ? 'OK' : 'Fallido/' . $discordDeletionError));
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Mensaje deshabilitado correctamente.',
|
||||||
|
'discord_deletion_attempted' => !empty($message['mensaje_discord_id']) && $message['tipo_envio'] === 'inmediato',
|
||||||
|
'discord_deletion_success' => $discordDeletionSuccess,
|
||||||
|
'discord_deletion_error' => $discordDeletionError
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logToFile('discord/errors.log', "Error deshabilitando mensaje: " . $e->getMessage(), 'ERROR');
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Ocurrió un error en el servidor.'], 500);
|
||||||
|
}
|
||||||
7
discord/api/messages/php_errors.log
Executable file
7
discord/api/messages/php_errors.log
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
[30-Nov-2025 16:17:06 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[30-Nov-2025 16:20:55 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[30-Nov-2025 16:22:33 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[30-Nov-2025 16:34:09 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[30-Nov-2025 16:34:29 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[30-Nov-2025 17:02:10 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[03-Dec-2025 20:51:19 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
49
discord/api/messages/retry.php
Executable file
49
discord/api/messages/retry.php
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Reintentar Mensaje Discord
|
||||||
|
* Cambia el estado de un mensaje a 'pendiente'.
|
||||||
|
*/
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso (reutilizamos 'send_messages' ya que es una acción relacionada)
|
||||||
|
if (!hasPermission('send_messages', 'discord')) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No tienes permiso para gestionar mensajes de Discord.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$messageId = $input['id'] ?? null;
|
||||||
|
|
||||||
|
if (empty($messageId)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Falta el ID del mensaje.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Cambiar el estado del mensaje a 'pendiente'
|
||||||
|
$stmt = $db->prepare("UPDATE mensajes_discord SET estado = 'pendiente' WHERE id = ?");
|
||||||
|
$stmt->execute([$messageId]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() > 0) {
|
||||||
|
jsonResponse(['success' => true, 'message' => 'Mensaje marcado como pendiente.']);
|
||||||
|
} else {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No se encontró el mensaje o no se pudo actualizar.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Error en la base de datos: ' . $e->getMessage()], 500);
|
||||||
|
}
|
||||||
221
discord/api/messages/send.php
Executable file
221
discord/api/messages/send.php
Executable file
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Enviar Mensaje Discord
|
||||||
|
*/
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Habilitar errores para debug (quitar en producción estricta)
|
||||||
|
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
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso
|
||||||
|
if (!hasPermission('send_messages', 'discord')) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No tienes permiso para enviar mensajes de Discord.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$destinatariosInput = $input['destinatario_id'] ?? []; // Puede ser un string o un array
|
||||||
|
$contenido = $input['contenido'] ?? '';
|
||||||
|
$tipoEnvio = $input['tipo_envio'] ?? 'inmediato'; // 'inmediato', 'programado', 'recurrente'
|
||||||
|
|
||||||
|
// Asegurarse de que $destinatariosInput sea siempre un array
|
||||||
|
if (!is_array($destinatariosInput)) {
|
||||||
|
$destinatariosInput = [$destinatariosInput];
|
||||||
|
}
|
||||||
|
$destinatarios = array_filter(array_map('trim', $destinatariosInput)); // Limpiar y filtrar vacíos
|
||||||
|
|
||||||
|
if (empty($destinatarios) || empty($contenido)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Faltan datos requeridos: destinatario(s) y contenido'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Procesar contenido HTML para extraer imágenes y limpiar texto (una vez para todos)
|
||||||
|
$dom = new DOMDocument();
|
||||||
|
// Suprimir errores de HTML mal formado y usar UTF-8
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $contenido, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||||
|
libxml_clear_errors();
|
||||||
|
|
||||||
|
$embeds = [];
|
||||||
|
$images = $dom->getElementsByTagName('img');
|
||||||
|
|
||||||
|
// Extraer hasta 10 imágenes (límite de Discord)
|
||||||
|
$count = 0;
|
||||||
|
foreach ($images as $img) {
|
||||||
|
if ($count >= 10) break;
|
||||||
|
$src = $img->getAttribute('src');
|
||||||
|
if ($src) {
|
||||||
|
$embeds[] = [
|
||||||
|
'image' => ['url' => $src]
|
||||||
|
];
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpiar texto: convertir saltos de línea y eliminar tags
|
||||||
|
$cleanContent = str_replace(['<br>', '<br/>', '<p>'], ["\n", "\n", "\n"], $contenido);
|
||||||
|
$cleanContent = strip_tags($cleanContent);
|
||||||
|
$cleanContent = html_entity_decode($cleanContent);
|
||||||
|
$cleanContent = trim($cleanContent);
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$botToken = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||||
|
|
||||||
|
if (!$botToken) {
|
||||||
|
throw new Exception("Token de bot no configurado");
|
||||||
|
}
|
||||||
|
|
||||||
|
$allResults = [];
|
||||||
|
|
||||||
|
foreach ($destinatarios as $destinatarioId) {
|
||||||
|
$messageStatus = 'pendiente'; // Estado por defecto para programados/recurrentes
|
||||||
|
$discordMessageId = null;
|
||||||
|
$errorMessage = null;
|
||||||
|
|
||||||
|
if ($tipoEnvio === 'inmediato') {
|
||||||
|
$url = "https://discord.com/api/v10/channels/{$destinatarioId}/messages";
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'content' => $cleanContent
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($embeds)) {
|
||||||
|
$data['embeds'] = $embeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Authorization: Bot ' . $botToken,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$responseJson = json_decode($response, true);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
$errorMessage = "Error cURL: " . $curlError;
|
||||||
|
} elseif ($httpCode >= 400) {
|
||||||
|
$errorMessage = $responseJson['message'] ?? 'Error desconocido de Discord';
|
||||||
|
if (isset($responseJson['errors'])) {
|
||||||
|
$errorMessage .= ' - ' . json_encode($responseJson['errors']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$messageStatus = 'enviado';
|
||||||
|
$discordMessageId = $responseJson['id'] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Guardar en base de datos (Historial) para CADA destinatario
|
||||||
|
// Inmediato: estado 'enviado' o 'fallido'
|
||||||
|
// Programado/Recurrente: estado 'pendiente'
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO mensajes_discord (usuario_id, canal_id, contenido, estado, mensaje_discord_id, fecha_envio, tipo_envio)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
|
||||||
|
$scheduledSendTime = null;
|
||||||
|
if ($tipoEnvio === 'programado' && isset($input['fecha_envio'])) {
|
||||||
|
// Convertir la fecha local a UTC para guardar en DB
|
||||||
|
$datetime = new DateTime($input['fecha_envio'], new DateTimeZone($_ENV['TIME_ZONE_ENVIOS'] ?? 'UTC'));
|
||||||
|
$datetime->setTimezone(new DateTimeZone('UTC'));
|
||||||
|
$scheduledSendTime = $datetime->format('Y-m-d H:i:s');
|
||||||
|
} elseif ($tipoEnvio === 'inmediato') {
|
||||||
|
$scheduledSendTime = date('Y-m-d H:i:s'); // La fecha de envío es ahora
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$userData->userId,
|
||||||
|
$destinatarioId,
|
||||||
|
$contenido,
|
||||||
|
($tipoEnvio === 'inmediato' && $messageStatus === 'fallido') ? 'fallido' : $messageStatus, // Estado correcto para inmediato/fallido
|
||||||
|
$discordMessageId,
|
||||||
|
$scheduledSendTime,
|
||||||
|
$tipoEnvio
|
||||||
|
]);
|
||||||
|
|
||||||
|
$messageDbId = $db->lastInsertId();
|
||||||
|
|
||||||
|
// Si es recurrente, guardar también en la tabla recurrentes_discord
|
||||||
|
if ($tipoEnvio === 'recurrente') {
|
||||||
|
// Validar y obtener hora de envío
|
||||||
|
$horaEnvio = $input['hora_envio'] ?? '09:00:00';
|
||||||
|
if (!preg_match('/^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/', $horaEnvio) && !preg_match('/^([01]\d|2[0-3]):([0-5]\d)$/', $horaEnvio)) {
|
||||||
|
$horaEnvio = '09:00:00'; // Valor por defecto si es inválido
|
||||||
|
}
|
||||||
|
if (strlen($horaEnvio) === 5) { // Si viene sin segundos, añadir
|
||||||
|
$horaEnvio .= ':00';
|
||||||
|
}
|
||||||
|
|
||||||
|
$nextSendTime = null; // Esto debería ser calculado por un demonio de scheduling
|
||||||
|
|
||||||
|
$stmtRecur = $db->prepare("
|
||||||
|
INSERT INTO recurrentes_discord (mensaje_id, frecuencia, hora_envio, dia_semana, dia_mes, activo, proximo_envio)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 1, ?)
|
||||||
|
");
|
||||||
|
$stmtRecur->execute([
|
||||||
|
$messageDbId,
|
||||||
|
$input['frecuencia'] ?? 'diario',
|
||||||
|
$horaEnvio,
|
||||||
|
$input['dia_semana'] ?? null,
|
||||||
|
$input['dia_mes'] ?? null,
|
||||||
|
$nextSendTime // Será calculado por el demonio
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$allResults[] = [
|
||||||
|
'destinatario_id' => $destinatarioId,
|
||||||
|
'message_db_id' => $messageDbId,
|
||||||
|
'status' => ($tipoEnvio === 'inmediato' && $messageStatus === 'fallido') ? 'fallido' : $messageStatus,
|
||||||
|
'error' => $errorMessage,
|
||||||
|
'discord_message_id' => $discordMessageId,
|
||||||
|
'tipo_envio' => $tipoEnvio
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($messageStatus === 'enviado') {
|
||||||
|
logToFile('discord/messages.log', "Mensaje enviado a {$destinatarioId} por {$userData->username}");
|
||||||
|
} elseif ($messageStatus === 'pendiente') {
|
||||||
|
logToFile('discord/messages.log', "Mensaje {$tipoEnvio} guardado para {$destinatarioId} por {$userData->username}");
|
||||||
|
} else {
|
||||||
|
logToFile('discord/errors.log', "Error enviando mensaje a {$destinatarioId}: {$errorMessage}", 'ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determinar el éxito general de la operación
|
||||||
|
$overallSuccess = array_reduce($allResults, function($carry, $item) {
|
||||||
|
// Considerar éxito si al menos un mensaje fue enviado o está pendiente
|
||||||
|
return $carry || ($item['status'] === 'enviado' || $item['status'] === 'pendiente');
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => $overallSuccess,
|
||||||
|
'message' => 'Procesamiento de mensajes completado.',
|
||||||
|
'details' => $allResults,
|
||||||
|
'overall_status' => $overallSuccess ? 'partial_success_or_pending' : 'all_failed'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logToFile('discord/errors.log', "Error general en el envío de mensajes: " . $e->getMessage(), 'ERROR');
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
41
discord/api/recipients/create.php
Executable file
41
discord/api/recipients/create.php
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso
|
||||||
|
if (!hasPermission('manage_recipients', 'discord')) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No tienes permiso para crear destinatarios de Discord.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$nombre = trim($input['nombre'] ?? '');
|
||||||
|
$discord_id = trim($input['discord_id'] ?? '');
|
||||||
|
$tipo = trim($input['tipo'] ?? 'canal');
|
||||||
|
|
||||||
|
if (empty($nombre) || empty($discord_id)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Faltan datos'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO destinatarios_discord (nombre, discord_id, tipo, activo)
|
||||||
|
VALUES (?, ?, ?, 1)
|
||||||
|
");
|
||||||
|
$stmt->execute([$nombre, $discord_id, $tipo]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
if ($e->getCode() == 23000) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Este ID de Discord ya está registrado'], 409);
|
||||||
|
}
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
33
discord/api/recipients/delete.php
Executable file
33
discord/api/recipients/delete.php
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso
|
||||||
|
if (!hasPermission('manage_recipients', 'discord')) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No tienes permiso para eliminar destinatarios de Discord.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $input['id'] ?? null;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'ID requerido'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("DELETE FROM destinatarios_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
85
discord/api/recipients/edit.php
Executable file
85
discord/api/recipients/edit.php
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Editar Destinatario Discord
|
||||||
|
*/
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Habilitar errores para debug (quitar en producción estricta)
|
||||||
|
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
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso
|
||||||
|
if (!hasPermission('manage_recipients', 'discord')) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No tienes permiso para editar destinatarios de Discord.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $input['id'] ?? null;
|
||||||
|
$nombre = trim($input['nombre'] ?? '');
|
||||||
|
$discord_id = trim($input['discord_id'] ?? '');
|
||||||
|
$tipo = trim($input['tipo'] ?? '');
|
||||||
|
|
||||||
|
// Validaciones
|
||||||
|
if (empty($id) || !is_numeric($id)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'ID de destinatario inválido'], 400);
|
||||||
|
}
|
||||||
|
if (empty($nombre) || empty($discord_id) || empty($tipo)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Faltan datos requeridos (nombre, ID de Discord, tipo)'], 400);
|
||||||
|
}
|
||||||
|
if (!in_array($tipo, ['canal', 'usuario', 'grupo'])) { // Permitir 'grupo' aunque no esté en UI aún
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Tipo de destinatario inválido'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// 1. Verificar si el destinatario existe y si el usuario tiene permisos
|
||||||
|
$stmt = $db->prepare("SELECT usuario_id FROM destinatarios_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$recipient = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$recipient) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Destinatario no encontrado'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opcional: Si se implementara la propiedad del destinatario, se verificaría aquí
|
||||||
|
// if ($userData->rol !== 'Admin' && $recipient['usuario_id'] != $userData->userId) {
|
||||||
|
// jsonResponse(['success' => false, 'error' => 'No tiene permisos para editar este destinatario'], 403);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 2. Verificar duplicidad de discord_id (excluyendo el propio destinatario)
|
||||||
|
$stmt = $db->prepare("SELECT id FROM destinatarios_discord WHERE discord_id = ? AND id != ?");
|
||||||
|
$stmt->execute([$discord_id, $id]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Ya existe un destinatario con este ID de Discord'], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Actualizar el destinatario
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE destinatarios_discord
|
||||||
|
SET nombre = ?, discord_id = ?, tipo = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$nombre, $discord_id, $tipo, $id]);
|
||||||
|
|
||||||
|
logToFile('discord/recipients.log', "Destinatario editado: ID={$id}, Nombre={$nombre}, Usuario={$userData->username}");
|
||||||
|
jsonResponse(['success' => true, 'message' => 'Destinatario actualizado correctamente']);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logToFile('discord/errors.log', "Error editando destinatario: " . $e->getMessage(), 'ERROR');
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Ocurrió un error en el servidor.'], 500);
|
||||||
|
}
|
||||||
125
discord/api/recipients/kick.php
Executable file
125
discord/api/recipients/kick.php
Executable file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Expulsar/Remover Destinatario Discord
|
||||||
|
* Expulsa a un usuario de un guild o remueve el bot de un canal/grupo (según el tipo)
|
||||||
|
*/
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Habilitar errores para debug (quitar en producción estricta)
|
||||||
|
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
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso
|
||||||
|
if (!hasPermission('manage_recipients', 'discord')) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No tienes permiso para expulsar destinatarios de Discord.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$recipientDbId = $input['id'] ?? null; // ID de nuestra base de datos
|
||||||
|
|
||||||
|
if (empty($recipientDbId) || !is_numeric($recipientDbId)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'ID de destinatario inválido o no proporcionado'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
$botToken = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||||
|
$guildId = $_ENV['DISCORD_GUILD_ID'] ?? getenv('DISCORD_GUILD_ID');
|
||||||
|
|
||||||
|
if (!$botToken) {
|
||||||
|
throw new Exception("Token de bot de Discord no configurado.");
|
||||||
|
}
|
||||||
|
if (!$guildId) {
|
||||||
|
throw new Exception("ID de Guild de Discord no configurado.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Obtener detalles del destinatario de nuestra DB
|
||||||
|
$stmt = $db->prepare("SELECT discord_id, tipo FROM destinatarios_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$recipientDbId]);
|
||||||
|
$recipient = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$recipient) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Destinatario no encontrado en la base de datos local.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$discordId = $recipient['discord_id'];
|
||||||
|
$tipo = $recipient['tipo'];
|
||||||
|
|
||||||
|
$actionSuccess = false;
|
||||||
|
$actionMessage = '';
|
||||||
|
|
||||||
|
// Lógica para expulsar/remover según el tipo
|
||||||
|
switch ($tipo) {
|
||||||
|
case 'usuario':
|
||||||
|
// Expulsar usuario de un guild (servidor)
|
||||||
|
// Endpoint: DELETE /guilds/{guild.id}/members/{user.id}
|
||||||
|
$url = "https://discord.com/api/v10/guilds/{$guildId}/members/{$discordId}";
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Authorization: Bot ' . $botToken,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
throw new Exception("Error cURL al intentar expulsar al usuario de Discord: " . $curlError);
|
||||||
|
}
|
||||||
|
if ($httpCode === 204) { // 204 No Content es éxito para DELETE
|
||||||
|
$actionSuccess = true;
|
||||||
|
$actionMessage = "Usuario {$discordId} expulsado del guild {$guildId} en Discord.";
|
||||||
|
} else {
|
||||||
|
$responseJson = json_decode($response, true);
|
||||||
|
$actionMessage = "Error de Discord al expulsar usuario ({$httpCode}): " . ($responseJson['message'] ?? 'Desconocido');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'canal':
|
||||||
|
case 'grupo':
|
||||||
|
// Para canales/grupos, "expulsar" podría significar que el bot abandone el canal/grupo
|
||||||
|
// Esto es más complejo y no hay un endpoint directo "abandonar canal" para un bot en v10 sin conocer el webhook.
|
||||||
|
// Para simplificar, por ahora solo marcaremos como removido en nuestra DB.
|
||||||
|
// Una implementación real necesitaría una lógica para que el bot salga del canal/thread.
|
||||||
|
$actionSuccess = true; // Por ahora, se asume éxito en la acción "local"
|
||||||
|
$actionMessage = "No hay una API directa para que el bot 'expulse' de un canal/grupo Discord. Eliminado de la base de datos local.";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Tipo de destinatario no soportado para expulsión.'], 400);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($actionSuccess) {
|
||||||
|
// Eliminar el destinatario de nuestra base de datos
|
||||||
|
$deleteStmt = $db->prepare("DELETE FROM destinatarios_discord WHERE id = ?");
|
||||||
|
$deleteStmt->execute([$recipientDbId]);
|
||||||
|
|
||||||
|
logToFile('discord/recipients.log', "Destinatario '{$recipient['discord_id']}' ({$tipo}) expulsado/eliminado de Discord y de la DB local por Usuario: {$userData->username}.");
|
||||||
|
jsonResponse(['success' => true, 'message' => 'Destinatario expulsado/eliminado correctamente.', 'discord_action' => $actionMessage]);
|
||||||
|
} else {
|
||||||
|
logToFile('discord/errors.log', "Fallo al expulsar/eliminar destinatario '{$recipient['discord_id']}' ({$tipo}): {$actionMessage}", 'ERROR');
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Fallo al realizar la acción en Discord: ' . $actionMessage], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logToFile('discord/errors.log', "Error general en la API de expulsión de destinatarios: " . $e->getMessage(), 'ERROR');
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Ocurrió un error en el servidor: ' . $e->getMessage()], 500);
|
||||||
|
}
|
||||||
100
discord/api/templates/create.php
Executable file
100
discord/api/templates/create.php
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API de Plantillas de Discord - Crear
|
||||||
|
* REFRACTORIZADO para usar la tabla `comandos_discord`
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No autenticado: ' . $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission('manage_templates', 'discord')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No tienes permiso para crear plantillas de Discord.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$nombre = trim($data['nombre'] ?? '');
|
||||||
|
$comando = ltrim(trim($data['comando'] ?? ''), '#/');
|
||||||
|
$contenido = $data['contenido'] ?? '';
|
||||||
|
|
||||||
|
if (empty($nombre) || empty($contenido)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'El nombre y el contenido de la plantilla son obligatorios.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
try {
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// 1. Verificar si el comando ya existe en la tabla `comandos_discord`
|
||||||
|
if (!empty($comando)) {
|
||||||
|
$stmt = $db->prepare("SELECT id FROM comandos_discord WHERE comando = ?");
|
||||||
|
$stmt->execute([$comando]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$db->rollBack();
|
||||||
|
http_response_code(409); // Conflict
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Ya existe un comando con ese nombre.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Insertar la plantilla (sin la columna `comando`)
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO plantillas_discord (nombre, contenido, usuario_id, fecha_creacion, fecha_modificacion)
|
||||||
|
VALUES (?, ?, ?, NOW(), NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([$nombre, $contenido, $userData->userId]);
|
||||||
|
$newTemplateId = $db->lastInsertId();
|
||||||
|
|
||||||
|
// 3. Si hay un comando, insertarlo en la tabla `comandos_discord`
|
||||||
|
if (!empty($comando)) {
|
||||||
|
logToFile('discord/templates.log', "Intentando insertar comando en comandos_discord. Comando: {$comando}, Plantilla ID: {$newTemplateId}", 'INFO');
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO comandos_discord (comando, descripcion, plantilla_id)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
");
|
||||||
|
// Usamos el nombre de la plantilla como descripción por defecto
|
||||||
|
$stmt->execute([$comando, $nombre, $newTemplateId]);
|
||||||
|
logToFile('discord/templates.log', "Comando insertado en comandos_discord. Comando: {$comando}, Plantilla ID: {$newTemplateId}", 'INFO');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
logToFile('discord/templates.log', "Plantilla creada: {$nombre} (ID: {$newTemplateId}), por Usuario: {$userData->username}");
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Plantilla creada correctamente.',
|
||||||
|
'templateId' => $newTemplateId
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($db->inTransaction()) {
|
||||||
|
$db->rollBack();
|
||||||
|
}
|
||||||
|
http_response_code(500);
|
||||||
|
logToFile('discord/errors.log', 'Error creando plantilla: ' . $e->getMessage(), 'ERROR');
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Ocurrió un error en el servidor al crear la plantilla.']);
|
||||||
|
}
|
||||||
139
discord/api/templates/delete.php
Executable file
139
discord/api/templates/delete.php
Executable file
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API de Plantillas de Discord - Eliminar
|
||||||
|
* Este script es llamado por la función deleteTemplate() en list.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Habilitar logging de errores
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
// Función de logging mejorada
|
||||||
|
function logError($message, $data = null) {
|
||||||
|
$logDir = '/var/www/html/bot/logs/discord/';
|
||||||
|
if (!is_dir($logDir)) {
|
||||||
|
@mkdir($logDir, 0777, true);
|
||||||
|
}
|
||||||
|
$logFile = $logDir . 'error.log';
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$logMessage = "[$timestamp] [ERROR] $message";
|
||||||
|
if ($data) {
|
||||||
|
$logMessage .= "\n" . (is_string($data) ? $data : json_encode($data, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
error_log($logMessage . "\n", 3, $logFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iniciar buffer para capturar cualquier salida no deseada
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Registrar inicio
|
||||||
|
logError("Inicio de eliminación de plantilla");
|
||||||
|
|
||||||
|
// Incluir dependencias
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Limpiar buffer por si hay salida no deseada
|
||||||
|
ob_clean();
|
||||||
|
|
||||||
|
// Autenticación JWT para API (no redirigir)
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Acceso no autorizado.']);
|
||||||
|
logError("Error de autenticación");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
logError("Usuario autenticado: " . json_encode(['id' => $userData->userId, 'username' => $userData->username]));
|
||||||
|
|
||||||
|
// Verificar método
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido.']);
|
||||||
|
logError("Método no permitido: " . $_SERVER['REQUEST_METHOD']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener datos del cuerpo de la petición
|
||||||
|
$input = file_get_contents('php://input');
|
||||||
|
$data = json_decode($input, true);
|
||||||
|
$template_id = $data['id'] ?? null;
|
||||||
|
|
||||||
|
logError("Datos recibidos", ['input' => $input, 'data' => $data, 'template_id' => $template_id]);
|
||||||
|
|
||||||
|
// Validar ID
|
||||||
|
if (!$template_id || !is_numeric($template_id)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'ID de plantilla inválido o no proporcionado.']);
|
||||||
|
logError("ID de plantilla inválido", ['template_id' => $template_id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conectar a la base de datos
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
logError("Conexión a BD exitosa");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logError("Error al conectar a la base de datos", $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iniciar transacción
|
||||||
|
$db->beginTransaction();
|
||||||
|
logError("Transacción iniciada");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Eliminar comandos asociados (si existen)
|
||||||
|
$stmt = $db->prepare("DELETE FROM comandos_discord WHERE plantilla_id = ?");
|
||||||
|
$stmt->execute([$template_id]);
|
||||||
|
$deletedCommands = $stmt->rowCount();
|
||||||
|
logError("Comandos eliminados", ['count' => $deletedCommands]);
|
||||||
|
|
||||||
|
// 2. Intentar eliminar la plantilla
|
||||||
|
$stmt = $db->prepare("DELETE FROM plantillas_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$template_id]);
|
||||||
|
$deleted = $stmt->rowCount();
|
||||||
|
|
||||||
|
if ($deleted > 0) {
|
||||||
|
$db->commit();
|
||||||
|
logError("Plantilla eliminada exitosamente", ['id' => $template_id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
$db->rollBack();
|
||||||
|
http_response_code(404);
|
||||||
|
logError("No se encontró la plantilla para eliminar", ['id' => $template_id]);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No se encontró la plantilla para eliminar.']);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$db->rollBack();
|
||||||
|
logError("Error en la transacción", [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Ocurrió un error en el servidor.',
|
||||||
|
'detail' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logError("Error no manejado", [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Error interno del servidor.',
|
||||||
|
'detail' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpiar cualquier salida no deseada
|
||||||
|
ob_end_flush();
|
||||||
138
discord/api/templates/edit.php
Executable file
138
discord/api/templates/edit.php
Executable file
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API de Plantillas de Discord - Editar
|
||||||
|
* REFRACTORIZADO para usar la tabla `comandos_discord`
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Acceso no autorizado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission('manage_templates', 'discord')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No tienes permiso para editar plantillas de Discord.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$plantilla_id = $data['id'] ?? null;
|
||||||
|
$nombre = trim($data['nombre'] ?? '');
|
||||||
|
$comando = ltrim(trim($data['comando'] ?? ''), '#/');
|
||||||
|
$contenido = $data['contenido'] ?? '';
|
||||||
|
|
||||||
|
if (!$plantilla_id || !is_numeric($plantilla_id)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'ID de plantilla inválido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (empty($nombre) || empty($contenido)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'El nombre y el contenido son obligatorios.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
try {
|
||||||
|
logToFile('discord/templates.log', "Iniciando edición de plantilla (ID: {$plantilla_id}). Comando recibido: {$comando}", 'INFO');
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// 1. Verificar que la plantilla existe y el usuario tiene permiso
|
||||||
|
$stmt = $db->prepare("SELECT usuario_id FROM plantillas_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$plantilla_id]);
|
||||||
|
$plantillaExistente = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$plantillaExistente) {
|
||||||
|
throw new Exception('Plantilla no encontrada.', 404);
|
||||||
|
}
|
||||||
|
if ($userData->rol !== 'Admin' && $plantillaExistente['usuario_id'] != $userData->userId) {
|
||||||
|
throw new Exception('No tiene permisos para editar esta plantilla.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Actualizar la plantilla en sí (nombre y contenido)
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE plantillas_discord
|
||||||
|
SET nombre = ?, contenido = ?, fecha_modificacion = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$nombre, $contenido, $plantilla_id]);
|
||||||
|
|
||||||
|
// 3. Gestionar el comando en la tabla `comandos_discord`
|
||||||
|
// Primero, obtener el comando actual si existe
|
||||||
|
$stmt = $db->prepare("SELECT id, comando FROM comandos_discord WHERE plantilla_id = ?");
|
||||||
|
$stmt->execute([$plantilla_id]);
|
||||||
|
$comandoExistente = $stmt->fetch();
|
||||||
|
|
||||||
|
logToFile('discord/templates.log', "Comando existente para plantilla {$plantilla_id}: " . ($comandoExistente ? json_encode($comandoExistente) : 'Ninguno'), 'INFO');
|
||||||
|
|
||||||
|
|
||||||
|
// Antes de insertar/actualizar, verificar si el nuevo nombre de comando ya está en uso por OTRA plantilla
|
||||||
|
if (!empty($comando)) {
|
||||||
|
$stmt = $db->prepare("SELECT id FROM comandos_discord WHERE comando = ? AND plantilla_id != ?");
|
||||||
|
$stmt->execute([$comando, $plantilla_id]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
throw new Exception('Ya existe otro comando con ese nombre.', 409);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lógica de casos
|
||||||
|
if (!empty($comando) && $comandoExistente) {
|
||||||
|
// Caso: El comando se está modificando
|
||||||
|
if ($comando !== $comandoExistente['comando']) {
|
||||||
|
logToFile('discord/templates.log', "Modificando comando existente para plantilla {$plantilla_id}. De: {$comandoExistente['comando']} a: {$comando}", 'INFO');
|
||||||
|
$stmt = $db->prepare("UPDATE comandos_discord SET comando = ?, descripcion = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$comando, $nombre, $comandoExistente['id']]);
|
||||||
|
} else {
|
||||||
|
logToFile('discord/templates.log', "Comando existente no modificado para plantilla {$plantilla_id}. Valor: {$comando}", 'INFO');
|
||||||
|
}
|
||||||
|
} elseif (!empty($comando) && !$comandoExistente) {
|
||||||
|
// Caso: Se está añadiendo un comando nuevo
|
||||||
|
logToFile('discord/templates.log', "Añadiendo nuevo comando para plantilla {$plantilla_id}. Comando: {$comando}", 'INFO');
|
||||||
|
$stmt = $db->prepare("INSERT INTO comandos_discord (comando, descripcion, plantilla_id) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$comando, $nombre, $plantilla_id]);
|
||||||
|
} elseif (empty($comando) && $comandoExistente) {
|
||||||
|
// Caso: Se está eliminando el comando
|
||||||
|
logToFile('discord/templates.log', "Eliminando comando existente para plantilla {$plantilla_id}. Comando: {$comandoExistente['comando']}", 'INFO');
|
||||||
|
$stmt = $db->prepare("DELETE FROM comandos_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$comandoExistente['id']]);
|
||||||
|
} else {
|
||||||
|
logToFile('discord/templates.log', "No se gestionó comando para plantilla {$plantilla_id}. Comando recibido: {$comando}, Existente: " . ($comandoExistente ? $comandoExistente['comando'] : 'Ninguno'), 'INFO');
|
||||||
|
}
|
||||||
|
// Si no hay comando nuevo y no había uno existente, no se hace nada.
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
logToFile('discord/templates.log', "Edición de plantilla completada (ID: {$plantilla_id}).", 'INFO');
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Plantilla actualizada correctamente.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($db->inTransaction()) {
|
||||||
|
$db->rollBack();
|
||||||
|
}
|
||||||
|
$code = $e->getCode() >= 400 ? $e->getCode() : 500;
|
||||||
|
http_response_code($code);
|
||||||
|
logToFile('discord/templates.log', 'ERROR: Error editando plantilla (ID: ' . ($plantilla_id ?? 'N/A') . '): ' . $e->getMessage(), 'ERROR');
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
64
discord/api/templates/list.php
Executable file
64
discord/api/templates/list.php
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API de Plantillas de Discord - Listar
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Para depuración
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
if (!isAuthenticated()) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No autenticado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso
|
||||||
|
if (!hasPermission('view_templates', 'discord')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No tienes permiso para ver las plantillas de Discord.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Búsqueda (opcional, para futuras mejoras)
|
||||||
|
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT p.id, p.nombre, p.comando, p.fecha_modificacion, u.username
|
||||||
|
FROM plantillas_discord p
|
||||||
|
LEFT JOIN usuarios u ON p.usuario_id = u.id
|
||||||
|
";
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
if (!empty($search)) {
|
||||||
|
$sql .= " WHERE p.nombre LIKE ? OR p.comando LIKE ?";
|
||||||
|
$params[] = "%{$search}%";
|
||||||
|
$params[] = "%{$search}%";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY p.fecha_modificacion DESC";
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$templates = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'templates' => $templates
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
error_log('Error en /discord/api/templates/list.php: ' . $e->getMessage());
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error del servidor al obtener las plantillas.']);
|
||||||
|
}
|
||||||
36
discord/api/templates/php_errors.log
Executable file
36
discord/api/templates/php_errors.log
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
[04-Dec-2025 14:59:12 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 14:59:12 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 14:59:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:57:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:57:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:57:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:57:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:58:35 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:58:35 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:58:35 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:58:35 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:58:49 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:58:49 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:58:49 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:58:49 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 15:59:17 America/Mexico_City] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/discord/api/templates/delete.php:19
|
||||||
|
Stack trace:
|
||||||
|
#0 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/api/templates/delete.php on line 19
|
||||||
|
[04-Dec-2025 15:59:57 America/Mexico_City] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/discord/api/templates/delete.php:19
|
||||||
|
Stack trace:
|
||||||
|
#0 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/api/templates/delete.php on line 19
|
||||||
|
[04-Dec-2025 22:04:20 UTC] PHP Parse error: syntax error, unexpected token "=" in /var/www/html/bot/discord/api/templates/delete.php on line 20
|
||||||
|
[04-Dec-2025 22:05:28 UTC] PHP Parse error: syntax error, unexpected token "exit", expecting "]" in /var/www/html/bot/discord/api/templates/delete.php on line 24
|
||||||
|
[04-Dec-2025 22:05:56 UTC] PHP Parse error: syntax error, unexpected token "exit", expecting "]" in /var/www/html/bot/discord/api/templates/delete.php on line 24
|
||||||
|
[04-Dec-2025 22:06:02 UTC] PHP Parse error: syntax error, unexpected token "exit", expecting "]" in /var/www/html/bot/discord/api/templates/delete.php on line 24
|
||||||
|
[04-Dec-2025 22:08:10 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 26
|
||||||
|
[04-Dec-2025 22:11:10 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 26
|
||||||
|
[04-Dec-2025 22:13:26 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||||
|
[04-Dec-2025 22:28:45 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||||
|
[04-Dec-2025 22:31:33 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||||
|
[04-Dec-2025 22:32:17 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||||
|
[04-Dec-2025 22:32:21 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||||
|
[04-Dec-2025 22:32:47 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||||
|
[04-Dec-2025 22:34:50 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 19
|
||||||
143
discord/api/welcome/send_test.php
Executable file
143
discord/api/welcome/send_test.php
Executable file
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API - Enviar Mensaje de Prueba de Bienvenida
|
||||||
|
*/
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
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
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso
|
||||||
|
if (!hasPermission('manage_welcome', 'discord')) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No tienes permiso para gestionar el mensaje de bienvenida de Discord.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// 1. Obtener configuración de bienvenida
|
||||||
|
$stmt = $db->query("
|
||||||
|
SELECT b.*, g.ruta as imagen_ruta
|
||||||
|
FROM bienvenida_discord b
|
||||||
|
LEFT JOIN gallery g ON b.imagen_id = g.id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$config = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$config) {
|
||||||
|
throw new Exception("No hay configuración de bienvenida guardada");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($config['canal_id'])) {
|
||||||
|
throw new Exception("No se ha configurado un canal de bienvenida");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Obtener idiomas activos para los botones
|
||||||
|
$stmt = $db->query("SELECT codigo, nombre, nombre_nativo, bandera FROM idiomas WHERE activo = 1 ORDER BY nombre ASC");
|
||||||
|
$idiomas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 3. Preparar contenido del mensaje
|
||||||
|
$contenido = $config['texto'] ?? "Bienvenido al servidor!";
|
||||||
|
|
||||||
|
// Reemplazar variables
|
||||||
|
$dummyUser = "<@{$userData->id}> (Usuario de Prueba)";
|
||||||
|
$contenido = str_replace('{usuario}', $dummyUser, $contenido);
|
||||||
|
|
||||||
|
// Limpiar HTML a texto plano (similar a send.php)
|
||||||
|
$cleanContent = str_replace(['<br>', '<br/>', '<p>'], ["\n", "\n", "\n"], $contenido);
|
||||||
|
$cleanContent = strip_tags($cleanContent);
|
||||||
|
$cleanContent = html_entity_decode($cleanContent);
|
||||||
|
$cleanContent = trim($cleanContent);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'content' => $cleanContent
|
||||||
|
];
|
||||||
|
|
||||||
|
// 4. (Imagen eliminada por solicitud)
|
||||||
|
// if (!empty($config['imagen_ruta'])) { ... }
|
||||||
|
|
||||||
|
// 5. Agregar botones de idioma (Components)
|
||||||
|
if (!empty($idiomas)) {
|
||||||
|
$components = [];
|
||||||
|
$currentRow = ['type' => 1, 'components' => []];
|
||||||
|
|
||||||
|
foreach ($idiomas as $index => $lang) {
|
||||||
|
// Discord permite max 5 botones por fila, max 5 filas
|
||||||
|
if (count($currentRow['components']) >= 5) {
|
||||||
|
$components[] = $currentRow;
|
||||||
|
$currentRow = ['type' => 1, 'components' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usar bandera si existe, sino nombre nativo, sino nombre
|
||||||
|
$label = $lang['bandera'] ?: ($lang['nombre_nativo'] ?: $lang['nombre']);
|
||||||
|
|
||||||
|
$currentRow['components'][] = [
|
||||||
|
'type' => 2, // Button
|
||||||
|
'style' => 1, // Primary (Blurple)
|
||||||
|
'label' => $label,
|
||||||
|
'custom_id' => "lang_select_" . $lang['codigo']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($currentRow['components'])) {
|
||||||
|
$components[] = $currentRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['components'] = $components;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Enviar a Discord
|
||||||
|
$botToken = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||||
|
$url = "https://discord.com/api/v10/channels/{$config['canal_id']}/messages";
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Authorization: Bot ' . $botToken,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
throw new Exception("Error cURL: " . $curlError);
|
||||||
|
}
|
||||||
|
|
||||||
|
$responseJson = json_decode($response, true);
|
||||||
|
|
||||||
|
if ($httpCode >= 400) {
|
||||||
|
$errorMsg = $responseJson['message'] ?? 'Error desconocido de Discord';
|
||||||
|
if (isset($responseJson['errors'])) {
|
||||||
|
$errorMsg .= ' - ' . json_encode($responseJson['errors']);
|
||||||
|
}
|
||||||
|
throw new Exception("Discord API Error ({$httpCode}): {$errorMsg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Mensaje de prueba enviado correctamente',
|
||||||
|
'debug_url' => $imageUrl ?? 'No image'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
211
discord/dashboard_discord.php
Executable file
211
discord/dashboard_discord.php
Executable file
@@ -0,0 +1,211 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../shared/bootstrap.php';
|
||||||
|
|
||||||
|
// El bootstrap.php ya maneja la autenticación y carga $userData
|
||||||
|
// $userData está disponible globalmente a través de JWTAuth::getUserData() si se necesita de nuevo
|
||||||
|
?>
|
||||||
|
<!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 __('discord_dashboard_title'); ?></title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>💬 <?php echo __('discord_dashboard_header'); ?></h1>
|
||||||
|
<a href="/index.php" class="btn-back">← <?php echo __('back_to_main_dashboard'); ?></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="modules-grid">
|
||||||
|
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||||
|
<a href="/discord/views/templates/list.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-file-alt"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('templates_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('templates_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('send_messages', 'discord')): ?>
|
||||||
|
<a href="/discord/views/messages/create.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-paper-plane"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('create_message_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('create_message_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('view_sent_messages', 'discord')): ?>
|
||||||
|
<a href="/discord/views/messages/sent.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-history"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('sent_messages_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('sent_messages_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('manage_recipients', 'discord')): ?>
|
||||||
|
<a href="/discord/views/recipients/list.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-users"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('recipients_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('recipients_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('manage_commands', 'discord')): ?>
|
||||||
|
<a href="/discord/views/commands/list.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-terminal"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('commands_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('commands_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('manage_welcome', 'discord')): ?>
|
||||||
|
<a href="/discord/views/welcome/config.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-handshake"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('welcome_message_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('welcome_message_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('view_logs', 'discord')): ?>
|
||||||
|
<a href="/discord/views/logs/list.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-list-alt"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('system_logs_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('system_logs_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('view_gallery')): ?>
|
||||||
|
<a href="/gallery/index.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-images"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('gallery_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('gallery_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('manage_languages')): ?>
|
||||||
|
<a href="/shared/languages/manager.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-language"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('languages_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('languages_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('test_connection', 'discord')): ?>
|
||||||
|
<a href="/discord/test_connection.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-plug"></i></div>
|
||||||
|
<div class="module-title"><?php echo __('connection_test_module_title'); ?></div>
|
||||||
|
<div class="module-description"><?php echo __('connection_test_module_description'); ?></div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('view_logs', 'discord')): ?>
|
||||||
|
<a href="/discord/views/features.php" class="module-card">
|
||||||
|
<div class="module-icon"><i class="fas fa-robot"></i></div>
|
||||||
|
<div class="module-title">Funciones del Bot</div>
|
||||||
|
<div class="module-description">Documentación de las funciones automáticas y comandos del bot.</div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
55
discord/register_commands.php
Executable file
55
discord/register_commands.php
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Script para registrar los Slash Commands en Discord.
|
||||||
|
* Ejecutar manualmente una sola vez o cuando los comandos cambien.
|
||||||
|
* php discord/register_commands.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Discord\Discord;
|
||||||
|
use Discord\Builders\Components\ActionRow;
|
||||||
|
use Discord\Builders\Components\Button;
|
||||||
|
use Discord\Builders\MessageBuilder;
|
||||||
|
use Discord\Builders\CommandBuilder;
|
||||||
|
use Discord\Parts\Interactions\Command\Command;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$discord = new Discord([
|
||||||
|
'token' => $_ENV['DISCORD_BOT_TOKEN'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$discord->on('ready', function (Discord $discord) {
|
||||||
|
echo "Bot de registro iniciado..." . PHP_EOL;
|
||||||
|
|
||||||
|
// Crear el comando
|
||||||
|
$command = new Command($discord, [
|
||||||
|
'name' => 'start',
|
||||||
|
'description' => 'Muestra el mensaje de bienvenida y las opciones de idioma.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Registrar el comando globalmente
|
||||||
|
$discord->application->commands->save($command)->then(
|
||||||
|
function (Command $command) {
|
||||||
|
echo "Comando '/{$command->name}' registrado exitosamente!" . PHP_EOL;
|
||||||
|
},
|
||||||
|
function (\Exception $e) {
|
||||||
|
echo "Error al registrar el comando: " . $e->getMessage() . PHP_EOL;
|
||||||
|
}
|
||||||
|
)->done(function() use ($discord) {
|
||||||
|
echo "Registro de comandos completado. Cerrando." . PHP_EOL;
|
||||||
|
$discord->close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$discord->run();
|
||||||
95
discord/src/CommandLocker.php
Executable file
95
discord/src/CommandLocker.php
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class CommandLocker {
|
||||||
|
private $pdo;
|
||||||
|
private $lockTimeout = 300; // 5 minutos de tiempo de espera para el bloqueo
|
||||||
|
|
||||||
|
public function __construct(PDO $pdo) {
|
||||||
|
$this->pdo = $pdo;
|
||||||
|
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function log($message, $data = []) {
|
||||||
|
// This will be handled by the main bot's logger
|
||||||
|
// For now, do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function acquireLock($command, $chatId, $type = 'command', $data = []) {
|
||||||
|
$this->log("Intentando adquirir bloqueo", ['command' => $command, 'chatId' => $chatId, 'type' => $type]);
|
||||||
|
$this->cleanupExpiredLocks();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
|
$query = "SELECT id, status, created_at FROM command_locks WHERE command = ? AND chat_id = ? ORDER BY created_at DESC LIMIT 1 FOR UPDATE";
|
||||||
|
$stmt = $this->pdo->prepare($query);
|
||||||
|
$stmt->execute([$command, $chatId]);
|
||||||
|
$existingLock = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($existingLock && $existingLock['status'] === 'processing' && strtotime($existingLock['created_at']) > strtotime('-5 minutes')) {
|
||||||
|
$this->log("Bloqueo ya en proceso, rechazando nueva solicitud");
|
||||||
|
$this->pdo->rollBack();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expiresAt = (new DateTime('+5 minutes'))->format('Y-m-d H:i:s');
|
||||||
|
$dataJson = !empty($data) ? json_encode($data) : null;
|
||||||
|
|
||||||
|
if ($existingLock) {
|
||||||
|
$upd = $this->pdo->prepare("UPDATE command_locks SET type = ?, status='processing', data = ?, expires_at = ?, updated_at = NOW() WHERE id = ?");
|
||||||
|
$upd->execute([$type, $dataJson, $expiresAt, $existingLock['id']]);
|
||||||
|
$this->pdo->commit();
|
||||||
|
return (int)$existingLock['id'];
|
||||||
|
} else {
|
||||||
|
$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);
|
||||||
|
$stmt->execute([$chatId, $command, $type, $dataJson, $expiresAt]);
|
||||||
|
$lockId = $this->pdo->lastInsertId();
|
||||||
|
$this->pdo->commit();
|
||||||
|
return (int)$lockId;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log("Error al adquirir bloqueo", ['error' => $e->getMessage()]);
|
||||||
|
if ($this->pdo->inTransaction()) {
|
||||||
|
$this->pdo->rollBack();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateLockStatus($lockId, $status, $messageId = null) {
|
||||||
|
$this->log("Actualizando estado de bloqueo", ['lockId' => $lockId, 'status' => $status]);
|
||||||
|
try {
|
||||||
|
$query = "UPDATE command_locks SET status = ?, message_id = COALESCE(?, message_id), updated_at = NOW() WHERE id = ?";
|
||||||
|
$stmt = $this->pdo->prepare($query);
|
||||||
|
return $stmt->execute([$status, $messageId, $lockId]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log("Error al actualizar estado de bloqueo", ['error' => $e->getMessage()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function releaseLock($lockId, $messageId = null) {
|
||||||
|
return $this->updateLockStatus($lockId, 'completed', $messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function failLock($lockId, $errorMessage = '') {
|
||||||
|
$this->log("Marcando bloqueo como fallido", ['lockId' => $lockId, 'errorMessage' => $errorMessage]);
|
||||||
|
try {
|
||||||
|
$query = "UPDATE command_locks SET status = 'failed', data = JSON_SET(COALESCE(data, '{}'), '$.error', ?) WHERE id = ?";
|
||||||
|
$stmt = $this->pdo->prepare($query);
|
||||||
|
return $stmt->execute([$errorMessage, $lockId]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log("Error al marcar bloqueo como fallido", ['error' => $e->getMessage()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanupExpiredLocks() {
|
||||||
|
try {
|
||||||
|
$this->pdo->exec("DELETE FROM command_locks WHERE expires_at <= NOW()");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log("Error al limpiar bloqueos expirados", ['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
discord/src/DiscordSender.php
Executable file
76
discord/src/DiscordSender.php
Executable file
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class DiscordSender
|
||||||
|
{
|
||||||
|
private const API_BASE_URL = 'https://discord.com/api/v10';
|
||||||
|
private string $token;
|
||||||
|
|
||||||
|
public function __construct(string $token)
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendMessage(string $channelId, string $htmlContent) {
|
||||||
|
$converter = new HtmlToDiscordMarkdownConverter();
|
||||||
|
$markdown = $converter->convert($htmlContent);
|
||||||
|
|
||||||
|
$sentMessageIds = [];
|
||||||
|
|
||||||
|
// Simple implementation: send the whole markdown.
|
||||||
|
// The original had complex logic to split text and images.
|
||||||
|
// This is a simplification to get it working first.
|
||||||
|
try {
|
||||||
|
$response = $this->sendApiRequest("/channels/{$channelId}/messages", ['content' => $markdown]);
|
||||||
|
if (isset($response['id'])) {
|
||||||
|
$sentMessageIds[] = $response['id'];
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("DiscordSender Error: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sentMessageIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendRawMessage(string $channelId, string $content): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->sendApiRequest("/channels/{$channelId}/messages", ['content' => $content]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("DiscordSender Error: " . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendApiRequest(string $endpoint, array $payload, string $method = 'POST') {
|
||||||
|
$url = self::API_BASE_URL . $endpoint;
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
$headers = [
|
||||||
|
'Authorization: Bot ' . $this->token,
|
||||||
|
'User-Agent: BotDiscord (https://github.com/nickpons/bot, 1.0)'
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$headers[] = 'Content-Type: application/json';
|
||||||
|
$postData = json_encode($payload);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
discord/src/HtmlToDiscordMarkdownConverter.php
Executable file
18
discord/src/HtmlToDiscordMarkdownConverter.php
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class HtmlToDiscordMarkdownConverter
|
||||||
|
{
|
||||||
|
public function convert(string $html): string
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function convertToArray(string $html): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function absoluteUrl($url) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
discord/src/Translate.php
Executable file
60
discord/src/Translate.php
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
<?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]);
|
||||||
|
return $response[0]['language'] ?? 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if ($method === 'POST') {
|
||||||
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response_body = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($http_code >= 400) {
|
||||||
|
// In case of an error, log it or handle it
|
||||||
|
error_log("LibreTranslate API error. HTTP Code: {$http_code}, Response: {$response_body}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode($response_body, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
discord/test_access.php
Executable file
5
discord/test_access.php
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
echo "<h1>Test Discord Folder</h1>";
|
||||||
|
echo "<p>Si ves esto, la carpeta es accesible.</p>";
|
||||||
|
echo "<p>Ruta actual: " . __DIR__ . "</p>";
|
||||||
|
?>
|
||||||
242
discord/test_connection.php
Executable file
242
discord/test_connection.php
Executable file
@@ -0,0 +1,242 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Test de Conexión con Discord
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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__ . '/../shared/auth/jwt.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
$bot_token = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||||
|
$guild_id = $_ENV['DISCORD_GUILD_ID'] ?? getenv('DISCORD_GUILD_ID');
|
||||||
|
|
||||||
|
$testResults = [];
|
||||||
|
$allSuccess = true;
|
||||||
|
|
||||||
|
// Test 1: Verificar token
|
||||||
|
$testResults[] = [
|
||||||
|
'test' => 'Verificar token configurado',
|
||||||
|
'success' => !empty($bot_token),
|
||||||
|
'message' => !empty($bot_token) ? 'Token configurado correctamente' : 'Token no configurado'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test 2: Obtener información del bot
|
||||||
|
if (!empty($bot_token)) {
|
||||||
|
$ch = curl_init('https://discord.com/api/v10/users/@me');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Authorization: Bot ' . $bot_token,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$success = $httpCode === 200;
|
||||||
|
$allSuccess = $allSuccess && $success;
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$botInfo = json_decode($response, true);
|
||||||
|
$testResults[] = [
|
||||||
|
'test' => 'Conectar con Discord API',
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Conectado como: ' . $botInfo['username'] . '#' . $botInfo['discriminator'],
|
||||||
|
'data' => $botInfo
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$testResults[] = [
|
||||||
|
'test' => 'Conectar con Discord API',
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error HTTP ' . $httpCode . ': ' . $response
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Obtener información del servidor/guild
|
||||||
|
if (!empty($bot_token) && !empty($guild_id)) {
|
||||||
|
$ch = curl_init("https://discord.com/api/v10/guilds/{$guild_id}");
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Authorization: Bot ' . $bot_token,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$success = $httpCode === 200;
|
||||||
|
$allSuccess = $allSuccess && $success;
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$guildInfo = json_decode($response, true);
|
||||||
|
$testResults[] = [
|
||||||
|
'test' => 'Acceder al servidor Discord',
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Servidor: ' . $guildInfo['name'] . ' (Miembros: ' . ($guildInfo['approximate_member_count'] ?? 'N/A') . ')',
|
||||||
|
'data' => $guildInfo
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$testResults[] = [
|
||||||
|
'test' => 'Acceder al servidor Discord',
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error HTTP ' . $httpCode . ': ' . $response
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Listar canales
|
||||||
|
if (!empty($bot_token) && !empty($guild_id)) {
|
||||||
|
$ch = curl_init("https://discord.com/api/v10/guilds/{$guild_id}/channels");
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Authorization: Bot ' . $bot_token,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$success = $httpCode === 200;
|
||||||
|
$allSuccess = $allSuccess && $success;
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$channels = json_decode($response, true);
|
||||||
|
$textChannels = array_filter($channels, fn($c) => $c['type'] === 0);
|
||||||
|
$testResults[] = [
|
||||||
|
'test' => 'Listar canales',
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Se encontraron ' . count($textChannels) . ' canales de texto',
|
||||||
|
'data' => array_slice($textChannels, 0, 5)
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$testResults[] = [
|
||||||
|
'test' => 'Listar canales',
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error HTTP ' . $httpCode
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Test Discord - Sistema de Bots</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #5865F2 0%, #4752C4 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #5865F2;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.test-result {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-left: 5px solid #ddd;
|
||||||
|
}
|
||||||
|
.test-result.success {
|
||||||
|
border-left-color: #28a745;
|
||||||
|
}
|
||||||
|
.test-result.error {
|
||||||
|
border-left-color: #dc3545;
|
||||||
|
}
|
||||||
|
.test-name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.test-message {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.badge-success { background: #d4edda; color: #155724; }
|
||||||
|
.badge-error { background: #f8d7da; color: #721c24; }
|
||||||
|
.btn-back {
|
||||||
|
display: inline-block;
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
background: <?php echo $allSuccess ? '#d4edda' : '#f8d7da'; ?>;
|
||||||
|
color: <?php echo $allSuccess ? '#155724' : '#721c24'; ?>;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🧪 Test de Conexión con Discord</h1>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<?php if ($allSuccess): ?>
|
||||||
|
✅ Todos los tests pasaron correctamente
|
||||||
|
<?php else: ?>
|
||||||
|
❌ Algunos tests fallaron
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php foreach ($testResults as $result): ?>
|
||||||
|
<div class="test-result <?php echo $result['success'] ? 'success' : 'error'; ?>">
|
||||||
|
<div class="test-name">
|
||||||
|
<?php echo htmlspecialchars($result['test']); ?>
|
||||||
|
<span class="badge <?php echo $result['success'] ? 'badge-success' : 'badge-error'; ?>">
|
||||||
|
<?php echo $result['success'] ? 'OK' : 'ERROR'; ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="test-message"><?php echo htmlspecialchars($result['message']); ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<a href="/discord/dashboard_discord.php" class="btn-back">← Volver al Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
discord/test_hello.php
Executable file
1
discord/test_hello.php
Executable file
@@ -0,0 +1 @@
|
|||||||
|
<?php echo 'Hello from discord dir'; ?>
|
||||||
304
discord/views/commands/list.php
Executable file
304
discord/views/commands/list.php
Executable file
@@ -0,0 +1,304 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar permiso para ver la página de comandos
|
||||||
|
if (!hasPermission('view_commands', 'discord')) {
|
||||||
|
die('No tienes permiso para ver los comandos de Discord.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Obtener comandos con información de la plantilla asociada
|
||||||
|
$stmt = $db->query("
|
||||||
|
SELECT c.id, p.nombre, c.comando, c.fecha_creacion, c.descripcion
|
||||||
|
FROM comandos_discord c
|
||||||
|
LEFT JOIN plantillas_discord p ON c.plantilla_id = p.id
|
||||||
|
ORDER BY c.comando ASC
|
||||||
|
");
|
||||||
|
$comandos = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<!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>Comandos Discord - Sistema de Bots</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-tag {
|
||||||
|
background: #2b2d31;
|
||||||
|
color: #dbdee1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--discord-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.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: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
float: right;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-terminal"></i> Comandos Discord</h1>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||||
|
<a href="/discord/views/templates/create.php" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Nuevo Comando (Plantilla)
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<?php if (empty($comandos)): ?>
|
||||||
|
<div style="text-align: center; padding: 40px; color: #666;">
|
||||||
|
<i class="fas fa-terminal" style="font-size: 48px; margin-bottom: 20px; color: #ddd;"></i>
|
||||||
|
<h3>No hay comandos configurados</h3>
|
||||||
|
<p>Los comandos se definen al crear o editar una plantilla.</p>
|
||||||
|
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||||
|
<a href="/discord/views/templates/create.php" class="btn btn-primary" style="margin-top: 10px;">
|
||||||
|
Ir a Plantillas
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Comando</th>
|
||||||
|
<th>Plantilla Asociada</th>
|
||||||
|
<th>Fecha Creación</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($comandos as $cmd): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="command-tag"><?php echo htmlspecialchars($cmd['comando']); ?></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<i class="fas fa-file-alt"></i> <?php echo htmlspecialchars($cmd['nombre']); ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo date('d/m/Y', strtotime($cmd['fecha_creacion'])); ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||||
|
<a href="/discord/views/templates/edit.php?id=<?php echo $cmd['id']; ?>" class="btn btn-edit">
|
||||||
|
<i class="fas fa-edit"></i> Editar
|
||||||
|
</a>
|
||||||
|
<button onclick="deleteCommand(<?php echo $cmd['id']; ?>, '<?php echo htmlspecialchars($cmd['comando'], ENT_QUOTES); ?>')" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash"></i> Eliminar
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts eliminados ya que no se necesita modal local -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function deleteCommand(templateId, commandName) {
|
||||||
|
if (!confirm(`¿Estás seguro de eliminar el comando "${commandName}"? Esto eliminará también la plantilla asociada.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/templates/delete.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ id: templateId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Comando y plantilla eliminados correctamente.');
|
||||||
|
location.reload(); // Recargar la página para actualizar la lista
|
||||||
|
} else {
|
||||||
|
alert('Error al eliminar: ' + (result.error || 'Error desconocido.'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al enviar la solicitud de eliminación:', error);
|
||||||
|
alert('Error de conexión al intentar eliminar el comando.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
discord/views/commands/php_errors.log
Executable file
5
discord/views/commands/php_errors.log
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
[04-Dec-2025 16:44:49 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'comando' in 'field list' in /var/www/html/bot/discord/views/commands/list.php:23
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/discord/views/commands/list.php(23): PDO->query()
|
||||||
|
#1 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/views/commands/list.php on line 23
|
||||||
113
discord/views/features.php
Executable file
113
discord/views/features.php
Executable file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../shared/bootstrap.php';
|
||||||
|
|
||||||
|
// El bootstrap.php ya maneja la autenticación
|
||||||
|
if (!hasPermission('view_logs', 'discord')) {
|
||||||
|
die('No tienes permiso para ver esta página.');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!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>Funciones del Bot de Discord</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color); font-size: 24px; }
|
||||||
|
.btn-back {
|
||||||
|
background: #6c757d; color: white; padding: 10px 20px;
|
||||||
|
border-radius: 8px; text-decoration: none; transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.btn-back:hover { transform: translateY(-2px); }
|
||||||
|
.container { max-width: 1000px; margin: 0 auto; }
|
||||||
|
.card {
|
||||||
|
background: white; border-radius: 15px; padding: 30px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.feature {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
.feature:last-child { border-bottom: none; }
|
||||||
|
.feature-title { font-size: 18px; font-weight: 700; color: #333; margin-bottom: 8px; }
|
||||||
|
.feature-event { font-size: 14px; color: var(--discord-color); font-family: monospace; margin-bottom: 8px; }
|
||||||
|
.feature-description { font-size: 15px; color: #666; line-height: 1.6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-robot"></i> Funciones del Bot de Discord</h1>
|
||||||
|
<a href="/discord/dashboard_discord.php" class="btn-back">← Volver al Dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<div class="feature">
|
||||||
|
<div class="feature-title">Mensajes de Bienvenida a Nuevos Miembros</div>
|
||||||
|
<div class="feature-event">Evento: Un nuevo usuario se une al servidor</div>
|
||||||
|
<p class="feature-description">
|
||||||
|
Cuando un nuevo miembro se une, el bot le da la bienvenida en un canal específico. Este mensaje es totalmente configurable desde el panel de administración, permitiendo cambiar el texto, añadir una imagen y habilitar botones para que el usuario seleccione su idioma preferido.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature">
|
||||||
|
<div class="feature-title">Sistema de Plantillas y Comandos</div>
|
||||||
|
<div class="feature-event">Evento: Un usuario escribe un comando como #ayuda o /comandos</div>
|
||||||
|
<p class="feature-description">
|
||||||
|
El bot puede responder a comandos que empiezan con <code>#</code>. Cada comando está asociado a una plantilla de texto predefinida.
|
||||||
|
<br>- <strong>Comandos de Plantilla:</strong> Al usar un comando como <code>#reglas</code>, el bot envía el contenido de esa plantilla al canal.
|
||||||
|
<br>- <strong>Traducción de Plantillas:</strong> Junto con la plantilla, el bot añade botones para que los usuarios puedan traducirla instantáneamente a los idiomas activos.
|
||||||
|
<br>- <strong>Listar Comandos:</strong> El comando <code>/comandos</code> muestra una lista de todos los comandos de plantilla disponibles.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature">
|
||||||
|
<div class="feature-title">Traducción Automática de Mensajes</div>
|
||||||
|
<div class="feature-event">Evento: Al recibir un mensaje que no es un comando</div>
|
||||||
|
<p class="feature-description">
|
||||||
|
Para romper las barreras del idioma, el bot añade un botón <strong>"Traducir / Translate"</strong> debajo de los mensajes que no son comandos. Al hacer clic, el bot traduce el mensaje original al idioma que el usuario haya configurado y se lo envía como un mensaje privado (efímero) que solo él puede ver.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature">
|
||||||
|
<div class="feature-title">Gestión de Preferencias de Idioma</div>
|
||||||
|
<div class="feature-event">Evento: Clic en un botón de selección de idioma</div>
|
||||||
|
<p class="feature-description">
|
||||||
|
El bot permite a los usuarios establecer su idioma preferido.
|
||||||
|
<br>- <strong>Botones de Bienvenida:</strong> Al hacer clic en los botones de idioma del mensaje de bienvenida, el bot guarda la preferencia del usuario.
|
||||||
|
<br>- <strong>Traducciones Personalizadas:</strong> Esta preferencia se usa para saber a qué idioma traducir cuando el usuario utiliza una función de traducción.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature">
|
||||||
|
<div class="feature-title">Registro de Usuarios para Mejor Interacción</div>
|
||||||
|
<div class="feature-event">Evento: Al unirse un miembro o interactuar por primera vez</div>
|
||||||
|
<p class="feature-description">
|
||||||
|
Para personalizar la experiencia, el bot guarda información básica de los usuarios.
|
||||||
|
<br>- <strong>Al unirse al servidor:</strong> Si está activado en la configuración de bienvenida, el bot registra al nuevo miembro.
|
||||||
|
<br>- <strong>Al seleccionar un idioma:</strong> El bot registra o actualiza al usuario que interactúa con los botones de idioma para guardar su preferencia.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
322
discord/views/logs/list.php
Executable file
322
discord/views/logs/list.php
Executable file
@@ -0,0 +1,322 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Paginación
|
||||||
|
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||||
|
$perPage = 20;
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
// Filtros
|
||||||
|
$nivel = isset($_GET['nivel']) ? $_GET['nivel'] : '';
|
||||||
|
$origen = isset($_GET['origen']) ? $_GET['origen'] : '';
|
||||||
|
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||||
|
|
||||||
|
// Construir Query
|
||||||
|
$where = ["1=1"];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($nivel) {
|
||||||
|
$where[] = "nivel = ?";
|
||||||
|
$params[] = $nivel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($origen) {
|
||||||
|
$where[] = "origen = ?";
|
||||||
|
$params[] = $origen;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$where[] = "descripcion LIKE ?";
|
||||||
|
$params[] = "%$search%";
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = implode(" AND ", $where);
|
||||||
|
|
||||||
|
// Total para paginación
|
||||||
|
$stmt = $db->prepare("SELECT COUNT(*) FROM logs_discord WHERE $whereClause");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$totalLogs = $stmt->fetchColumn();
|
||||||
|
$totalPages = ceil($totalLogs / $perPage);
|
||||||
|
|
||||||
|
// Obtener logs
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT l.*, u.username
|
||||||
|
FROM logs_discord l
|
||||||
|
LEFT JOIN usuarios u ON l.usuario_id = u.id
|
||||||
|
WHERE $whereClause
|
||||||
|
ORDER BY l.fecha DESC
|
||||||
|
LIMIT $perPage OFFSET $offset
|
||||||
|
");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$logs = $stmt->fetchAll();
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!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>Logs Discord - Sistema de Bots</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
--bg-color: #f0f2f5;
|
||||||
|
--text-color: #333;
|
||||||
|
--success: #28a745;
|
||||||
|
--warning: #ffc107;
|
||||||
|
--danger: #dc3545;
|
||||||
|
--info: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: var(--bg-color);
|
||||||
|
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: var(--discord-color); font-size: 24px; }
|
||||||
|
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.btn-primary { background: var(--discord-color); color: white; }
|
||||||
|
.btn-secondary { background: #6c757d; color: white; }
|
||||||
|
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th, td { padding: 15px; text-align: left; border-bottom: 1px solid #eee; }
|
||||||
|
th { background: #f8f9fa; color: #555; font-weight: 600; }
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.badge-info { background: var(--info); }
|
||||||
|
.badge-warning { background: var(--warning); color: #333; }
|
||||||
|
.badge-error { background: var(--danger); }
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a.active {
|
||||||
|
background: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal JSON */
|
||||||
|
.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: 80%; max-width: 800px; border-radius: 15px; position: relative; max-height: 80vh; overflow-y: auto; }
|
||||||
|
.close-modal { position: absolute; top: 15px; right: 20px; font-size: 24px; cursor: pointer; color: #aaa; }
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-list-alt"></i> Logs del Sistema</h1>
|
||||||
|
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<form class="filters" method="GET">
|
||||||
|
<input type="text" name="search" class="form-control" placeholder="Buscar en descripción..." value="<?php echo htmlspecialchars($search); ?>">
|
||||||
|
|
||||||
|
<select name="nivel" class="form-control">
|
||||||
|
<option value="">-- Todos los Niveles --</option>
|
||||||
|
<option value="info" <?php echo $nivel === 'info' ? 'selected' : ''; ?>>Info</option>
|
||||||
|
<option value="warning" <?php echo $nivel === 'warning' ? 'selected' : ''; ?>>Warning</option>
|
||||||
|
<option value="error" <?php echo $nivel === 'error' ? 'selected' : ''; ?>>Error</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select name="origen" class="form-control">
|
||||||
|
<option value="">-- Todos los Orígenes --</option>
|
||||||
|
<option value="sistema" <?php echo $origen === 'sistema' ? 'selected' : ''; ?>>Sistema</option>
|
||||||
|
<option value="usuario" <?php echo $origen === 'usuario' ? 'selected' : ''; ?>>Usuario</option>
|
||||||
|
<option value="bot" <?php echo $origen === 'bot' ? 'selected' : ''; ?>>Bot</option>
|
||||||
|
<option value="webhook" <?php echo $origen === 'webhook' ? 'selected' : ''; ?>>Webhook</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-filter"></i> Filtrar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<?php if ($nivel || $origen || $search): ?>
|
||||||
|
<a href="list.php" class="btn btn-secondary">Limpiar</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Nivel</th>
|
||||||
|
<th>Origen</th>
|
||||||
|
<th>Descripción</th>
|
||||||
|
<th>Usuario</th>
|
||||||
|
<th>Detalles</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($logs)): ?>
|
||||||
|
<tr><td colspan="6" style="text-align:center; color:#666;">No hay registros encontrados.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($logs as $log): ?>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size: 14px; color: #666;"><?php echo $log['fecha']; ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-<?php echo $log['nivel']; ?>">
|
||||||
|
<?php echo strtoupper($log['nivel']); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><?php echo ucfirst($log['origen']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars(substr($log['descripcion'], 0, 100)) . (strlen($log['descripcion']) > 100 ? '...' : ''); ?></td>
|
||||||
|
<td><?php echo $log['username'] ? htmlspecialchars($log['username']) : '-'; ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($log['datos_json']): ?>
|
||||||
|
<button class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px;"
|
||||||
|
onclick='showDetails(<?php echo json_encode($log['datos_json']); ?>)'>
|
||||||
|
<i class="fas fa-code"></i> JSON
|
||||||
|
</button>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color:#ccc;">-</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Paginación -->
|
||||||
|
<?php if ($totalPages > 1): ?>
|
||||||
|
<div class="pagination">
|
||||||
|
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||||
|
<a href="?page=<?php echo $i; ?>&nivel=<?php echo $nivel; ?>&origen=<?php echo $origen; ?>&search=<?php echo $search; ?>"
|
||||||
|
class="<?php echo $i === $page ? 'active' : ''; ?>">
|
||||||
|
<?php echo $i; ?>
|
||||||
|
</a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Detalles -->
|
||||||
|
<div id="detailsModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close-modal" onclick="closeModal()">×</span>
|
||||||
|
<h2>Detalles del Log</h2>
|
||||||
|
<pre id="jsonContent"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showDetails(jsonString) {
|
||||||
|
try {
|
||||||
|
// Si ya es objeto, usarlo, si es string, parsearlo
|
||||||
|
const obj = typeof jsonString === 'string' ? JSON.parse(jsonString) : jsonString;
|
||||||
|
document.getElementById('jsonContent').textContent = JSON.stringify(obj, null, 2);
|
||||||
|
document.getElementById('detailsModal').style.display = 'block';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert('Error al parsear JSON');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('detailsModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target == document.getElementById('detailsModal')) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
736
discord/views/messages/create.php
Executable file
736
discord/views/messages/create.php
Executable file
@@ -0,0 +1,736 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar permiso para enviar mensajes
|
||||||
|
if (!hasPermission('send_messages', 'discord')) {
|
||||||
|
die('No tienes permiso para crear y enviar mensajes de Discord.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Obtener plantillas para el selector
|
||||||
|
$stmt = $db->query("SELECT id, nombre, contenido FROM plantillas_discord ORDER BY nombre ASC");
|
||||||
|
$plantillas = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Obtener destinatarios guardados (si existen)
|
||||||
|
$stmt = $db->query("SELECT id, nombre, discord_id as identificador, tipo FROM destinatarios_discord ORDER BY nombre ASC");
|
||||||
|
$destinatarios = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<!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>Crear Mensaje Discord - Sistema de Bots</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/4.1.5/css/flag-icons.min.css" />
|
||||||
|
<style>
|
||||||
|
/* Estilos para los botones de traducción */
|
||||||
|
.translation-buttons {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-buttons .btn-translate {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-buttons .btn-translate:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-buttons .btn-translate:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-buttons .flag-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 0 1px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-buttons .loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--discord-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select2 Customization */
|
||||||
|
.select2-container--default .select2-selection--single {
|
||||||
|
height: 45px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||||
|
line-height: 45px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 43px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Galería */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 900px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
cursor: pointer;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-paper-plane"></i> Crear Mensaje Discord</h1>
|
||||||
|
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-container">
|
||||||
|
<form id="messageForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="destinatario_id">Destinatarios (Canal ID o Usuario ID) *</label>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<div style="flex-grow: 1;">
|
||||||
|
<select id="destinatario_select" class="form-control" style="width: 100%;" multiple="multiple">
|
||||||
|
<?php foreach ($destinatarios as $dest): ?>
|
||||||
|
<option value="<?php echo htmlspecialchars($dest['identificador']); ?>">
|
||||||
|
<?php echo htmlspecialchars($dest['nombre']); ?> (<?php echo $dest['tipo']; ?>)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="flex-grow: 1;">
|
||||||
|
<input type="text" id="destinatario_manual" name="destinatario_manual" class="form-control" placeholder="O pega IDs manualmente aquí, separados por comas">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-help">Selecciona uno o varios destinatarios guardados, o ingresa IDs manualmente separados por comas.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="plantilla_id">Cargar Plantilla (Opcional)</label>
|
||||||
|
<select id="plantilla_id" class="form-control" onchange="loadTemplate(this.value)">
|
||||||
|
<option value="">-- Seleccionar Plantilla --</option>
|
||||||
|
<?php foreach ($plantillas as $plantilla): ?>
|
||||||
|
<option value="<?php echo htmlspecialchars($plantilla['id']); ?>">
|
||||||
|
<?php echo htmlspecialchars($plantilla['nombre']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contenido">Contenido del Mensaje *</label>
|
||||||
|
<div id="translation-buttons" class="translation-buttons" style="margin-bottom: 10px; display: flex; gap: 5px;">
|
||||||
|
<!-- Los botones se cargarán dinámicamente con JavaScript -->
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="openGallery()" class="btn btn-success" style="margin-bottom: 10px; padding: 5px 10px; font-size: 12px;">
|
||||||
|
<i class="fas fa-images"></i> Insertar Imagen
|
||||||
|
</button>
|
||||||
|
<textarea id="contenido" name="contenido" class="form-control" rows="10" required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tipo de Envío *</label>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="send_immediate" name="send_type" value="inmediato" checked>
|
||||||
|
<label for="send_immediate">Inmediato</label>
|
||||||
|
|
||||||
|
<input type="radio" id="send_scheduled" name="send_type" value="programado" class="ml-3">
|
||||||
|
<label for="send_scheduled">Programado</label>
|
||||||
|
|
||||||
|
<input type="radio" id="send_recurring" name="send_type" value="recurrente" class="ml-3">
|
||||||
|
<label for="send_recurring">Recurrente</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="scheduled_options" class="form-group" style="display: none;">
|
||||||
|
<label for="schedule_datetime">Fecha y Hora de Envío *</label>
|
||||||
|
<input type="datetime-local" id="schedule_datetime" name="schedule_datetime" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="recurring_options" class="form-group" style="display: none;">
|
||||||
|
<label for="recurrence_frequency">Frecuencia de Recurrencia *</label>
|
||||||
|
<select id="recurrence_frequency" name="recurrence_frequency" class="form-control">
|
||||||
|
<option value="diario">Diario</option>
|
||||||
|
<option value="semanal">Semanal</option>
|
||||||
|
<option value="mensual">Mensual</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="recurring_details" style="margin-top: 15px;">
|
||||||
|
<!-- Detalles específicos de recurrencia (día de la semana, día del mes) -->
|
||||||
|
<div id="weekly_options" style="display: none;">
|
||||||
|
<label for="recurrence_day_of_week">Día de la Semana</label>
|
||||||
|
<select id="recurrence_day_of_week" name="recurrence_day_of_week" class="form-control">
|
||||||
|
<option value="1">Lunes</option>
|
||||||
|
<option value="2">Martes</option>
|
||||||
|
<option value="3">Miércoles</option>
|
||||||
|
<option value="4">Jueves</option>
|
||||||
|
<option value="5">Viernes</option>
|
||||||
|
<option value="6">Sábado</option>
|
||||||
|
<option value="7">Domingo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="monthly_options" style="display: none;">
|
||||||
|
<label for="recurrence_day_of_month">Día del Mes</label>
|
||||||
|
<input type="number" id="recurrence_day_of_month" name="recurrence_day_of_month" class="form-control" min="1" max="31">
|
||||||
|
</div>
|
||||||
|
<label for="recurrence_time">Hora de Envío</label>
|
||||||
|
<input type="time" id="recurrence_time" name="recurrence_time" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" id="btnEnviar">
|
||||||
|
<i class="fas fa-paper-plane"></i> Enviar Ahora
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="previewMessage()">
|
||||||
|
<i class="fas fa-eye"></i> Vista Previa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Galería -->
|
||||||
|
<div id="galleryModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeGallery()">×</span>
|
||||||
|
<h2>Galería de Imágenes</h2>
|
||||||
|
<div class="gallery-grid" id="galleryGrid"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Templates Data Hidden -->
|
||||||
|
<script>
|
||||||
|
const templates = <?php echo json_encode($plantillas); ?>;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Función para cargar los idiomas activos
|
||||||
|
function loadActiveLanguages() {
|
||||||
|
return fetch('/shared/languages/get_active.php')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.languages) {
|
||||||
|
return data.languages;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error cargando idiomas:', error);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para crear botones de traducción
|
||||||
|
function createTranslationButtons(languages) {
|
||||||
|
const container = $('#translation-buttons');
|
||||||
|
container.empty();
|
||||||
|
|
||||||
|
languages.forEach(lang => {
|
||||||
|
if (lang.codigo !== 'es') { // No mostramos el botón para español (idioma original)
|
||||||
|
const button = $(`
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary translate-btn"
|
||||||
|
data-lang="${lang.codigo}"
|
||||||
|
title="Traducir a ${lang.nombre}">
|
||||||
|
<span class="flag-icon flag-icon-${lang.bandera || 'globe'}"></span>
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
container.append(button);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manejador de eventos para los botones de traducción
|
||||||
|
$('.translate-btn').on('click', async function() {
|
||||||
|
const targetLang = $(this).data('lang');
|
||||||
|
const content = $('#contenido').val();
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
alert('Por favor, ingrese un mensaje para traducir.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/shared/translations/translate.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: content,
|
||||||
|
target: targetLang,
|
||||||
|
source: 'es' // Asumimos que el texto original está en español
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Agregar la traducción al final del contenido
|
||||||
|
const translatedText = `\n\n--- Traducción a ${targetLang.toUpperCase()} ---\n${data.translatedText}`;
|
||||||
|
$('#contenido').val(content + translatedText);
|
||||||
|
|
||||||
|
// Eliminar el botón de traducción
|
||||||
|
$(this).remove();
|
||||||
|
} else {
|
||||||
|
alert('Error al traducir: ' + (data.error || 'Error desconocido'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al traducir:', error);
|
||||||
|
alert('Error al conectar con el servicio de traducción.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar los botones de traducción al iniciar
|
||||||
|
$(document).ready(function() {
|
||||||
|
loadActiveLanguages().then(languages => {
|
||||||
|
createTranslationButtons(languages);
|
||||||
|
});
|
||||||
|
$('#contenido').summernote({
|
||||||
|
height: 300,
|
||||||
|
toolbar: [
|
||||||
|
['style', ['style']],
|
||||||
|
['font', ['bold', 'underline', 'clear']],
|
||||||
|
['color', ['color']],
|
||||||
|
['para', ['ul', 'ol', 'paragraph']],
|
||||||
|
['insert', ['link']],
|
||||||
|
['view', ['fullscreen', 'codeview', 'help']]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicializar Select2 para selección múltiple
|
||||||
|
$('#destinatario_select').select2({
|
||||||
|
placeholder: "-- Seleccionar Destinatarios Guardados --",
|
||||||
|
allowClear: true // Permite deseleccionar
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sincronizar select con input manual
|
||||||
|
$('#destinatario_select').on('change', function() {
|
||||||
|
const selectedIds = $(this).val(); // Array de IDs del select2
|
||||||
|
const manualIds = $('#destinatario_manual').val().split(',').map(id => id.trim()).filter(id => id !== '');
|
||||||
|
|
||||||
|
// Combinar y eliminar duplicados
|
||||||
|
const combinedIds = [...new Set([...selectedIds, ...manualIds])];
|
||||||
|
|
||||||
|
// Actualizar el campo manual para reflejar todas las selecciones y entradas manuales
|
||||||
|
$('#destinatario_manual').val(combinedIds.join(', '));
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#destinatario_manual').on('input', function() {
|
||||||
|
const manualIds = $(this).val().split(',').map(id => id.trim()).filter(id => id !== '');
|
||||||
|
const selectedIds = $('#destinatario_select').val(); // IDs del select2
|
||||||
|
|
||||||
|
// Asegurarse de que el select2 no se deseleccione si se añade manualmente
|
||||||
|
// Esto es complejo si se quiere mantener el estado exacto en ambos sentidos.
|
||||||
|
// Para simplificar, solo aseguramos que el input manual tenga todos los IDs.
|
||||||
|
const combinedIds = [...new Set([...manualIds, ...selectedIds])];
|
||||||
|
// Intentar seleccionar en select2 lo que está en manual si existe
|
||||||
|
$('#destinatario_select').val(manualIds).trigger('change');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadTemplate(id) {
|
||||||
|
if (!id) return;
|
||||||
|
const template = templates.find(t => t.id == id);
|
||||||
|
if (template) {
|
||||||
|
if (confirm('¿Reemplazar el contenido actual con la plantilla?')) {
|
||||||
|
$('#contenido').summernote('code', template.contenido);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGallery() {
|
||||||
|
$('#galleryModal').show();
|
||||||
|
loadGalleryImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGallery() {
|
||||||
|
$('#galleryModal').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGalleryImages() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gallery/api/list.php');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const grid = document.getElementById('galleryGrid');
|
||||||
|
if (data.success && data.images.length > 0) {
|
||||||
|
grid.innerHTML = data.images.map(img => `
|
||||||
|
<div class="gallery-item" onclick="insertImage('${img.url}')">
|
||||||
|
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
grid.innerHTML = '<p>No hay imágenes.</p>';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertImage(url) {
|
||||||
|
const fullUrl = window.location.origin + url;
|
||||||
|
$('#contenido').summernote('insertImage', fullUrl);
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewMessage() {
|
||||||
|
const content = $('#contenido').summernote('code');
|
||||||
|
const win = window.open('', 'Preview', 'width=800,height=600');
|
||||||
|
win.document.write('<div style="padding:20px;font-family:sans-serif;">' + content + '</div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cerrar modal click fuera
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target == document.getElementById('galleryModal')) {
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lógica para mostrar/ocultar campos de programación/recurrencia
|
||||||
|
const sendTypeRadios = document.querySelectorAll('input[name="send_type"]');
|
||||||
|
const scheduledOptions = document.getElementById('scheduled_options');
|
||||||
|
const recurringOptions = document.getElementById('recurring_options');
|
||||||
|
const recurrenceFrequency = document.getElementById('recurrence_frequency');
|
||||||
|
const weeklyOptions = document.getElementById('weekly_options');
|
||||||
|
const monthlyOptions = document.getElementById('monthly_options');
|
||||||
|
|
||||||
|
function toggleSendTypeOptions() {
|
||||||
|
const selectedSendType = document.querySelector('input[name="send_type"]:checked').value;
|
||||||
|
scheduledOptions.style.display = 'none';
|
||||||
|
recurringOptions.style.display = 'none';
|
||||||
|
|
||||||
|
if (selectedSendType === 'programado') {
|
||||||
|
scheduledOptions.style.display = 'block';
|
||||||
|
} else if (selectedSendType === 'recurrente') {
|
||||||
|
recurringOptions.style.display = 'block';
|
||||||
|
toggleRecurringDetails(); // Mostrar detalles específicos al cargar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRecurringDetails() {
|
||||||
|
const selectedFrequency = recurrenceFrequency.value;
|
||||||
|
weeklyOptions.style.display = 'none';
|
||||||
|
monthlyOptions.style.display = 'none';
|
||||||
|
|
||||||
|
if (selectedFrequency === 'semanal') {
|
||||||
|
weeklyOptions.style.display = 'block';
|
||||||
|
} else if (selectedFrequency === 'mensual') {
|
||||||
|
monthlyOptions.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar al cargar la página
|
||||||
|
toggleSendTypeOptions();
|
||||||
|
|
||||||
|
// Escuchar cambios en los tipos de envío
|
||||||
|
sendTypeRadios.forEach(radio => {
|
||||||
|
radio.addEventListener('change', toggleSendTypeOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Escuchar cambios en la frecuencia de recurrencia
|
||||||
|
recurrenceFrequency.addEventListener('change', toggleRecurringDetails);
|
||||||
|
|
||||||
|
$('#messageForm').on('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const selectedDestinatarios = $('#destinatario_select').val() || []; // Array de IDs del select2
|
||||||
|
const manualDestinatarios = $('#destinatario_manual').val()
|
||||||
|
.split(',')
|
||||||
|
.map(id => id.trim())
|
||||||
|
.filter(id => id !== '');
|
||||||
|
|
||||||
|
// Combinar y eliminar duplicados de ambos orígenes
|
||||||
|
const destinatarios = [...new Set([...selectedDestinatarios, ...manualDestinatarios])];
|
||||||
|
|
||||||
|
const contenido = $('#contenido').summernote('code');
|
||||||
|
const sendType = document.querySelector('input[name="send_type"]:checked').value;
|
||||||
|
let scheduleData = {};
|
||||||
|
|
||||||
|
if (destinatarios.length === 0 || !contenido) {
|
||||||
|
alert('Por favor selecciona al menos un destinatario y escribe un mensaje.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendType === 'programado') {
|
||||||
|
const scheduleDatetime = $('#schedule_datetime').val();
|
||||||
|
if (!scheduleDatetime) {
|
||||||
|
alert('Por favor, selecciona la fecha y hora de envío programado.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleData = {
|
||||||
|
fecha_envio: scheduleDatetime
|
||||||
|
};
|
||||||
|
} else if (sendType === 'recurrente') {
|
||||||
|
const recurrenceFrequencyVal = $('#recurrence_frequency').val();
|
||||||
|
const recurrenceTime = $('#recurrence_time').val();
|
||||||
|
|
||||||
|
if (!recurrenceFrequencyVal || !recurrenceTime) {
|
||||||
|
alert('Por favor, completa la frecuencia y hora de envío recurrente.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleData = {
|
||||||
|
frecuencia: recurrenceFrequencyVal,
|
||||||
|
hora_envio: recurrenceTime
|
||||||
|
};
|
||||||
|
|
||||||
|
if (recurrenceFrequencyVal === 'semanal') {
|
||||||
|
const dayOfWeek = $('#recurrence_day_of_week').val();
|
||||||
|
if (!dayOfWeek) {
|
||||||
|
alert('Por favor, selecciona el día de la semana para el envío recurrente.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleData.dia_semana = dayOfWeek;
|
||||||
|
} else if (recurrenceFrequencyVal === 'mensual') {
|
||||||
|
const dayOfMonth = $('#recurrence_day_of_month').val();
|
||||||
|
if (!dayOfMonth || dayOfMonth < 1 || dayOfMonth > 31) {
|
||||||
|
alert('Por favor, ingresa un día válido del mes (1-31) para el envío recurrente.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleData.dia_mes = dayOfMonth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`¿Enviar mensaje (${sendType}) a ${destinatarios.length} destinatario(s)?`)) return;
|
||||||
|
|
||||||
|
$('#btnEnviar').prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Enviando...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/messages/send.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
destinatario_id: destinatarios, // Ahora puede ser un array
|
||||||
|
contenido: contenido,
|
||||||
|
tipo_envio: sendType,
|
||||||
|
...scheduleData // Añadir datos de programación/recurrencia
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('¡Mensaje enviado correctamente!');
|
||||||
|
window.location.href = '/discord/views/messages/sent.php';
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (result.error || 'Error desconocido'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
} finally {
|
||||||
|
$('#btnEnviar').prop('disabled', false).html('<i class="fas fa-paper-plane"></i> Enviar Ahora');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
39
discord/views/messages/php_errors.log
Executable file
39
discord/views/messages/php_errors.log
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
[29-Nov-2025 04:52:45 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
|
||||||
|
#1 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
|
||||||
|
[29-Nov-2025 04:53:24 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
|
||||||
|
#1 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
|
||||||
|
[29-Nov-2025 04:53:28 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
|
||||||
|
#1 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
|
||||||
|
[29-Nov-2025 04:54:04 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
|
||||||
|
#1 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
|
||||||
|
[30-Nov-2025 15:23:38 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in /var/www/html/bot/discord/views/messages/sent.php:51
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/discord/views/messages/sent.php(51): PDOStatement->execute()
|
||||||
|
#1 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/views/messages/sent.php on line 51
|
||||||
|
[30-Nov-2025 15:24:23 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in /var/www/html/bot/discord/views/messages/sent.php:51
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/discord/views/messages/sent.php(51): PDOStatement->execute()
|
||||||
|
#1 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/views/messages/sent.php on line 51
|
||||||
|
[30-Nov-2025 15:25:34 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in /var/www/html/bot/discord/views/messages/sent.php:51
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/discord/views/messages/sent.php(51): PDOStatement->execute()
|
||||||
|
#1 {main}
|
||||||
|
thrown in /var/www/html/bot/discord/views/messages/sent.php on line 51
|
||||||
|
[30-Nov-2025 16:17:08 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
|
||||||
|
[30-Nov-2025 16:19:47 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
|
||||||
|
[30-Nov-2025 16:20:43 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
|
||||||
|
[30-Nov-2025 16:22:34 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
|
||||||
377
discord/views/messages/sent.php
Executable file
377
discord/views/messages/sent.php
Executable file
@@ -0,0 +1,377 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Paginación
|
||||||
|
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||||
|
$limit = 10;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
// Obtener estado de filtro si existe
|
||||||
|
$filterStatus = $_GET['status'] ?? 'todos'; // Default a 'todos'
|
||||||
|
|
||||||
|
// Construir la cláusula WHERE para filtrar por estado
|
||||||
|
$whereClause = "WHERE m.estado != 'deshabilitado'"; // No mostrar deshabilitados por defecto en la lista principal
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($filterStatus !== 'todos') {
|
||||||
|
$whereClause = "WHERE m.estado = ?";
|
||||||
|
$params[] = $filterStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Obtener total de mensajes (según filtro)
|
||||||
|
$stmt = $db->prepare("SELECT COUNT(*) FROM mensajes_discord m {$whereClause}");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$totalMessages = $stmt->fetchColumn();
|
||||||
|
$totalPages = ceil($totalMessages / $limit);
|
||||||
|
|
||||||
|
// Obtener mensajes (según filtro)
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT m.*, u.username
|
||||||
|
FROM mensajes_discord m
|
||||||
|
LEFT JOIN usuarios u ON m.usuario_id = u.id
|
||||||
|
{$whereClause}
|
||||||
|
ORDER BY m.fecha_envio DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
");
|
||||||
|
$stmt->bindValue(count($params) + 1, $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(count($params) + 2, $offset, PDO::PARAM_INT);
|
||||||
|
|
||||||
|
// Bind WHERE parameters manually
|
||||||
|
foreach ($params as $k => $v) {
|
||||||
|
$stmt->bindValue($k + 1, $v);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
$mensajes = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<!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>Mensajes - Discord</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-dest {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--discord-color);
|
||||||
|
background: #eef0ff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status {
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-enviado { background-color: #28a745; }
|
||||||
|
.status-pendiente { background-color: #ffc107; }
|
||||||
|
.status-fallido { background-color: #dc3545; }
|
||||||
|
.status-deshabilitado { background-color: #6c757d; }
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
background: white;
|
||||||
|
color: var(--discord-color);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link.active {
|
||||||
|
background: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.filter-buttons .btn {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.filter-buttons .btn.active {
|
||||||
|
background-color: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-history"></i> Historial de Mensajes Discord</h1>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
<a href="create.php" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Nuevo Mensaje
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<a href="?status=todos" class="btn <?php echo $filterStatus === 'todos' ? 'active' : ''; ?>">Todos</a>
|
||||||
|
<a href="?status=enviado" class="btn <?php echo $filterStatus === 'enviado' ? 'active' : ''; ?>">Enviados</a>
|
||||||
|
<a href="?status=pendiente" class="btn <?php echo $filterStatus === 'pendiente' ? 'active' : ''; ?>">Pendientes</a>
|
||||||
|
<a href="?status=fallido" class="btn <?php echo $filterStatus === 'fallido' ? 'active' : ''; ?>">Fallidos</a>
|
||||||
|
<a href="?status=deshabilitado" class="btn <?php echo $filterStatus === 'deshabilitado' ? 'active' : ''; ?>">Deshabilitados</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (empty($mensajes)): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-inbox" style="font-size: 48px; margin-bottom: 20px; color: #ddd;"></i>
|
||||||
|
<h2>No hay mensajes en este estado</h2>
|
||||||
|
<p>Los mensajes que cumplan este criterio aparecerán aquí.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="messages-list">
|
||||||
|
<?php foreach ($mensajes as $msg): ?>
|
||||||
|
<div class="message-card">
|
||||||
|
<div class="message-header">
|
||||||
|
<div>
|
||||||
|
Enviado a: <span class="message-dest"><?php echo htmlspecialchars($msg['canal_id']); ?></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="message-status status-<?php echo htmlspecialchars($msg['estado']); ?>">
|
||||||
|
<?php echo htmlspecialchars($msg['estado']); ?>
|
||||||
|
</span>
|
||||||
|
<i class="fas fa-clock" style="margin-left: 10px;"></i> <?php echo date('d/m/Y H:i', strtotime($msg['fecha_envio'] ?? 'now')); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-content"><?php echo strip_tags($msg['contenido']); ?></div>
|
||||||
|
|
||||||
|
<div class="message-footer">
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-user"></i> Por: <?php echo htmlspecialchars($msg['username'] ?? 'Sistema'); ?>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
ID Discord: <?php echo htmlspecialchars($msg['mensaje_discord_id'] ?? 'N/A'); ?>
|
||||||
|
<button class="btn btn-warning btn-sm" onclick="retryMessage(<?php echo $msg['id']; ?>)" style="margin-left: 15px;">
|
||||||
|
<i class="fas fa-sync-alt"></i> Reintentar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="deleteMessage(<?php echo $msg['id']; ?>)" style="margin-left: 5px;">
|
||||||
|
<i class="fas fa-trash"></i> Eliminar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($totalPages > 1): ?>
|
||||||
|
<div class="pagination">
|
||||||
|
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||||
|
<a href="?page=<?php echo $i; ?>&status=<?php echo htmlspecialchars($filterStatus); ?>" class="page-link <?php echo $i === $page ? 'active' : ''; ?>">
|
||||||
|
<?php echo $i; ?>
|
||||||
|
</a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function retryMessage(messageId) {
|
||||||
|
if (!confirm('¿Estás seguro de que quieres volver a poner este mensaje en la cola como "pendiente"?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/messages/retry.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ id: messageId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Mensaje marcado como pendiente.');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error al reintentar el mensaje: ' + (result.error || 'Error desconocido.'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al enviar la solicitud de reintento:', error);
|
||||||
|
alert('Error de conexión al intentar reintentar el mensaje.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMessage(messageId) {
|
||||||
|
if (!confirm('¿Estás seguro de que quieres eliminar/deshabilitar este mensaje?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/messages/delete.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ id: messageId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Mensaje deshabilitado correctamente.');
|
||||||
|
location.reload(); // Recargar la página para actualizar la lista
|
||||||
|
} else {
|
||||||
|
alert('Error al deshabilitar el mensaje: ' + (result.error || 'Error desconocido.'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al enviar la solicitud de eliminación:', error);
|
||||||
|
alert('Error de conexión al intentar deshabilitar el mensaje.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
461
discord/views/recipients/list.php
Executable file
461
discord/views/recipients/list.php
Executable file
@@ -0,0 +1,461 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar permiso para ver la página de destinatarios
|
||||||
|
if (!hasPermission('view_recipients', 'discord')) {
|
||||||
|
die('No tienes permiso para ver los destinatarios de Discord.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Obtener destinatarios
|
||||||
|
$stmt = $db->query("SELECT * FROM destinatarios_discord ORDER BY nombre ASC");
|
||||||
|
$destinatarios = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<!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>Destinatarios Discord - Sistema de Bots</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-channel {
|
||||||
|
background: #e3e7ff;
|
||||||
|
color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-user {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-grupo {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--discord-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.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: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
float: right;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-users"></i> Destinatarios Discord</h1>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
<?php if (hasPermission('manage_recipients', 'discord')): ?>
|
||||||
|
<button onclick="openCreateModal()" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Nuevo Destinatario
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<?php if (empty($destinatarios)): ?>
|
||||||
|
<div style="text-align: center; padding: 40px; color: #666;">
|
||||||
|
<i class="fas fa-users-slash" style="font-size: 48px; margin-bottom: 20px; color: #ddd;"></i>
|
||||||
|
<h3>No hay destinatarios guardados</h3>
|
||||||
|
<p>Agrega canales o usuarios frecuentes para enviar mensajes más rápido.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nombre</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>ID Discord</th>
|
||||||
|
<th>Fecha Registro</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($destinatarios as $dest): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong><?php echo htmlspecialchars($dest['nombre']); ?></strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge <?php
|
||||||
|
if ($dest['tipo'] == 'canal') echo 'badge-channel';
|
||||||
|
else if ($dest['tipo'] == 'usuario') echo 'badge-user';
|
||||||
|
else if ($dest['tipo'] == 'grupo') echo 'badge-grupo';
|
||||||
|
?>">
|
||||||
|
<?php echo ucfirst($dest['tipo']); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><code><?php echo htmlspecialchars($dest['discord_id']); ?></code></td>
|
||||||
|
<td><?php echo date('d/m/Y', strtotime($dest['fecha_registro'])); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if (hasPermission('manage_recipients', 'discord')): ?>
|
||||||
|
<button onclick="editRecipient(<?php echo $dest['id']; ?>, '<?php echo htmlspecialchars($dest['nombre'], ENT_QUOTES); ?>', '<?php echo htmlspecialchars($dest['discord_id'], ENT_QUOTES); ?>', '<?php echo htmlspecialchars($dest['tipo'], ENT_QUOTES); ?>')" class="btn btn-primary" style="padding: 5px 10px; font-size: 12px;">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<?php if ($dest['tipo'] === 'usuario'): ?>
|
||||||
|
<button onclick="kickRecipient(<?php echo $dest['id']; ?>)" class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;">
|
||||||
|
<i class="fas fa-user-slash"></i> Expulsar
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<button onclick="deleteRecipient(<?php echo $dest['id']; ?>)" class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Crear -->
|
||||||
|
<div id="createModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeCreateModal()">×</span>
|
||||||
|
<h2 style="margin-bottom: 20px;">Nuevo Destinatario</h2>
|
||||||
|
<form id="createForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nombre (Alias)</label>
|
||||||
|
<input type="text" name="nombre" class="form-control" required placeholder="Ej: Canal General">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>ID de Discord</label>
|
||||||
|
<input type="text" name="discord_id" class="form-control" required placeholder="Ej: 123456789012345678">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tipo</label>
|
||||||
|
<select name="tipo" class="form-control">
|
||||||
|
<option value="canal">Canal</option>
|
||||||
|
<option value="usuario">Usuario (DM)</option>
|
||||||
|
<option value="grupo">Grupo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%;">Guardar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Editar -->
|
||||||
|
<div id="editModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeEditModal()">×</span>
|
||||||
|
<h2 style="margin-bottom: 20px;">Editar Destinatario</h2>
|
||||||
|
<form id="editForm">
|
||||||
|
<input type="hidden" id="edit_id" name="id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nombre (Alias)</label>
|
||||||
|
<input type="text" id="edit_nombre" name="nombre" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>ID de Discord</label>
|
||||||
|
<input type="text" id="edit_discord_id" name="discord_id" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tipo</label>
|
||||||
|
<select id="edit_tipo" name="tipo" class="form-control">
|
||||||
|
<option value="canal">Canal</option>
|
||||||
|
<option value="usuario">Usuario (DM)</option>
|
||||||
|
<option value="grupo">Grupo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="width: 100%;">Guardar Cambios</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openCreateModal() {
|
||||||
|
document.getElementById('createModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreateModal() {
|
||||||
|
document.getElementById('createModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal() {
|
||||||
|
document.getElementById('editModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditModal() {
|
||||||
|
document.getElementById('editModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function editRecipient(id, nombre, discord_id, tipo) {
|
||||||
|
document.getElementById('edit_id').value = id;
|
||||||
|
document.getElementById('edit_nombre').value = nombre;
|
||||||
|
document.getElementById('edit_discord_id').value = discord_id;
|
||||||
|
document.getElementById('edit_tipo').value = tipo;
|
||||||
|
openEditModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('createForm').onsubmit = async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/recipients/create.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('editForm').onsubmit = async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/recipients/edit.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function deleteRecipient(id) {
|
||||||
|
if (!confirm('¿Eliminar este destinatario? Esta acción es solo local.')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/recipients/delete.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({id})
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function kickRecipient(id) {
|
||||||
|
if (!confirm('¿Estás seguro de expulsar este usuario de Discord? Esta acción es irreversible en Discord.')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/recipients/kick.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({id})
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Usuario expulsado de Discord y eliminado de la lista local.');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error al expulsar: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target == document.getElementById('createModal')) {
|
||||||
|
closeCreateModal();
|
||||||
|
}
|
||||||
|
if (event.target == document.getElementById('editModal')) {
|
||||||
|
closeEditModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
discord/views/recipients/php_errors.log
Executable file
1
discord/views/recipients/php_errors.log
Executable file
@@ -0,0 +1 @@
|
|||||||
|
[29-Nov-2025 21:47:58 America/Mexico_City] PHP Fatal error: Cannot redeclare hasPermission() (previously declared in /var/www/html/bot/shared/utils/helpers.php:97) in /var/www/html/bot/shared/auth/jwt.php on line 216
|
||||||
529
discord/views/templates/create.php
Executable file
529
discord/views/templates/create.php
Executable file
@@ -0,0 +1,529 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// 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__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar permiso para ver y crear plantillas
|
||||||
|
if (!hasPermission('editar_plantillas')) {
|
||||||
|
die('No tienes permiso para crear plantillas de Discord.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP logic for initial display, not for form processing
|
||||||
|
$error = $_GET['error'] ?? '';
|
||||||
|
$success = $_GET['success'] ?? '';
|
||||||
|
?>
|
||||||
|
<!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>Crear Plantilla - Discord</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: #fee;
|
||||||
|
color: #c33;
|
||||||
|
border-left: 4px solid #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #efe;
|
||||||
|
color: #3c3;
|
||||||
|
border-left: 4px solid #3c3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--discord-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-editor {
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-editor.note-frame {
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal de galería */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 900px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
cursor: pointer;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area:hover {
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-plus"></i> Crear Plantilla Discord</h1>
|
||||||
|
<a href="list.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-container">
|
||||||
|
<div id="alert-messages">
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="createTemplateForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="nombre">Nombre de la Plantilla *</label>
|
||||||
|
<input type="text" id="nombre" name="nombre" required value="">
|
||||||
|
<div class="form-help">Nombre descriptivo para identificar la plantilla</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="comando">Comando (opcional)</label>
|
||||||
|
<input type="text" id="comando" name="comando" placeholder="Ej: /comandos, #asedio" value="">
|
||||||
|
<div class="form-help">Comando para invocar esta plantilla en Discord. Debe ser único.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contenido">Contenido *</label>
|
||||||
|
<button type="button" onclick="openGallery()" class="btn btn-success" style="margin-bottom: 10px;">
|
||||||
|
<i class="fas fa-images"></i> Insertar Imagen
|
||||||
|
</button>
|
||||||
|
<textarea id="contenido" name="contenido"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Guardar Plantilla
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="previewContent()" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-eye"></i> Vista Previa
|
||||||
|
</button>
|
||||||
|
<a href="list.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancelar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Galería -->
|
||||||
|
<div id="galleryModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2><i class="fas fa-images"></i> Galería de Imágenes</h2>
|
||||||
|
<span class="close" onclick="closeGallery()">×</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
|
||||||
|
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: #ddd;"></i>
|
||||||
|
<p style="margin-top: 10px; color: #666;">Haz clic para subir una imagen o arrastra aquí</p>
|
||||||
|
<input type="file" id="fileInput" accept="image/*" onchange="uploadImage(this)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gallery-grid" id="galleryGrid">
|
||||||
|
<p style="text-align: center; color: #999;">Cargando imágenes...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||||
|
<script>
|
||||||
|
console.log('Scripts loaded');
|
||||||
|
console.log('jQuery:', typeof $);
|
||||||
|
console.log('Summernote:', typeof $.fn.summernote);
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
console.log('Document ready');
|
||||||
|
console.log('Textarea exists:', $('#contenido').length);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$('#contenido').summernote({
|
||||||
|
height: 300,
|
||||||
|
toolbar: [
|
||||||
|
['style', ['style']],
|
||||||
|
['font', ['bold', 'underline', 'clear']],
|
||||||
|
['color', ['color']],
|
||||||
|
['para', ['ul', 'ol', 'paragraph']],
|
||||||
|
['table', ['table']],
|
||||||
|
['insert', ['link']],
|
||||||
|
['view', ['fullscreen', 'codeview', 'help']]
|
||||||
|
],
|
||||||
|
placeholder: 'Escribe el contenido de tu plantilla aquí...'
|
||||||
|
});
|
||||||
|
console.log('Summernote initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing Summernote:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission via Fetch API
|
||||||
|
$('#createTemplateForm').on('submit', async function(e) {
|
||||||
|
e.preventDefault(); // Prevent default form submission
|
||||||
|
|
||||||
|
const nombre = $('#nombre').val();
|
||||||
|
const comando = $('#comando').val();
|
||||||
|
const contenido = $('#contenido').summernote('code'); // Get content from Summernote
|
||||||
|
|
||||||
|
// Clear previous alerts
|
||||||
|
$('#alert-messages').empty();
|
||||||
|
|
||||||
|
if (!nombre || !contenido) {
|
||||||
|
showAlert('El nombre y el contenido son obligatorios.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/templates/create.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ nombre, comando, contenido })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showAlert('Plantilla creada correctamente.', 'success');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = 'list.php';
|
||||||
|
}, 1500); // Redirect after a short delay
|
||||||
|
} else {
|
||||||
|
showAlert(result.error || 'Error desconocido al crear la plantilla.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al enviar la solicitud:', error);
|
||||||
|
showAlert('Error de conexión al guardar la plantilla.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function openGallery() {
|
||||||
|
document.getElementById('galleryModal').style.display = 'block';
|
||||||
|
loadGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGallery() {
|
||||||
|
document.getElementById('galleryModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGallery() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gallery/api/list.php');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const grid = document.getElementById('galleryGrid');
|
||||||
|
|
||||||
|
if (data.success && data.images.length > 0) {
|
||||||
|
grid.innerHTML = data.images.map(img => `
|
||||||
|
<div class="gallery-item" onclick="insertImage('${img.url}')">
|
||||||
|
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
grid.innerHTML = '<p style="text-align: center; color: #999;">No hay imágenes disponibles</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cargando galería:', error);
|
||||||
|
document.getElementById('galleryGrid').innerHTML = '<p style="text-align: center; color: #c33;">Error cargando imágenes</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertImage(url) {
|
||||||
|
const fullUrl = window.location.origin + url;
|
||||||
|
$('#contenido').summernote('insertImage', fullUrl);
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(input) {
|
||||||
|
if (!input.files || !input.files[0]) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', input.files[0]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gallery/api/upload.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
alert('Imagen subida correctamente');
|
||||||
|
loadGallery();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error al subir la imagen');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewContent() {
|
||||||
|
const content = $('#contenido').summernote('code');
|
||||||
|
const win = window.open('', 'Preview', 'width=800,height=600');
|
||||||
|
const html = '<!DOCTYPE html>' +
|
||||||
|
'<html>' +
|
||||||
|
'<head>' +
|
||||||
|
'<title>Vista Previa</title>' +
|
||||||
|
'<style>' +
|
||||||
|
'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 20px; background: #f8f9fa; }' +
|
||||||
|
'.preview-container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }' +
|
||||||
|
'</style>' +
|
||||||
|
'</head>' +
|
||||||
|
'<body>' +
|
||||||
|
'<div class="preview-container">' +
|
||||||
|
content +
|
||||||
|
'</div>' +
|
||||||
|
'</body>' +
|
||||||
|
'</html>';
|
||||||
|
win.document.write(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cerrar modal al hacer clic fuera
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('galleryModal');
|
||||||
|
if (event.target == modal) {
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(message, type) {
|
||||||
|
const alertDiv = `<div class="alert alert-${type}">${message}</div>`;
|
||||||
|
$('#alert-messages').html(alertDiv);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
544
discord/views/templates/edit.php
Executable file
544
discord/views/templates/edit.php
Executable file
@@ -0,0 +1,544 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar permiso para ver y editar plantillas
|
||||||
|
if (!hasPermission('manage_templates', 'discord')) {
|
||||||
|
die('No tienes permiso para editar plantillas de Discord.'); // Mensaje de error más general para evitar leaks
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener ID de la plantilla
|
||||||
|
$id = $_GET['id'] ?? null;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
header('Location: list.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT p.*, c.comando
|
||||||
|
FROM plantillas_discord p
|
||||||
|
LEFT JOIN comandos_discord c ON p.id = c.plantilla_id
|
||||||
|
WHERE p.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$plantilla = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$plantilla) {
|
||||||
|
header('Location: list.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar propiedad de la plantilla si no es Admin
|
||||||
|
if ($userData->rol !== 'Admin' && $plantilla['usuario_id'] != $userData->userId) {
|
||||||
|
die('No tienes permiso para editar esta plantilla, ya que no te pertenece.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = $_GET['error'] ?? '';
|
||||||
|
$success = $_GET['success'] ?? '';
|
||||||
|
?>
|
||||||
|
<!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>Editar Plantilla - Discord</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: #fee;
|
||||||
|
color: #c33;
|
||||||
|
border-left: 4px solid #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #efe;
|
||||||
|
color: #3c3;
|
||||||
|
border-left: 4px solid #3c3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--discord-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-editor {
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-editor.note-frame {
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 900px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
color: var(--discord-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
cursor: pointer;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area:hover {
|
||||||
|
border-color: var(--discord-color);
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-edit"></i> Editar Plantilla</h1>
|
||||||
|
<a href="list.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-container">
|
||||||
|
<div id="alert-messages">
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="editTemplateForm">
|
||||||
|
<input type="hidden" id="templateId" value="<?php echo htmlspecialchars($plantilla['id']); ?>">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="nombre">Nombre de la Plantilla *</label>
|
||||||
|
<input type="text" id="nombre" name="nombre" required value="<?php echo htmlspecialchars($plantilla['nombre']); ?>">
|
||||||
|
<div class="form-help">Nombre descriptivo para identificar la plantilla</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="comando">Comando (opcional)</label>
|
||||||
|
<input type="text" id="comando" name="comando" placeholder="Ej: /comandos, #asedio" value="<?php echo htmlspecialchars($plantilla['comando'] ?? ''); ?>">
|
||||||
|
<div class="form-help">Comando para invocar esta plantilla en Discord. Debe ser único.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contenido">Contenido *</label>
|
||||||
|
<button type="button" onclick="openGallery()" class="btn btn-success" style="margin-bottom: 10px;">
|
||||||
|
<i class="fas fa-images"></i> Insertar Imagen
|
||||||
|
</button>
|
||||||
|
<textarea id="contenido" name="contenido"><?php echo htmlspecialchars($plantilla['contenido']); ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Guardar Cambios
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="previewContent()" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-eye"></i> Vista Previa
|
||||||
|
</button>
|
||||||
|
<a href="list.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancelar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Galería -->
|
||||||
|
<div id="galleryModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2><i class="fas fa-images"></i> Galería de Imágenes</h2>
|
||||||
|
<span class="close" onclick="closeGallery()">×</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
|
||||||
|
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: #ddd;"></i>
|
||||||
|
<p style="margin-top: 10px; color: #666;">Haz clic para subir una imagen o arrastra aquí</p>
|
||||||
|
<input type="file" id="fileInput" accept="image/*" onchange="uploadImage(this)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gallery-grid" id="galleryGrid">
|
||||||
|
<p style="text-align: center; color: #999;">Cargando imágenes...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
console.log('edit.php script loaded and ready');
|
||||||
|
$('#contenido').summernote({
|
||||||
|
height: 300,
|
||||||
|
toolbar: [
|
||||||
|
['style', ['style']],
|
||||||
|
['font', ['bold', 'underline', 'clear']],
|
||||||
|
['color', ['color']],
|
||||||
|
['para', ['ul', 'ol', 'paragraph']],
|
||||||
|
['table', ['table']],
|
||||||
|
['insert', ['link']],
|
||||||
|
['view', ['fullscreen', 'codeview', 'help']]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle form submission via Fetch API
|
||||||
|
$('#editTemplateForm').on('submit', async function(e) {
|
||||||
|
e.preventDefault(); // Prevent default form submission
|
||||||
|
|
||||||
|
const id = $('#templateId').val();
|
||||||
|
const nombre = $('#nombre').val();
|
||||||
|
const comando = $('#comando').val();
|
||||||
|
const contenido = $('#contenido').summernote('code'); // Get content from Summernote
|
||||||
|
|
||||||
|
// Clear previous alerts
|
||||||
|
$('#alert-messages').empty();
|
||||||
|
|
||||||
|
if (!nombre || !contenido) {
|
||||||
|
showAlert('El nombre y el contenido son obligatorios.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/templates/edit.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ id, nombre, comando, contenido })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showAlert('Plantilla actualizada correctamente.', 'success');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = 'list.php';
|
||||||
|
}, 1500); // Redirect after a short delay
|
||||||
|
} else {
|
||||||
|
showAlert(result.error || 'Error desconocido al actualizar la plantilla.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al enviar la solicitud:', error);
|
||||||
|
showAlert('Error de conexión al guardar la plantilla.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function openGallery() {
|
||||||
|
document.getElementById('galleryModal').style.display = 'block';
|
||||||
|
loadGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGallery() {
|
||||||
|
document.getElementById('galleryModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGallery() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gallery/api/list.php');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const grid = document.getElementById('galleryGrid');
|
||||||
|
|
||||||
|
if (data.success && data.images.length > 0) {
|
||||||
|
grid.innerHTML = data.images.map(img => `
|
||||||
|
<div class="gallery-item" onclick="insertImage('${img.url}')">
|
||||||
|
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
grid.innerHTML = '<p style="text-align: center; color: #999;">No hay imágenes disponibles</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cargando galería:', error);
|
||||||
|
document.getElementById('galleryGrid').innerHTML = '<p style="text-align: center; color: #c33;">Error cargando imágenes</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertImage(url) {
|
||||||
|
const fullUrl = window.location.origin + url;
|
||||||
|
$('#contenido').summernote('insertImage', fullUrl);
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(input) {
|
||||||
|
if (!input.files || !input.files[0]) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', input.files[0]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gallery/api/upload.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
alert('Imagen subida correctamente');
|
||||||
|
loadGallery();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error al subir la imagen');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewContent() {
|
||||||
|
const content = $('#contenido').summernote('code');
|
||||||
|
const win = window.open('', 'Preview', 'width=800,height=600');
|
||||||
|
win.document.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Vista Previa</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.preview-container {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="preview-container">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('galleryModal');
|
||||||
|
if (event.target == modal) {
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(message, type) {
|
||||||
|
const alertDiv = `<div class="alert alert-${type}">${message}</div>`;
|
||||||
|
$('#alert-messages').html(alertDiv);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
336
discord/views/templates/list.php
Executable file
336
discord/views/templates/list.php
Executable file
@@ -0,0 +1,336 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Habilitar logging para depuración
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
// Cargar configuración
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar permiso para ver la página
|
||||||
|
if (!hasPermission('view_templates', 'discord')) {
|
||||||
|
die('No tienes permiso para ver las plantillas de Discord.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener plantillas
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->query("
|
||||||
|
SELECT p.*, c.comando, u.username
|
||||||
|
FROM plantillas_discord p
|
||||||
|
LEFT JOIN comandos_discord c ON p.id = c.plantilla_id
|
||||||
|
LEFT JOIN usuarios u ON p.usuario_id = u.id
|
||||||
|
ORDER BY p.fecha_creacion DESC
|
||||||
|
");
|
||||||
|
$plantillas = $stmt->fetchAll();
|
||||||
|
?>
|
||||||
|
<!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>Plantillas Discord - Sistema de Bots</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--discord-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--discord-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.templates-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-command {
|
||||||
|
display: inline-block;
|
||||||
|
background: #e3e7ff;
|
||||||
|
color: var(--discord-color);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-meta {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-content {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-content::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(transparent, #f8f9fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 60px 40px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 64px;
|
||||||
|
color: #ddd;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h2 {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-file-alt"></i> Plantillas Discord</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||||
|
<a href="create.php" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Nueva Plantilla
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<?php if (empty($plantillas)): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
<h2>No hay plantillas creadas</h2>
|
||||||
|
<p>Crea tu primera plantilla para empezar a enviar mensajes en Discord</p>
|
||||||
|
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||||
|
<a href="create.php" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Crear Primera Plantilla
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="templates-grid">
|
||||||
|
<?php foreach ($plantillas as $plantilla): ?>
|
||||||
|
<div class="template-card">
|
||||||
|
<div class="template-header">
|
||||||
|
<div>
|
||||||
|
<div class="template-title"><?php echo htmlspecialchars($plantilla['nombre']); ?></div>
|
||||||
|
<?php if ($plantilla['comando']): ?>
|
||||||
|
<span class="template-command"><?php echo htmlspecialchars($plantilla['comando']); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="template-meta">
|
||||||
|
<i class="fas fa-user"></i> <?php echo htmlspecialchars($plantilla['username'] ?? 'Desconocido'); ?>
|
||||||
|
|
|
||||||
|
<i class="fas fa-clock"></i> <?php echo date('d/m/Y H:i', strtotime($plantilla['fecha_creacion'])); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="template-content">
|
||||||
|
<?php echo $plantilla['contenido']; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="template-actions">
|
||||||
|
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||||
|
<a href="edit.php?id=<?php echo $plantilla['id']; ?>" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-edit"></i> Editar
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (hasPermission('view_templates', 'discord')): ?>
|
||||||
|
<button onclick="previewTemplate(<?php echo $plantilla['id']; ?>)" class="btn btn-secondary btn-sm">
|
||||||
|
<i class="fas fa-eye"></i> Vista Previa
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||||
|
<button onclick="deleteTemplate(<?php echo $plantilla['id']; ?>, '<?php echo htmlspecialchars($plantilla['nombre'], ENT_QUOTES); ?>')" class="btn btn-danger btn-sm">
|
||||||
|
<i class="fas fa-trash"></i> Eliminar
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function previewTemplate(id) {
|
||||||
|
window.open('preview.php?id=' + id, 'preview', 'width=800,height=600');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTemplate(id, nombre) {
|
||||||
|
if (!confirm(`¿Estás seguro de eliminar la plantilla "${nombre}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/templates/delete.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ id })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
alert('Plantilla eliminada correctamente');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error al eliminar: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error de conexión');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
40
discord/views/templates/php_errors.log
Executable file
40
discord/views/templates/php_errors.log
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
[29-Nov-2025 04:28:49 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
|
||||||
|
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
|
||||||
|
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
|
||||||
|
#3 /var/www/html/bot/discord/views/templates/list.php(10): JWTAuth::requireAuth()
|
||||||
|
#4 {main}
|
||||||
|
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
|
||||||
|
[29-Nov-2025 04:37:46 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
|
||||||
|
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
|
||||||
|
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
|
||||||
|
#3 /var/www/html/bot/discord/views/templates/list.php(15): JWTAuth::requireAuth()
|
||||||
|
#4 {main}
|
||||||
|
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
|
||||||
|
[29-Nov-2025 04:38:17 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
|
||||||
|
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
|
||||||
|
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
|
||||||
|
#3 /var/www/html/bot/discord/views/templates/list.php(15): JWTAuth::requireAuth()
|
||||||
|
#4 {main}
|
||||||
|
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
|
||||||
|
[29-Nov-2025 04:40:46 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
|
||||||
|
Stack trace:
|
||||||
|
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
|
||||||
|
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
|
||||||
|
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
|
||||||
|
#3 /var/www/html/bot/discord/views/templates/list.php(15): JWTAuth::requireAuth()
|
||||||
|
#4 {main}
|
||||||
|
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
|
||||||
|
[29-Nov-2025 04:43:07 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[29-Nov-2025 04:44:44 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[29-Nov-2025 05:03:31 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[29-Nov-2025 16:24:11 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[29-Nov-2025 16:37:32 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||||
|
[04-Dec-2025 14:44:31 America/Mexico_City] PHP Warning: Undefined array key "comando" in /var/www/html/bot/discord/views/templates/list.php on line 262
|
||||||
|
[04-Dec-2025 14:45:03 America/Mexico_City] PHP Warning: Undefined array key "comando" in /var/www/html/bot/discord/views/templates/list.php on line 262
|
||||||
|
[04-Dec-2025 14:45:33 America/Mexico_City] PHP Warning: Undefined array key "comando" in /var/www/html/bot/discord/views/templates/list.php on line 262
|
||||||
130
discord/views/templates/preview.php
Executable file
130
discord/views/templates/preview.php
Executable file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||||
|
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
$id = $_GET['id'] ?? null;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
die('ID no proporcionado');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("SELECT * FROM plantillas_discord WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$plantilla = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$plantilla) {
|
||||||
|
die('Plantilla no encontrada');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!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>Vista Previa: <?php echo htmlspecialchars($plantilla['nombre']); ?></title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #313338;
|
||||||
|
color: #dbdee1;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-message {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #313338;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
font-size: 1rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos básicos para simular Discord */
|
||||||
|
strong { font-weight: 700; }
|
||||||
|
em { font-style: italic; }
|
||||||
|
u { text-decoration: underline; }
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #00a8fc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #2b2d31;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #2b2d31;
|
||||||
|
border: 1px solid #1e1f22;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 6px 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 0 4px;
|
||||||
|
border-left: 4px solid #4e5058;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 { font-size: 1.5rem; }
|
||||||
|
h2 { font-size: 1.25rem; }
|
||||||
|
h3 { font-size: 1rem; }
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="discord-message">
|
||||||
|
<div class="message-content">
|
||||||
|
<?php echo $plantilla['contenido']; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
475
discord/views/welcome/config.php
Executable file
475
discord/views/welcome/config.php
Executable file
@@ -0,0 +1,475 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
|
||||||
|
// Verificar permiso para gestionar el mensaje de bienvenida
|
||||||
|
if (!hasPermission('manage_welcome', 'discord')) {
|
||||||
|
die('No tienes permiso para gestionar la configuración del mensaje de bienvenida de Discord.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Manejar guardado
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
// Verificar permiso para la acción de guardar
|
||||||
|
if (!hasPermission('manage_welcome', 'discord')) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'No tienes permiso para guardar la configuración del mensaje de bienvenida.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verificar si ya existe configuración (solo debe haber una por ahora, o una por servidor si escalamos)
|
||||||
|
// Por simplicidad asumimos una configuración global única (id=1) o la creamos
|
||||||
|
|
||||||
|
$canal_id = $input['canal_id'] ?? '';
|
||||||
|
$texto = $input['texto'] ?? '';
|
||||||
|
$imagen_id = !empty($input['imagen_id']) ? $input['imagen_id'] : null;
|
||||||
|
$idiomas_habilitados = json_encode($input['idiomas_habilitados'] ?? []);
|
||||||
|
$registrar = isset($input['registrar_usuario']) ? (int)$input['registrar_usuario'] : 1;
|
||||||
|
$activo = isset($input['activo']) ? (int)$input['activo'] : 1;
|
||||||
|
|
||||||
|
// Intentar actualizar primero
|
||||||
|
$stmt = $db->prepare("SELECT id FROM bienvenida_discord LIMIT 1");
|
||||||
|
$stmt->execute();
|
||||||
|
$exists = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE bienvenida_discord
|
||||||
|
SET canal_id = ?, texto = ?, imagen_id = ?, idiomas_habilitados = ?, registrar_usuario = ?, activo = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$canal_id, $texto, $imagen_id, $idiomas_habilitados, $registrar, $activo, $exists]);
|
||||||
|
} else {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO bienvenida_discord (canal_id, texto, imagen_id, idiomas_habilitados, registrar_usuario, activo)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([$canal_id, $texto, $imagen_id, $idiomas_habilitados, $registrar, $activo]);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener configuración actual
|
||||||
|
$stmt = $db->query("
|
||||||
|
SELECT b.*, g.ruta as imagen_ruta
|
||||||
|
FROM bienvenida_discord b
|
||||||
|
LEFT JOIN gallery g ON b.imagen_id = g.id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$config = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Obtener canales destinatarios
|
||||||
|
$stmt = $db->query("SELECT discord_id, nombre FROM destinatarios_discord WHERE tipo = 'canal' ORDER BY nombre ASC");
|
||||||
|
$canales = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Obtener idiomas activos
|
||||||
|
$stmt = $db->query("SELECT id, codigo, nombre FROM idiomas WHERE activo = 1 ORDER BY nombre ASC");
|
||||||
|
$idiomas = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Decodificar idiomas seleccionados
|
||||||
|
$idiomasSeleccionados = [];
|
||||||
|
if ($config && $config['idiomas_habilitados']) {
|
||||||
|
$idiomasSeleccionados = json_decode($config['idiomas_habilitados'], true) ?? [];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!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>Bienvenida Discord - Sistema de Bots</title>
|
||||||
|
|
||||||
|
<!-- FontAwesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Summernote CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Select2 CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--discord-color: #5865F2;
|
||||||
|
--discord-dark: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||||
|
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: var(--discord-color); font-size: 24px; }
|
||||||
|
|
||||||
|
.container { max-width: 1000px; margin: 0 auto; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group { margin-bottom: 20px; }
|
||||||
|
.form-group label { display: block; margin-bottom: 8px; font-weight: 600; }
|
||||||
|
.form-control { width: 100%; padding: 10px; border: 2px solid #eee; border-radius: 8px; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.btn-primary { background: var(--discord-color); color: white; }
|
||||||
|
.btn-secondary { background: #6c757d; color: white; }
|
||||||
|
|
||||||
|
.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(--discord-color); }
|
||||||
|
input:checked + .slider:before { transform: translateX(26px); }
|
||||||
|
|
||||||
|
/* Modal Galería */
|
||||||
|
.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: 5% auto; padding: 20px; width: 80%; max-width: 900px; border-radius: 15px; max-height: 80vh; overflow-y: auto; }
|
||||||
|
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 20px; }
|
||||||
|
.gallery-item { border: 2px solid #eee; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.2s; position: relative; }
|
||||||
|
.gallery-item:hover { border-color: var(--discord-color); transform: translateY(-2px); }
|
||||||
|
.gallery-item img { width: 100%; height: 120px; object-fit: cover; }
|
||||||
|
.gallery-item.selected { border-color: var(--discord-color); box-shadow: 0 0 0 3px rgba(88, 101, 242, 0.3); }
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.image-preview img { width: 100%; height: 100%; object-fit: contain; }
|
||||||
|
.remove-image {
|
||||||
|
position: absolute; top: 10px; right: 10px;
|
||||||
|
background: rgba(255,0,0,0.8); color: white;
|
||||||
|
border: none; border-radius: 50%; width: 30px; height: 30px;
|
||||||
|
cursor: pointer; display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-handshake"></i> Configuración de Bienvenida</h1>
|
||||||
|
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<form id="welcomeForm">
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label>Activar Bienvenida</label>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" name="activo" <?php echo ($config['activo'] ?? 1) ? 'checked' : ''; ?>>
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label>Registrar Usuario en BD</label>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" name="registrar_usuario" <?php echo ($config['registrar_usuario'] ?? 1) ? 'checked' : ''; ?>>
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Canal de Bienvenida</label>
|
||||||
|
<select name="canal_id" class="form-control select2" required>
|
||||||
|
<option value="">-- Seleccionar Canal --</option>
|
||||||
|
<?php foreach ($canales as $canal): ?>
|
||||||
|
<option value="<?php echo $canal['discord_id']; ?>"
|
||||||
|
<?php echo ($config['canal_id'] ?? '') == $canal['discord_id'] ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($canal['nombre']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small style="color: #666;">Si no aparece, agrégalo en "Destinatarios".</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Idiomas Disponibles (Botones Automáticos)</label>
|
||||||
|
<select name="idiomas_habilitados[]" id="idiomas_habilitados_select" class="form-control" multiple="multiple">
|
||||||
|
<?php foreach ($idiomas as $lang): ?>
|
||||||
|
<option value="<?php echo $lang['codigo']; ?>"
|
||||||
|
<?php echo in_array($lang['codigo'], $idiomasSeleccionados) ? 'selected' : ''; ?>>
|
||||||
|
<?php echo htmlspecialchars($lang['nombre']); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<small style="color: #666;">Selecciona los idiomas que se mostrarán como botones en el mensaje de bienvenida.</small>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<a href="/shared/languages/manager.php" style="font-size: 12px; color: var(--discord-color); text-decoration: none;">
|
||||||
|
<i class="fas fa-cog"></i> Gestionar idiomas
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Mensaje de Bienvenida</label>
|
||||||
|
<textarea id="summernote" name="texto"><?php echo htmlspecialchars($config['texto'] ?? ''); ?></textarea>
|
||||||
|
<small style="color: #666;">Puedes usar {usuario} para mencionar al nuevo miembro.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Imagen Opcional</label>
|
||||||
|
<input type="hidden" id="imagen_id" name="imagen_id" value="<?php echo htmlspecialchars($config['imagen_id'] ?? ''); ?>">
|
||||||
|
<button type="button" onclick="openGallery()" class="btn btn-secondary" style="margin-bottom: 10px;">
|
||||||
|
<i class="fas fa-image"></i> Seleccionar Imagen
|
||||||
|
</button>
|
||||||
|
<div id="imagePreview" class="image-preview">
|
||||||
|
<?php if (!empty($config['imagen_id']) && !empty($config['imagen_ruta'])): ?>
|
||||||
|
<img src="/gallery/uploads/<?php echo basename($config['imagen_ruta']); ?>" alt="Imagen de bienvenida">
|
||||||
|
<button type="button" class="remove-image" style="display:block" onclick="removeImage()">×</button>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color: #ccc;">Sin imagen seleccionada</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||||
|
<?php if (hasPermission('manage_welcome', 'discord')): ?>
|
||||||
|
<button type="submit" class="btn btn-primary" style="flex: 1; justify-content: center;">
|
||||||
|
<i class="fas fa-save"></i> Guardar Configuración
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (hasPermission('manage_welcome', 'discord')): ?>
|
||||||
|
<button type="button" onclick="sendTestMessage()" class="btn btn-success" style="flex: 1; justify-content: center; background: #28a745;">
|
||||||
|
<i class="fas fa-paper-plane"></i> Probar Mensaje
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Galería -->
|
||||||
|
<div id="galleryModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span style="float:right; cursor:pointer; font-size:24px;" onclick="closeGallery()">×</span>
|
||||||
|
<h2>Seleccionar Imagen</h2>
|
||||||
|
<div id="galleryContainer" class="gallery-grid">
|
||||||
|
<!-- Se carga vía AJAX -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Librerías JS -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#summernote').summernote({
|
||||||
|
height: 200,
|
||||||
|
toolbar: [
|
||||||
|
['style', ['bold', 'italic', 'underline', 'clear']],
|
||||||
|
['para', ['ul', 'ol']],
|
||||||
|
['insert', ['link']],
|
||||||
|
['view', ['codeview']]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.select2').select2();
|
||||||
|
$('#idiomas_habilitados_select').select2(); // Inicializar el select2 para idiomas
|
||||||
|
|
||||||
|
// Galería
|
||||||
|
function openGallery() {
|
||||||
|
document.getElementById('galleryModal').style.display = 'block';
|
||||||
|
loadGalleryImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGallery() {
|
||||||
|
document.getElementById('galleryModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGalleryImages() {
|
||||||
|
const container = document.getElementById('galleryContainer');
|
||||||
|
container.innerHTML = '<p>Cargando...</p>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gallery/api/list.php');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
if (data.images && data.images.length > 0) {
|
||||||
|
data.images.forEach(img => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'gallery-item';
|
||||||
|
// La API devuelve url_thumbnail (ruta completa) y nombre_original
|
||||||
|
div.innerHTML = `<img src="${img.url_thumbnail}" alt="${img.nombre_original}">`;
|
||||||
|
// Pasamos img.nombre que es el nombre del archivo físico
|
||||||
|
div.onclick = () => selectImage(img.id, img.nombre, img.ruta); // Pasar también la ruta completa para preview
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
container.innerHTML = '<p>No hay imágenes en la galería.</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
container.innerHTML = '<p>Error cargando imágenes.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectImage(id, filename, ruta_completa) {
|
||||||
|
document.getElementById('imagen_id').value = id;
|
||||||
|
const preview = document.getElementById('imagePreview');
|
||||||
|
preview.innerHTML = `
|
||||||
|
<img src="${ruta_completa}" alt="Imagen de bienvenida">
|
||||||
|
<button type="button" class="remove-image" style="display:block" onclick="removeImage()">×</button>
|
||||||
|
`;
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeImage() {
|
||||||
|
document.getElementById('imagen_id').value = '';
|
||||||
|
document.getElementById('imagePreview').innerHTML = `
|
||||||
|
<span style="color: #ccc;">Sin imagen seleccionada</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar
|
||||||
|
document.getElementById('welcomeForm').onsubmit = async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
// Recolectar datos del formulario
|
||||||
|
const activo = $('input[name="activo"]').is(':checked') ? 1 : 0;
|
||||||
|
const registrar_usuario = $('input[name="registrar_usuario"]').is(':checked') ? 1 : 0;
|
||||||
|
const canal_id = $('select[name="canal_id"]').val();
|
||||||
|
const texto = $('#summernote').summernote('code');
|
||||||
|
const imagen_id = $('#imagen_id').val();
|
||||||
|
const idiomas_habilitados = $('#idiomas_habilitados_select').val(); // Array de códigos de idioma
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
activo: activo,
|
||||||
|
registrar_usuario: registrar_usuario,
|
||||||
|
canal_id: canal_id,
|
||||||
|
texto: texto,
|
||||||
|
imagen_id: imagen_id === '' ? null : imagen_id,
|
||||||
|
idiomas_habilitados: idiomas_habilitados
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(window.location.href, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Configuración guardada correctamente');
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function sendTestMessage() {
|
||||||
|
if (!confirm('¿Enviar mensaje de prueba al canal configurado?')) return;
|
||||||
|
|
||||||
|
const btn = document.querySelector('.btn-success');
|
||||||
|
const originalText = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Enviando...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/discord/api/welcome/send_test.php', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('Mensaje de prueba enviado con éxito a Discord!');
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert('Error de conexión');
|
||||||
|
} finally {
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target == document.getElementById('galleryModal')) {
|
||||||
|
closeGallery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
91
discord/webhook/index.php
Executable file
91
discord/webhook/index.php
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Webhook Principal de Discord
|
||||||
|
* Punto de entrada para interacciones de Discord que redirige al daemon
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Habilitar logging de errores en archivo, no en salida
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
|
ini_set('error_log', __DIR__ . '/../../logs/webhook_errors.log');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../shared/utils/helpers.php';
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constantes de Discord
|
||||||
|
define('DISCORD_PUBLIC_KEY', $_ENV['DISCORD_PUBLIC_KEY'] ?? getenv('DISCORD_PUBLIC_KEY'));
|
||||||
|
|
||||||
|
// Verificar firma de seguridad (Ed25519)
|
||||||
|
function verifySignature($body, $headers) {
|
||||||
|
$signature = $headers['x-signature-ed25519'] ?? '';
|
||||||
|
$timestamp = $headers['x-signature-timestamp'] ?? '';
|
||||||
|
$publicKey = DISCORD_PUBLIC_KEY;
|
||||||
|
|
||||||
|
if (\!$signature || \!$timestamp || \!$publicKey) {
|
||||||
|
error_log("Faltan parámetros de firma");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_exists('sodium_crypto_sign_verify_detached')) {
|
||||||
|
try {
|
||||||
|
$sig = hex2bin($signature);
|
||||||
|
$msg = $timestamp . $body;
|
||||||
|
$key = hex2bin($publicKey);
|
||||||
|
return sodium_crypto_sign_verify_detached($sig, $msg, $key);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error verificando firma (sodium): " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log("ADVERTENCIA: La extensión 'sodium' de PHP no está instalada. La verificación de firma de Discord es INSEGURA.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener headers y body
|
||||||
|
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
|
$body = file_get_contents('php://input');
|
||||||
|
|
||||||
|
// Registrar la solicitud para depuración
|
||||||
|
error_log("Solicitud recibida: " . json_encode([
|
||||||
|
'headers' => array_intersect_key($headers, ['x-signature-ed25519' => true, 'x-signature-timestamp' => true]),
|
||||||
|
'body' => $body
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Verificar firma
|
||||||
|
if (\!verifySignature($body, $headers)) {
|
||||||
|
http_response_code(401);
|
||||||
|
error_log("Firma no válida");
|
||||||
|
echo 'Invalid Signature';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decodificar JSON para verificar si es un PING
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
$type = $data['type'] ?? 0;
|
||||||
|
|
||||||
|
// Manejar PING (requerido por Discord para validar la URL del webhook)
|
||||||
|
if ($type === 1) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['type' => 1]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para otras interacciones, redirigir al daemon
|
||||||
|
$interactionId = $data['id'] ?? 'unknown';
|
||||||
|
error_log("Interacción recibida (ID: $interactionId). El daemon debería manejar esto.");
|
||||||
|
|
||||||
|
// NO responder aquí. El daemon se encargará de la respuesta completa.
|
||||||
|
// Simplemente terminamos el script del webhook con un 200 OK.
|
||||||
|
http_response_code(200);
|
||||||
|
exit;
|
||||||
105
gallery/api/delete.php
Executable file
105
gallery/api/delete.php
Executable file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API de Galería - Eliminar imagen
|
||||||
|
*/
|
||||||
|
|
||||||
|
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__ . '/../../shared/database/connection.php';
|
||||||
|
require_once __DIR__ . '/../../shared/auth/jwt.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
if (!isAuthenticated()) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No autenticado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso
|
||||||
|
if (!hasPermission('delete_gallery_images')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No tienes permiso para eliminar imágenes de la galería.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo admins pueden eliminar
|
||||||
|
if ($userData->rol !== 'Admin' && !JWTAuth::hasPermission($userData, 'gestionar_galeria')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No tiene permisos para eliminar imágenes']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'DELETE' && $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!isset($input['id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Falta el ID de la imagen']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Buscar la imagen
|
||||||
|
$stmt = $db->prepare("SELECT * FROM gallery WHERE id = ?");
|
||||||
|
$stmt->execute([$input['id']]);
|
||||||
|
$image = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$image) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Imagen no encontrada']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar que no esté siendo usada en mensajes de bienvenida
|
||||||
|
$stmt = $db->query("SELECT COUNT(*) as total FROM bienvenida_discord WHERE imagen_id = " . $input['id']);
|
||||||
|
$usedInDiscord = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
$stmt = $db->query("SELECT COUNT(*) as total FROM bienvenida_telegram WHERE imagen_id = " . $input['id']);
|
||||||
|
$usedInTelegram = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
if ($usedInDiscord > 0 || $usedInTelegram > 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No se puede eliminar. La imagen está siendo usada en mensajes de bienvenida']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar archivos físicos
|
||||||
|
$uploadPath = __DIR__ . '/../uploads/' . $image['nombre'];
|
||||||
|
$thumbnailPath = __DIR__ . '/../thumbnails/' . $image['nombre'];
|
||||||
|
|
||||||
|
if (file_exists($uploadPath)) {
|
||||||
|
unlink($uploadPath);
|
||||||
|
}
|
||||||
|
if (file_exists($thumbnailPath)) {
|
||||||
|
unlink($thumbnailPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar de la base de datos
|
||||||
|
$stmt = $db->prepare("DELETE FROM gallery WHERE id = ?");
|
||||||
|
$stmt->execute([$input['id']]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Imagen eliminada correctamente']);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error del servidor']);
|
||||||
|
error_log('Error en delete.php: ' . $e->getMessage());
|
||||||
|
}
|
||||||
67
gallery/api/edit.php
Executable file
67
gallery/api/edit.php
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API de Galería - Editar nombre de imagen
|
||||||
|
*/
|
||||||
|
|
||||||
|
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__ . '/../../shared/database/connection.php';
|
||||||
|
require_once __DIR__ . '/../../shared/auth/jwt.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
if (!isAuthenticated()) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No autenticado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar permiso
|
||||||
|
if (!hasPermission('edit_gallery_images')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No tienes permiso para editar imágenes de la galería.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'PUT' && $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!isset($input['id']) || !isset($input['nombre_original'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Faltan parámetros']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE gallery SET nombre_original = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$input['nombre_original'], $input['id']]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() > 0) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Nombre actualizado correctamente']);
|
||||||
|
} else {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Imagen no encontrada']);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error del servidor']);
|
||||||
|
error_log('Error en edit.php: ' . $e->getMessage());
|
||||||
|
}
|
||||||
109
gallery/api/list.php
Executable file
109
gallery/api/list.php
Executable file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API de Galería - Listar imágenes
|
||||||
|
*/
|
||||||
|
|
||||||
|
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__ . '/../../shared/database/connection.php';
|
||||||
|
require_once __DIR__ . '/../../shared/auth/jwt.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No autenticado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permitir acceso a cualquier usuario autenticado
|
||||||
|
// Opcional: Restringir si es necesario, pero por ahora es compartido
|
||||||
|
// if (!hasPermission('editar_plantillas') && !hasPermission('crear_mensajes')) { ... }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Parámetros de paginación
|
||||||
|
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||||
|
$perPage = isset($_GET['per_page']) ? min(100, max(10, intval($_GET['per_page']))) : 20;
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
// Búsqueda
|
||||||
|
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||||
|
|
||||||
|
// Construir query
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$where[] = "(nombre_original LIKE ? OR nombre LIKE ?)";
|
||||||
|
$params[] = "%{$search}%";
|
||||||
|
$params[] = "%{$search}%";
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
|
// Contar total
|
||||||
|
$stmt = $db->prepare("SELECT COUNT(*) as total FROM gallery $whereClause");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
// Obtener imágenes
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT g.*, u.username
|
||||||
|
FROM gallery g
|
||||||
|
LEFT JOIN usuarios u ON g.usuario_id = u.id
|
||||||
|
$whereClause
|
||||||
|
ORDER BY g.fecha_subida DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$params[] = $perPage;
|
||||||
|
$params[] = $offset;
|
||||||
|
$stmt->execute($params);
|
||||||
|
$images = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Formatear respuesta
|
||||||
|
$formattedImages = array_map(function($img) {
|
||||||
|
return [
|
||||||
|
'id' => $img['id'],
|
||||||
|
'nombre' => $img['nombre'],
|
||||||
|
'nombre_original' => $img['nombre_original'],
|
||||||
|
'url' => '/gallery/uploads/' . $img['nombre'],
|
||||||
|
'url_thumbnail' => '/gallery/thumbnails/' . $img['nombre'],
|
||||||
|
'tipo_mime' => $img['tipo_mime'],
|
||||||
|
'tamano' => $img['tamano'],
|
||||||
|
'ancho' => $img['ancho'],
|
||||||
|
'alto' => $img['alto'],
|
||||||
|
'usuario' => $img['username'],
|
||||||
|
'fecha_subida' => $img['fecha_subida']
|
||||||
|
];
|
||||||
|
}, $images);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'images' => $formattedImages,
|
||||||
|
'pagination' => [
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'total' => $total,
|
||||||
|
'total_pages' => ceil($total / $perPage)
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error del servidor']);
|
||||||
|
error_log('Error en list.php: ' . $e->getMessage());
|
||||||
|
}
|
||||||
24
gallery/api/php_errors.log
Executable file
24
gallery/api/php_errors.log
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
[30-Nov-2025 06:47:39 UTC] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/gallery/api/list.php:23
|
||||||
|
Stack trace:
|
||||||
|
#0 {main}
|
||||||
|
thrown in /var/www/html/bot/gallery/api/list.php on line 23
|
||||||
|
[30-Nov-2025 06:47:42 UTC] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/gallery/api/list.php:23
|
||||||
|
Stack trace:
|
||||||
|
#0 {main}
|
||||||
|
thrown in /var/www/html/bot/gallery/api/list.php on line 23
|
||||||
|
[30-Nov-2025 06:47:50 UTC] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/gallery/api/list.php:23
|
||||||
|
Stack trace:
|
||||||
|
#0 {main}
|
||||||
|
thrown in /var/www/html/bot/gallery/api/list.php on line 23
|
||||||
|
[30-Nov-2025 21:18:28 UTC] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/gallery/api/list.php:23
|
||||||
|
Stack trace:
|
||||||
|
#0 {main}
|
||||||
|
thrown in /var/www/html/bot/gallery/api/list.php on line 23
|
||||||
|
[30-Nov-2025 21:19:22 UTC] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/gallery/api/list.php:23
|
||||||
|
Stack trace:
|
||||||
|
#0 {main}
|
||||||
|
thrown in /var/www/html/bot/gallery/api/list.php on line 23
|
||||||
|
[30-Nov-2025 21:20:14 UTC] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/gallery/api/list.php:23
|
||||||
|
Stack trace:
|
||||||
|
#0 {main}
|
||||||
|
thrown in /var/www/html/bot/gallery/api/list.php on line 23
|
||||||
237
gallery/api/upload.php
Executable file
237
gallery/api/upload.php
Executable file
@@ -0,0 +1,237 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API de Galería - Subida de imágenes
|
||||||
|
*/
|
||||||
|
|
||||||
|
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__ . '/../../shared/database/connection.php';
|
||||||
|
require_once __DIR__ . '/../../shared/auth/jwt.php';
|
||||||
|
|
||||||
|
// Verificar autenticación
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if (!$userData) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No autenticado']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permitir subida a cualquier usuario autenticado
|
||||||
|
// Opcional: Restringir si es necesario
|
||||||
|
// if (!hasPermission('editar_plantillas') && !hasPermission('crear_mensajes')) { ... }
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Método no permitido']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_FILES['image'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No se envió ninguna imagen']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['image'];
|
||||||
|
|
||||||
|
// Validar errores de subida
|
||||||
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error al subir el archivo']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar tipo de archivo
|
||||||
|
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
|
||||||
|
if (!in_array($mimeType, $allowedTypes)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Tipo de archivo no permitido. Solo se aceptan JPG, PNG, GIF y WebP']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar tamaño (máximo 5MB)
|
||||||
|
if ($file['size'] > 5 * 1024 * 1024) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'El archivo es muy grande. Máximo 5MB']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Calcular hash MD5 del archivo
|
||||||
|
$hash = md5_file($file['tmp_name']);
|
||||||
|
|
||||||
|
// Verificar si ya existe una imagen con el mismo hash
|
||||||
|
$stmt = $db->prepare("SELECT * FROM gallery WHERE hash_md5 = ?");
|
||||||
|
$stmt->execute([$hash]);
|
||||||
|
$existing = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
// La imagen ya existe, devolver la existente
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'duplicate' => true,
|
||||||
|
'image' => [
|
||||||
|
'id' => $existing['id'],
|
||||||
|
'nombre' => $existing['nombre'],
|
||||||
|
'ruta' => $existing['ruta'],
|
||||||
|
'ruta_thumbnail' => $existing['ruta_thumbnail'],
|
||||||
|
'url' => '/gallery/uploads/' . $existing['nombre'],
|
||||||
|
'url_thumbnail' => '/gallery/thumbnails/' . $existing['nombre']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener dimensiones de la imagen
|
||||||
|
$imageInfo = getimagesize($file['tmp_name']);
|
||||||
|
$width = $imageInfo[0];
|
||||||
|
$height = $imageInfo[1];
|
||||||
|
|
||||||
|
// Generar nombre único para el archivo
|
||||||
|
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||||
|
$newName = time() . '_' . bin2hex(random_bytes(8)) . '.' . $extension;
|
||||||
|
|
||||||
|
// Rutas de destino
|
||||||
|
$uploadsDir = __DIR__ . '/../uploads/';
|
||||||
|
$thumbnailsDir = __DIR__ . '/../thumbnails/';
|
||||||
|
$uploadPath = $uploadsDir . $newName;
|
||||||
|
$thumbnailPath = $thumbnailsDir . $newName;
|
||||||
|
|
||||||
|
// Crear directorios si no existen
|
||||||
|
if (!is_dir($uploadsDir)) {
|
||||||
|
mkdir($uploadsDir, 0755, true);
|
||||||
|
}
|
||||||
|
if (!is_dir($thumbnailsDir)) {
|
||||||
|
mkdir($thumbnailsDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mover archivo subido
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
|
||||||
|
throw new Exception('Error al guardar el archivo');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear thumbnail
|
||||||
|
createThumbnail($uploadPath, $thumbnailPath, 300, 300);
|
||||||
|
|
||||||
|
// Guardar en la base de datos
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO gallery (nombre, nombre_original, ruta, ruta_thumbnail, hash_md5, tipo_mime, tamano, ancho, alto, usuario_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$newName,
|
||||||
|
$file['name'],
|
||||||
|
'uploads/' . $newName,
|
||||||
|
'thumbnails/' . $newName,
|
||||||
|
$hash,
|
||||||
|
$mimeType,
|
||||||
|
$file['size'],
|
||||||
|
$width,
|
||||||
|
$height,
|
||||||
|
$userData->userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$imageId = $db->lastInsertId();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'duplicate' => false,
|
||||||
|
'image' => [
|
||||||
|
'id' => $imageId,
|
||||||
|
'nombre' => $newName,
|
||||||
|
'ruta' => 'uploads/' . $newName,
|
||||||
|
'ruta_thumbnail' => 'thumbnails/' . $newName,
|
||||||
|
'url' => '/gallery/uploads/' . $newName,
|
||||||
|
'url_thumbnail' => '/gallery/thumbnails/' . $newName
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Error del servidor: ' . $e->getMessage()]);
|
||||||
|
error_log('Error en upload.php: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crear thumbnail de una imagen
|
||||||
|
*/
|
||||||
|
function createThumbnail($sourcePath, $destPath, $maxWidth, $maxHeight) {
|
||||||
|
$imageInfo = getimagesize($sourcePath);
|
||||||
|
$width = $imageInfo[0];
|
||||||
|
$height = $imageInfo[1];
|
||||||
|
$mimeType = $imageInfo['mime'];
|
||||||
|
|
||||||
|
// Cargar imagen según el tipo
|
||||||
|
switch ($mimeType) {
|
||||||
|
case 'image/jpeg':
|
||||||
|
$source = imagecreatefromjpeg($sourcePath);
|
||||||
|
break;
|
||||||
|
case 'image/png':
|
||||||
|
$source = imagecreatefrompng($sourcePath);
|
||||||
|
break;
|
||||||
|
case 'image/gif':
|
||||||
|
$source = imagecreatefromgif($sourcePath);
|
||||||
|
break;
|
||||||
|
case 'image/webp':
|
||||||
|
$source = imagecreatefromwebp($sourcePath);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Exception('Tipo de imagen no soportado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular nuevas dimensiones manteniendo el aspecto
|
||||||
|
$ratio = min($maxWidth / $width, $maxHeight / $height);
|
||||||
|
$newWidth = intval($width * $ratio);
|
||||||
|
$newHeight = intval($height * $ratio);
|
||||||
|
|
||||||
|
// Crear thumbnail
|
||||||
|
$thumbnail = imagecreatetruecolor($newWidth, $newHeight);
|
||||||
|
|
||||||
|
// Preservar transparencia para PNG y GIF
|
||||||
|
if ($mimeType === 'image/png' || $mimeType === 'image/gif') {
|
||||||
|
imagealphablending($thumbnail, false);
|
||||||
|
imagesavealpha($thumbnail, true);
|
||||||
|
$transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
|
||||||
|
imagefilledrectangle($thumbnail, 0, 0, $newWidth, $newHeight, $transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redimensionar
|
||||||
|
imagecopyresampled($thumbnail, $source, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
|
||||||
|
|
||||||
|
// Guardar thumbnail
|
||||||
|
switch ($mimeType) {
|
||||||
|
case 'image/jpeg':
|
||||||
|
imagejpeg($thumbnail, $destPath, 85);
|
||||||
|
break;
|
||||||
|
case 'image/png':
|
||||||
|
imagepng($thumbnail, $destPath, 8);
|
||||||
|
break;
|
||||||
|
case 'image/gif':
|
||||||
|
imagegif($thumbnail, $destPath);
|
||||||
|
break;
|
||||||
|
case 'image/webp':
|
||||||
|
imagewebp($thumbnail, $destPath, 85);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
imagedestroy($source);
|
||||||
|
imagedestroy($thumbnail);
|
||||||
|
}
|
||||||
411
gallery/index.php
Executable file
411
gallery/index.php
Executable file
@@ -0,0 +1,411 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
// 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__ . '/../shared/auth/jwt.php';
|
||||||
|
$userData = JWTAuth::requireAuth();
|
||||||
|
?>
|
||||||
|
<!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>Galería de Imágenes - Sistema de Bots</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #5865F2;
|
||||||
|
--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: var(--primary-color); font-size: 24px; }
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn-primary { background: var(--primary-color); color: white; }
|
||||||
|
.btn-secondary { background: #6c757d; color: white; }
|
||||||
|
.btn-success { background: var(--success-color); color: white; }
|
||||||
|
.btn-danger { background: var(--danger-color); color: white; }
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
border: 3px dashed #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-info {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 500px;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><i class="fas fa-images"></i> Galería de Imágenes</h1>
|
||||||
|
<a href="/index.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
|
||||||
|
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: var(--primary-color); margin-bottom: 15px;"></i>
|
||||||
|
<h3>Arrastra imágenes aquí o haz clic para seleccionar</h3>
|
||||||
|
<p style="color: #666; margin-top: 10px;">Formatos permitidos: JPG, PNG, GIF, WEBP (Máx. 5MB)</p>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="fileInput" accept="image/*" multiple style="display: none;">
|
||||||
|
|
||||||
|
<div id="uploadProgress" style="display: none; margin-top: 20px;">
|
||||||
|
<div style="background: #f8f9fa; padding: 10px; border-radius: 8px;">
|
||||||
|
<div id="progressText">Subiendo...</div>
|
||||||
|
<div style="background: #ddd; height: 20px; border-radius: 10px; margin-top: 10px; overflow: hidden;">
|
||||||
|
<div id="progressBar" style="background: var(--success-color); height: 100%; width: 0%; transition: width 0.3s;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||||
|
<h2 style="margin: 0;">Mis Imágenes</h2>
|
||||||
|
<input type="text" id="searchInput" placeholder="Buscar..." class="form-control" style="max-width: 300px; margin: 0;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="galleryContainer" class="gallery-grid">
|
||||||
|
<p style="text-align: center; color: #666;">Cargando...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Editar -->
|
||||||
|
<div id="editModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>Editar Imagen</h2>
|
||||||
|
<input type="text" id="editName" class="form-control" placeholder="Nombre de la imagen">
|
||||||
|
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||||
|
<button class="btn btn-success" onclick="saveEdit()">Guardar</button>
|
||||||
|
<button class="btn btn-secondary" onclick="closeEditModal()">Cancelar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentEditId = null;
|
||||||
|
|
||||||
|
// Subir archivos
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
const uploadArea = document.querySelector('.upload-area');
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', handleFiles);
|
||||||
|
|
||||||
|
uploadArea.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadArea.style.borderColor = 'var(--primary-color)';
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadArea.addEventListener('dragleave', () => {
|
||||||
|
uploadArea.style.borderColor = '#ddd';
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadArea.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadArea.style.borderColor = '#ddd';
|
||||||
|
fileInput.files = e.dataTransfer.files;
|
||||||
|
handleFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleFiles() {
|
||||||
|
const files = fileInput.files;
|
||||||
|
if (!files.length) return;
|
||||||
|
|
||||||
|
const progressDiv = document.getElementById('uploadProgress');
|
||||||
|
const progressBar = document.getElementById('progressBar');
|
||||||
|
const progressText = document.getElementById('progressText');
|
||||||
|
|
||||||
|
progressDiv.style.display = 'block';
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
progressText.textContent = `Subiendo ${i + 1}/${files.length}: ${file.name}`;
|
||||||
|
progressBar.style.width = ((i / files.length) * 100) + '%';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gallery/api/upload.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
alert('Error subiendo ' + file.name + ': ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error subiendo ' + file.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
progressText.textContent = '¡Completado!';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
progressDiv.style.display = 'none';
|
||||||
|
loadGallery();
|
||||||
|
fileInput.value = '';
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar galería
|
||||||
|
async function loadGallery(search = '') {
|
||||||
|
const container = document.getElementById('galleryContainer');
|
||||||
|
container.innerHTML = '<p style="text-align: center; color: #666;">Cargando...</p>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = '/gallery/api/list.php' + (search ? `?search=${encodeURIComponent(search)}` : '');
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success || !data.images.length) {
|
||||||
|
container.innerHTML = '<p style="text-align: center; color: #666;">No hay imágenes</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
data.images.forEach(img => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'gallery-item';
|
||||||
|
div.innerHTML = `
|
||||||
|
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
|
||||||
|
<div class="gallery-item-info">
|
||||||
|
<div class="gallery-item-name" title="${img.nombre_original}">${img.nombre_original}</div>
|
||||||
|
<div class="gallery-item-meta">
|
||||||
|
${Math.round(img.tamano / 1024)} KB • ${img.ancho}x${img.alto}
|
||||||
|
</div>
|
||||||
|
<div class="gallery-item-actions">
|
||||||
|
<button class="btn btn-primary" style="font-size: 11px; padding: 5px 10px;" onclick="openEditModal(${img.id}, '${img.nombre_original.replace(/'/g, "\\'")}')">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" style="font-size: 11px; padding: 5px 10px;" onclick="deleteImage(${img.id})">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
container.innerHTML = '<p style="text-align: center; color: red;">Error cargando imágenes</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(id, name) {
|
||||||
|
currentEditId = id;
|
||||||
|
document.getElementById('editName').value = name;
|
||||||
|
document.getElementById('editModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditModal() {
|
||||||
|
document.getElementById('editModal').style.display = 'none';
|
||||||
|
currentEditId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEdit() {
|
||||||
|
const newName = document.getElementById('editName').value;
|
||||||
|
if (!newName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gallery/api/edit.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: currentEditId,
|
||||||
|
nombre_original: newName
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
closeEditModal();
|
||||||
|
loadGallery();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteImage(id) {
|
||||||
|
if (!confirm('¿Eliminar esta imagen?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/gallery/api/delete.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({id: id})
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
loadGallery();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error de conexión');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscador
|
||||||
|
let searchTimeout;
|
||||||
|
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
loadGallery(e.target.value);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cargar al inicio
|
||||||
|
loadGallery();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
gallery/thumbnails/1764412975_c67c86ed901f1e25.webp
Executable file
BIN
gallery/thumbnails/1764412975_c67c86ed901f1e25.webp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
gallery/uploads/1764412975_c67c86ed901f1e25.webp
Executable file
BIN
gallery/uploads/1764412975_c67c86ed901f1e25.webp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
289
index.php
Executable file
289
index.php
Executable file
@@ -0,0 +1,289 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/shared/bootstrap.php';
|
||||||
|
|
||||||
|
// El bootstrap.php ya maneja la autenticación y carga $userData
|
||||||
|
// $userData está disponible globalmente a través de JWTAuth::getUserData() si se necesita de nuevo
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
if (isset($_GET['logout'])) {
|
||||||
|
setcookie('auth_token', '', time() - 3600, '/');
|
||||||
|
header('Location: /login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!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 __('main_panel_title'); ?></title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
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: #667eea;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge {
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rol-badge {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platforms {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-icon {
|
||||||
|
font-size: 60px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-card {
|
||||||
|
border-top: 5px solid #5865F2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-card .platform-title {
|
||||||
|
color: #5865F2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegram-card {
|
||||||
|
border-top: 5px solid #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegram-card .platform-title {
|
||||||
|
color: #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-description {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-top: 25px;
|
||||||
|
padding-top: 25px;
|
||||||
|
border-top: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>🤖 <?php echo __('bot_admin_system_title'); ?></h1>
|
||||||
|
<?php if (hasPermission('manage_languages')): ?>
|
||||||
|
<a href="/shared/languages/manager.php" style="text-decoration: none;">
|
||||||
|
<button class="btn-logout" style="background: #28a745; margin-right: 10px;">
|
||||||
|
<i class="fas fa-language"></i> <?php echo __('languages'); ?>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="user-badge">
|
||||||
|
👤 <?php echo htmlspecialchars($userData->username); ?>
|
||||||
|
</div>
|
||||||
|
<div class="rol-badge">
|
||||||
|
<?php echo htmlspecialchars($userData->rol); ?>
|
||||||
|
</div>
|
||||||
|
<a href="?logout=1">
|
||||||
|
<button class="btn-logout"><?php echo __('logout'); ?></button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="platforms">
|
||||||
|
<!-- Tarjeta Discord -->
|
||||||
|
<?php if (hasPermission('view_dashboard', 'discord')): ?>
|
||||||
|
<a href="/discord/dashboard_discord.php" class="platform-card discord-card">
|
||||||
|
<div class="platform-icon">💬</div>
|
||||||
|
<h2 class="platform-title"><?php echo __('discord'); ?></h2>
|
||||||
|
<p class="platform-description">
|
||||||
|
<?php echo __('discord_description'); ?>
|
||||||
|
</p>
|
||||||
|
<div class="platform-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="discord-users">-</div>
|
||||||
|
<div class="stat-label"><?php echo __('users'); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="discord-messages">-</div>
|
||||||
|
<div class="stat-label"><?php echo __('messages'); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="discord-templates">-</div>
|
||||||
|
<div class="stat-label"><?php echo __('templates'); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Tarjeta Telegram -->
|
||||||
|
<?php if (hasPermission('view_dashboard', 'telegram')): ?>
|
||||||
|
<a href="/telegram/dashboard_telegram.php" class="platform-card telegram-card">
|
||||||
|
<div class="platform-icon">✈️</div>
|
||||||
|
<h2 class="platform-title"><?php echo __('telegram'); ?></h2>
|
||||||
|
<p class="platform-description">
|
||||||
|
<?php echo __('telegram_description'); ?>
|
||||||
|
</p>
|
||||||
|
<div class="platform-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="telegram-users">-</div>
|
||||||
|
<div class="stat-label"><?php echo __('users'); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="telegram-messages">-</div>
|
||||||
|
<div class="stat-label"><?php echo __('messages'); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="telegram-templates">-</div>
|
||||||
|
<div class="stat-label"><?php echo __('templates'); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Cargar estadísticas
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
// Solo cargar si hay al menos una tarjeta visible (Discord o Telegram)
|
||||||
|
if (document.querySelector('.platform-card')) {
|
||||||
|
const response = await fetch('/shared/api/stats.php');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Discord
|
||||||
|
const discordUsers = document.getElementById('discord-users');
|
||||||
|
if (discordUsers) discordUsers.textContent = data.discord.users || 0;
|
||||||
|
const discordMessages = document.getElementById('discord-messages');
|
||||||
|
if (discordMessages) discordMessages.textContent = data.discord.messages || 0;
|
||||||
|
const discordTemplates = document.getElementById('discord-templates');
|
||||||
|
if (discordTemplates) discordTemplates.textContent = data.discord.templates || 0;
|
||||||
|
|
||||||
|
// Telegram
|
||||||
|
const telegramUsers = document.getElementById('telegram-users');
|
||||||
|
if (telegramUsers) telegramUsers.textContent = data.telegram.users || 0;
|
||||||
|
const telegramMessages = document.getElementById('telegram-messages');
|
||||||
|
if (telegramMessages) telegramMessages.textContent = data.telegram.messages || 0;
|
||||||
|
const telegramTemplates = document.getElementById('telegram-templates');
|
||||||
|
if (telegramTemplates) telegramTemplates.textContent = data.telegram.templates || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cargando estadísticas:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadStats();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
247
login.php
Executable file
247
login.php
Executable file
@@ -0,0 +1,247 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// 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__ . '/shared/database/connection.php';
|
||||||
|
require_once __DIR__ . '/shared/auth/jwt.php';
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
// Si ya está autenticado, redirigir al panel
|
||||||
|
$userData = JWTAuth::authenticate();
|
||||||
|
if ($userData) {
|
||||||
|
header('Location: /index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$username = $_POST['username'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($username) || empty($password)) {
|
||||||
|
$error = 'Por favor, complete todos los campos';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Buscar usuario
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT u.*, r.nombre as rol_nombre, i.codigo as idioma_codigo
|
||||||
|
FROM usuarios u
|
||||||
|
LEFT JOIN roles r ON u.rol_id = r.id
|
||||||
|
LEFT JOIN idiomas i ON u.idioma_id = i.id
|
||||||
|
WHERE u.username = ? AND u.activo = 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password'])) {
|
||||||
|
// Cargar permisos del usuario
|
||||||
|
$permisos = JWTAuth::loadUserPermissions($user['id']);
|
||||||
|
|
||||||
|
// Generar token JWT
|
||||||
|
$token = JWTAuth::generateToken(
|
||||||
|
$user['id'],
|
||||||
|
$user['username'],
|
||||||
|
$user['rol_nombre'] ?? 'Editor',
|
||||||
|
$user['idioma_codigo'] ?? 'es',
|
||||||
|
$permisos
|
||||||
|
);
|
||||||
|
|
||||||
|
// Guardar token en cookie
|
||||||
|
setcookie('auth_token', $token, [
|
||||||
|
'expires' => time() + 3600,
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => false, // Cambiar a true en producción con HTTPS
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Actualizar último acceso
|
||||||
|
$stmt = $db->prepare("UPDATE usuarios SET ultimo_acceso = NOW() WHERE id = ?");
|
||||||
|
$stmt->execute([$user['id']]);
|
||||||
|
|
||||||
|
// Redireccionar
|
||||||
|
$redirect = $_GET['redirect'] ?? '/index.php';
|
||||||
|
// Validar que la redirección sea interna para evitar open redirect
|
||||||
|
if (!preg_match('/^\/[a-zA-Z0-9_\-\/]+\.php(\?.*)?$/', $redirect)) {
|
||||||
|
$redirect = '/index.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: ' . $redirect);
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$error = 'Usuario o contraseña incorrectos';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$error = 'Error del sistema. Intente nuevamente.';
|
||||||
|
error_log('Error de login: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - Sistema de Bots</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: #fee;
|
||||||
|
color: #c33;
|
||||||
|
border-left: 4px solid #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #efe;
|
||||||
|
color: #3c3;
|
||||||
|
border-left: 4px solid #3c3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>🤖 Sistema de Bots</h1>
|
||||||
|
<p>Discord & Telegram Manager</p>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<a href="/admin/users/list.php" style="font-size: 12px; color: #667eea; text-decoration: none; border: 1px solid #667eea; padding: 4px 8px; border-radius: 12px;">
|
||||||
|
<i class="fas fa-users-cog"></i> Gestión de Usuarios
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Usuario</label>
|
||||||
|
<input type="text" id="username" name="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Contraseña</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-login">Iniciar Sesión</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
logs/discord/error.log
Executable file
20
logs/discord/error.log
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
[2025-12-04 22:43:22] [ERROR] Inicio de eliminación de plantilla
|
||||||
|
[2025-12-04 16:43:22] [ERROR] Usuario autenticado: {"id":1,"username":"nickpons666"}
|
||||||
|
[2025-12-04 16:43:22] [ERROR] Datos recibidos
|
||||||
|
{
|
||||||
|
"input": "{\"id\":3}",
|
||||||
|
"data": {
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
"template_id": 3
|
||||||
|
}
|
||||||
|
[2025-12-04 16:43:22] [ERROR] Conexión a BD exitosa
|
||||||
|
[2025-12-04 16:43:22] [ERROR] Transacción iniciada
|
||||||
|
[2025-12-04 16:43:22] [ERROR] Comandos eliminados
|
||||||
|
{
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
[2025-12-04 16:43:22] [ERROR] Plantilla eliminada exitosamente
|
||||||
|
{
|
||||||
|
"id": 3
|
||||||
|
}
|
||||||
2
logs/discord/messages.log
Executable file
2
logs/discord/messages.log
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
[2025-12-04 16:46:13] [INFO] Mensaje enviado a 1386836985280336022 por nickpons666
|
||||||
|
[2025-12-04 16:51:05] [INFO] Mensaje enviado a 1386836985280336022 por nickpons666
|
||||||
4
logs/discord/templates.log
Executable file
4
logs/discord/templates.log
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
[2025-12-04 16:44:36] [INFO] Iniciando edición de plantilla (ID: 4). Comando recibido: Asedio
|
||||||
|
[2025-12-04 16:44:36] [INFO] Comando existente para plantilla 4: {"id":1,"comando":"Asedio"}
|
||||||
|
[2025-12-04 16:44:36] [INFO] Comando existente no modificado para plantilla 4. Valor: Asedio
|
||||||
|
[2025-12-04 16:44:36] [INFO] Edición de plantilla completada (ID: 4).
|
||||||
0
logs/discord_bot_err.log
Executable file
0
logs/discord_bot_err.log
Executable file
257
logs/discord_bot_new.log
Executable file
257
logs/discord_bot_new.log
Executable file
@@ -0,0 +1,257 @@
|
|||||||
|
[2025-12-10 20:33:36] [PID: 2001] Iniciando bot de Discord...
|
||||||
|
[2025-12-10 20:33:36] [PID: 2001] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-10 20:33:50] [PID: 2001] ==================================================
|
||||||
|
[2025-12-10 20:33:50] [PID: 2001] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-10 20:33:50] [PID: 2001] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-10 20:33:50] [PID: 2001] ==================================================
|
||||||
|
[2025-12-11 14:50:13] [PID: 1936] Iniciando bot de Discord...
|
||||||
|
[2025-12-11 14:50:13] [PID: 1936] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-11 14:50:18] [PID: 1936] ==================================================
|
||||||
|
[2025-12-11 14:50:18] [PID: 1936] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-11 14:50:18] [PID: 1936] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-11 14:50:18] [PID: 1936] ==================================================
|
||||||
|
[2025-12-15 19:31:32] [PID: 1917] Iniciando bot de Discord...
|
||||||
|
[2025-12-15 19:31:33] [PID: 1917] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-15 19:31:39] [PID: 1917] ==================================================
|
||||||
|
[2025-12-15 19:31:39] [PID: 1917] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-15 19:31:39] [PID: 1917] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-15 19:31:39] [PID: 1917] ==================================================
|
||||||
|
[2025-12-16 06:56:48] [PID: 1860] Iniciando bot de Discord...
|
||||||
|
[2025-12-16 06:56:48] [PID: 1860] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-16 06:56:49] [PID: 1920] Iniciando bot de Discord...
|
||||||
|
[2025-12-16 06:56:49] [PID: 1920] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-16 06:56:51] [PID: 1991] Iniciando bot de Discord...
|
||||||
|
[2025-12-16 06:56:51] [PID: 1991] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-16 06:56:52] [PID: 1991] ==================================================
|
||||||
|
[2025-12-16 06:56:52] [PID: 1991] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-16 06:56:52] [PID: 1991] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-16 06:56:52] [PID: 1991] ==================================================
|
||||||
|
[2025-12-16 19:48:22] [PID: 1839] Iniciando bot de Discord...
|
||||||
|
[2025-12-16 19:48:22] [PID: 1839] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-16 19:48:23] [PID: 1926] Iniciando bot de Discord...
|
||||||
|
[2025-12-16 19:48:23] [PID: 1926] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-16 19:48:25] [PID: 1966] Iniciando bot de Discord...
|
||||||
|
[2025-12-16 19:48:25] [PID: 1966] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-16 19:48:29] [PID: 2081] Iniciando bot de Discord...
|
||||||
|
[2025-12-16 19:48:29] [PID: 2081] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-16 19:48:30] [PID: 2081] ==================================================
|
||||||
|
[2025-12-16 19:48:30] [PID: 2081] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-16 19:48:30] [PID: 2081] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-16 19:48:30] [PID: 2081] ==================================================
|
||||||
|
[2025-12-17 16:17:20] [PID: 1969] Iniciando bot de Discord...
|
||||||
|
[2025-12-17 16:17:20] [PID: 1969] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-17 16:17:20] [PID: 1980] Iniciando bot de Discord...
|
||||||
|
[2025-12-17 16:17:20] [PID: 1980] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-17 16:17:21] [PID: 2014] Iniciando bot de Discord...
|
||||||
|
[2025-12-17 16:17:21] [PID: 2014] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-17 16:17:23] [PID: 2056] Iniciando bot de Discord...
|
||||||
|
[2025-12-17 16:17:23] [PID: 2056] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-17 16:17:29] [PID: 2056] ==================================================
|
||||||
|
[2025-12-17 16:17:29] [PID: 2056] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-17 16:17:29] [PID: 2056] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-17 16:17:29] [PID: 2056] ==================================================
|
||||||
|
[2025-12-18 17:11:53] [PID: 1848] Iniciando bot de Discord...
|
||||||
|
[2025-12-18 17:11:53] [PID: 1848] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-18 17:11:55] [PID: 1947] Iniciando bot de Discord...
|
||||||
|
[2025-12-18 17:11:55] [PID: 1947] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-18 17:11:57] [PID: 1974] Iniciando bot de Discord...
|
||||||
|
[2025-12-18 17:11:57] [PID: 1974] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-18 17:12:00] [PID: 2073] Iniciando bot de Discord...
|
||||||
|
[2025-12-18 17:12:00] [PID: 2073] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-19 00:59:26] [PID: 1885] Iniciando bot de Discord...
|
||||||
|
[2025-12-19 00:59:26] [PID: 1885] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-19 00:59:30] [PID: 1885] ==================================================
|
||||||
|
[2025-12-19 00:59:30] [PID: 1885] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-19 00:59:30] [PID: 1885] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-19 00:59:30] [PID: 1885] ==================================================
|
||||||
|
[2025-12-19 17:40:04] [PID: 1880] Iniciando bot de Discord...
|
||||||
|
[2025-12-19 17:40:04] [PID: 1880] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-19 17:40:06] [PID: 1880] ==================================================
|
||||||
|
[2025-12-19 17:40:06] [PID: 1880] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-19 17:40:06] [PID: 1880] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-19 17:40:06] [PID: 1880] ==================================================
|
||||||
|
[2025-12-20 03:51:04] [PID: 1855] Iniciando bot de Discord...
|
||||||
|
[2025-12-20 03:51:04] [PID: 1855] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-20 03:51:05] [PID: 1855] ==================================================
|
||||||
|
[2025-12-20 03:51:05] [PID: 1855] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-20 03:51:05] [PID: 1855] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-20 03:51:05] [PID: 1855] ==================================================
|
||||||
|
[2025-12-20 22:11:21] [PID: 1871] Iniciando bot de Discord...
|
||||||
|
[2025-12-20 22:11:21] [PID: 1871] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-20 22:11:30] [PID: 1871] ==================================================
|
||||||
|
[2025-12-20 22:11:30] [PID: 1871] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-20 22:11:30] [PID: 1871] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-20 22:11:30] [PID: 1871] ==================================================
|
||||||
|
[2025-12-21 17:56:57] [PID: 2042] Iniciando bot de Discord...
|
||||||
|
[2025-12-21 17:56:57] [PID: 2042] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-21 17:57:17] [PID: 2042] ==================================================
|
||||||
|
[2025-12-21 17:57:17] [PID: 2042] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-21 17:57:17] [PID: 2042] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-21 17:57:17] [PID: 2042] ==================================================
|
||||||
|
[2025-12-23 20:36:17] [PID: 1868] Iniciando bot de Discord...
|
||||||
|
[2025-12-23 20:36:17] [PID: 1868] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-23 20:36:29] [PID: 1868] ==================================================
|
||||||
|
[2025-12-23 20:36:29] [PID: 1868] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-23 20:36:29] [PID: 1868] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-23 20:36:29] [PID: 1868] ==================================================
|
||||||
|
[2025-12-24 03:52:24] [PID: 1875] Iniciando bot de Discord...
|
||||||
|
[2025-12-24 03:52:24] [PID: 1875] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-24 03:52:24] [PID: 1924] Iniciando bot de Discord...
|
||||||
|
[2025-12-24 03:52:24] [PID: 1924] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-24 03:52:32] [PID: 1924] ==================================================
|
||||||
|
[2025-12-24 03:52:32] [PID: 1924] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-24 03:52:32] [PID: 1924] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-24 03:52:32] [PID: 1924] ==================================================
|
||||||
|
[2025-12-24 18:13:27] [PID: 1870] Iniciando bot de Discord...
|
||||||
|
[2025-12-24 18:13:27] [PID: 1870] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-24 18:13:28] [PID: 1906] Iniciando bot de Discord...
|
||||||
|
[2025-12-24 18:13:28] [PID: 1906] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-24 18:13:31] [PID: 1917] Iniciando bot de Discord...
|
||||||
|
[2025-12-24 18:13:31] [PID: 1917] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-24 18:13:34] [PID: 2012] Iniciando bot de Discord...
|
||||||
|
[2025-12-24 18:13:34] [PID: 2012] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-24 18:13:36] [PID: 2012] ==================================================
|
||||||
|
[2025-12-24 18:13:36] [PID: 2012] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-24 18:13:36] [PID: 2012] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-24 18:13:36] [PID: 2012] ==================================================
|
||||||
|
[2025-12-24 21:46:14] [PID: 1862] Iniciando bot de Discord...
|
||||||
|
[2025-12-24 21:46:14] [PID: 1862] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-24 21:46:27] [PID: 1862] ==================================================
|
||||||
|
[2025-12-24 21:46:27] [PID: 1862] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-24 21:46:27] [PID: 1862] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-24 21:46:27] [PID: 1862] ==================================================
|
||||||
|
[2025-12-25 21:18:38] [PID: 1876] Iniciando bot de Discord...
|
||||||
|
[2025-12-25 21:18:38] [PID: 1876] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-25 21:18:53] [PID: 1876] ==================================================
|
||||||
|
[2025-12-25 21:18:53] [PID: 1876] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-25 21:18:53] [PID: 1876] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-25 21:18:53] [PID: 1876] ==================================================
|
||||||
|
[2025-12-26 02:54:52] [PID: 1836] Iniciando bot de Discord...
|
||||||
|
[2025-12-26 02:54:52] [PID: 1836] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-26 02:54:52] [PID: 1885] Iniciando bot de Discord...
|
||||||
|
[2025-12-26 02:54:52] [PID: 1885] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-26 02:54:53] [PID: 1895] Iniciando bot de Discord...
|
||||||
|
[2025-12-26 02:54:53] [PID: 1895] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-26 02:54:55] [PID: 1945] Iniciando bot de Discord...
|
||||||
|
[2025-12-26 02:54:55] [PID: 1945] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-26 02:54:58] [PID: 1945] ==================================================
|
||||||
|
[2025-12-26 02:54:58] [PID: 1945] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-26 02:54:58] [PID: 1945] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-26 02:54:58] [PID: 1945] ==================================================
|
||||||
|
[2025-12-26 07:54:13] [PID: 1978] Iniciando bot de Discord...
|
||||||
|
[2025-12-26 07:54:13] [PID: 1978] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-26 07:54:20] [PID: 1978] ==================================================
|
||||||
|
[2025-12-26 07:54:20] [PID: 1978] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-26 07:54:20] [PID: 1978] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-26 07:54:20] [PID: 1978] ==================================================
|
||||||
|
[2025-12-26 18:30:43] [PID: 1852] Iniciando bot de Discord...
|
||||||
|
[2025-12-26 18:30:43] [PID: 1852] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-26 18:30:44] [PID: 1931] Iniciando bot de Discord...
|
||||||
|
[2025-12-26 18:30:44] [PID: 1931] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-26 18:30:46] [PID: 1968] Iniciando bot de Discord...
|
||||||
|
[2025-12-26 18:30:46] [PID: 1968] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-26 18:30:50] [PID: 2129] Iniciando bot de Discord...
|
||||||
|
[2025-12-26 18:30:50] [PID: 2129] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-26 18:30:51] [PID: 2129] ==================================================
|
||||||
|
[2025-12-26 18:30:51] [PID: 2129] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-26 18:30:51] [PID: 2129] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-26 18:30:51] [PID: 2129] ==================================================
|
||||||
|
[2025-12-27 19:20:39] [PID: 1874] Iniciando bot de Discord...
|
||||||
|
[2025-12-27 19:20:39] [PID: 1874] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-27 19:20:45] [PID: 1874] ==================================================
|
||||||
|
[2025-12-27 19:20:45] [PID: 1874] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-27 19:20:45] [PID: 1874] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-27 19:20:45] [PID: 1874] ==================================================
|
||||||
|
[2025-12-28 06:46:16] [PID: 1837] Iniciando bot de Discord...
|
||||||
|
[2025-12-28 06:46:16] [PID: 1837] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-28 06:46:17] [PID: 1909] Iniciando bot de Discord...
|
||||||
|
[2025-12-28 06:46:17] [PID: 1909] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-28 06:46:19] [PID: 1945] Iniciando bot de Discord...
|
||||||
|
[2025-12-28 06:46:19] [PID: 1945] Error fatal al conectar a la base de datos
|
||||||
|
Datos: {
|
||||||
|
"error": "SQLSTATE[HY000] [2002] Network is unreachable"
|
||||||
|
}
|
||||||
|
[2025-12-28 06:46:23] [PID: 2070] Iniciando bot de Discord...
|
||||||
|
[2025-12-28 06:46:23] [PID: 2070] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-28 06:46:24] [PID: 2070] ==================================================
|
||||||
|
[2025-12-28 06:46:24] [PID: 2070] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-28 06:46:24] [PID: 2070] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-28 06:46:24] [PID: 2070] ==================================================
|
||||||
|
[2025-12-28 17:21:42] [PID: 1847] Iniciando bot de Discord...
|
||||||
|
[2025-12-28 17:21:42] [PID: 1847] Conexión a la base de datos establecida.
|
||||||
|
[2025-12-28 17:21:47] [PID: 1847] ==================================================
|
||||||
|
[2025-12-28 17:21:47] [PID: 1847] Bot conectado y listo para escuchar!
|
||||||
|
[2025-12-28 17:21:47] [PID: 1847] Usuario: LastWarbot#7345
|
||||||
|
[2025-12-28 17:21:47] [PID: 1847] ==================================================
|
||||||
47245
logs/discord_bot_out.log
Executable file
47245
logs/discord_bot_out.log
Executable file
File diff suppressed because it is too large
Load Diff
98
logs/discordbot.err.log
Executable file
98
logs/discordbot.err.log
Executable file
@@ -0,0 +1,98 @@
|
|||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
|
Could not open input file: /var/www/html/bot/discord/bot/discord_bot.php
|
||||||
0
logs/discordbot.out.log
Executable file
0
logs/discordbot.out.log
Executable file
25989
logs/ponsprueba__access.log
Executable file
25989
logs/ponsprueba__access.log
Executable file
File diff suppressed because it is too large
Load Diff
243
logs/ponsprueba__error.log
Executable file
243
logs/ponsprueba__error.log
Executable file
@@ -0,0 +1,243 @@
|
|||||||
|
[Wed Dec 10 17:29:55.972640 2025] [autoindex:error] [pid 1888] [client 10.10.4.17:57224] AH01276: Cannot serve directory /var/www/html/bot/telegram/: No matching DirectoryIndex (index.html,index.cgi,index.pl,index.php,index.xhtml,index.htm) found, and server-generated directory index forbidden by Options directive, referer: http://ponsprueba.ddns.net/telegram/
|
||||||
|
[Wed Dec 10 17:31:40.134840 2025] [autoindex:error] [pid 1888] [client 10.10.4.17:35502] AH01276: Cannot serve directory /var/www/html/bot/telegram/: No matching DirectoryIndex (index.html,index.cgi,index.pl,index.php,index.xhtml,index.htm) found, and server-generated directory index forbidden by Options directive, referer: http://ponsprueba.ddns.net/telegram/
|
||||||
|
[Tue Dec 16 03:34:32.461284 2025] [php:error] [pid 1718] [client 10.10.4.17:38148] script '/var/www/html/bot/xmlrpc.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:43.448018 2025] [php:error] [pid 1772] [client 10.10.4.17:52632] script '/var/www/html/bot/nc4.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:43.884014 2025] [php:error] [pid 1773] [client 10.10.4.17:52638] script '/var/www/html/bot/d4.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:44.366931 2025] [php:error] [pid 1765] [client 10.10.4.17:52650] script '/var/www/html/bot/ad.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:44.813689 2025] [php:error] [pid 1768] [client 10.10.4.17:52656] script '/var/www/html/bot/dlex.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:45.250956 2025] [php:error] [pid 1770] [client 10.10.4.17:52668] script '/var/www/html/bot/classwithtostring.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:45.881923 2025] [php:error] [pid 1772] [client 10.10.4.17:52678] script '/var/www/html/bot/pass.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:46.316263 2025] [php:error] [pid 1773] [client 10.10.4.17:52684] script '/var/www/html/bot/good.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:46.752641 2025] [php:error] [pid 1765] [client 10.10.4.17:52686] script '/var/www/html/bot/ext.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:47.188453 2025] [php:error] [pid 1768] [client 10.10.4.17:52694] script '/var/www/html/bot/class20.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:48.961856 2025] [php:error] [pid 1772] [client 10.10.4.17:52720] script '/var/www/html/bot/aa.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:49.388639 2025] [php:error] [pid 1773] [client 10.10.4.17:52730] script '/var/www/html/bot/npi.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:49.871244 2025] [php:error] [pid 1765] [client 10.10.4.17:52742] script '/var/www/html/bot/ahax.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:50.305133 2025] [php:error] [pid 1768] [client 10.10.4.17:52746] script '/var/www/html/bot/pop.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:50.734658 2025] [php:error] [pid 1770] [client 10.10.4.17:52756] script '/var/www/html/bot/file17.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:51.616295 2025] [php:error] [pid 1773] [client 10.10.4.17:52760] script '/var/www/html/bot/about.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:52.069045 2025] [php:error] [pid 1765] [client 10.10.4.17:52762] script '/var/www/html/bot/litanies.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:52.760064 2025] [php:error] [pid 1768] [client 10.10.4.17:42408] script '/var/www/html/bot/g.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:53.411214 2025] [php:error] [pid 1770] [client 10.10.4.17:42414] script '/var/www/html/bot/readme.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:53.853324 2025] [php:error] [pid 1772] [client 10.10.4.17:42416] script '/var/www/html/bot/kwm4.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:54.558231 2025] [php:error] [pid 1773] [client 10.10.4.17:42430] script '/var/www/html/bot/just2.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:55.005543 2025] [php:error] [pid 1765] [client 10.10.4.17:42438] script '/var/www/html/bot/png.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:55.875559 2025] [php:error] [pid 1768] [client 10.10.4.17:42448] script '/var/www/html/bot/geger.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:56.329706 2025] [php:error] [pid 1770] [client 10.10.4.17:42458] script '/var/www/html/bot/let.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:56.758089 2025] [php:error] [pid 1772] [client 10.10.4.17:42470] script '/var/www/html/bot/np.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:57.183927 2025] [php:error] [pid 1773] [client 10.10.4.17:42484] script '/var/www/html/bot/ask.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:57.735597 2025] [php:error] [pid 1765] [client 10.10.4.17:42496] script '/var/www/html/bot/CLA.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:58.633280 2025] [php:error] [pid 1772] [client 10.10.4.17:42538] script '/var/www/html/bot/mek.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:59.085539 2025] [php:error] [pid 1765] [client 10.10.4.17:42554] script '/var/www/html/bot/fjpeb.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:47:59.578172 2025] [php:error] [pid 1768] [client 10.10.4.17:42560] script '/var/www/html/bot/ex.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:00.212549 2025] [php:error] [pid 1770] [client 10.10.4.17:42566] script '/var/www/html/bot/asd67.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:00.655952 2025] [php:error] [pid 1772] [client 10.10.4.17:42578] script '/var/www/html/bot/zwso.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:01.103126 2025] [php:error] [pid 1773] [client 10.10.4.17:42588] script '/var/www/html/bot/alfa.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:01.531581 2025] [php:error] [pid 1765] [client 10.10.4.17:42592] script '/var/www/html/bot/shlo.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:02.068802 2025] [php:error] [pid 1768] [client 10.10.4.17:42600] script '/var/www/html/bot/sec.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:02.597842 2025] [php:error] [pid 1770] [client 10.10.4.17:57124] script '/var/www/html/bot/natural.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:03.034750 2025] [php:error] [pid 1772] [client 10.10.4.17:57132] script '/var/www/html/bot/1.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:03.832338 2025] [php:error] [pid 1773] [client 10.10.4.17:57144] script '/var/www/html/bot/z.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:04.302546 2025] [php:error] [pid 1765] [client 10.10.4.17:57148] script '/var/www/html/bot/law.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:04.730791 2025] [php:error] [pid 1768] [client 10.10.4.17:57164] script '/var/www/html/bot/bluejackets.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:05.159708 2025] [php:error] [pid 1770] [client 10.10.4.17:57174] script '/var/www/html/bot/php.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:05.582354 2025] [php:error] [pid 1772] [client 10.10.4.17:57182] script '/var/www/html/bot/sx21_1.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:06.036937 2025] [php:error] [pid 1773] [client 10.10.4.17:57186] script '/var/www/html/bot/1aa.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:06.488609 2025] [php:error] [pid 1765] [client 10.10.4.17:57200] script '/var/www/html/bot/nx9.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:07.342592 2025] [php:error] [pid 1768] [client 10.10.4.17:57210] script '/var/www/html/bot/file.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:07.797472 2025] [php:error] [pid 1770] [client 10.10.4.17:57222] script '/var/www/html/bot/aw.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:08.338586 2025] [php:error] [pid 1772] [client 10.10.4.17:57232] script '/var/www/html/bot/sfvul.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:08.797224 2025] [php:error] [pid 1773] [client 10.10.4.17:57246] script '/var/www/html/bot/icdwb.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:09.233970 2025] [php:error] [pid 1765] [client 10.10.4.17:57252] script '/var/www/html/bot/ticket.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:09.806401 2025] [php:error] [pid 1768] [client 10.10.4.17:57256] script '/var/www/html/bot/elp.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:10.257324 2025] [php:error] [pid 1770] [client 10.10.4.17:57272] script '/var/www/html/bot/k.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:10.703607 2025] [php:error] [pid 1772] [client 10.10.4.17:57288] script '/var/www/html/bot/amphicyon.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:11.580813 2025] [php:error] [pid 1773] [client 10.10.4.17:57302] script '/var/www/html/bot/wsad.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:12.477526 2025] [php:error] [pid 1765] [client 10.10.4.17:37702] script '/var/www/html/bot/lock1.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:12.948229 2025] [php:error] [pid 1768] [client 10.10.4.17:37716] script '/var/www/html/bot/xp.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:13.586759 2025] [php:error] [pid 1770] [client 10.10.4.17:37732] script '/var/www/html/bot/e.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:14.125359 2025] [php:error] [pid 1772] [client 10.10.4.17:37748] script '/var/www/html/bot/v3.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:14.915413 2025] [php:error] [pid 1773] [client 10.10.4.17:37754] script '/var/www/html/bot/akcc.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:15.356910 2025] [php:error] [pid 1765] [client 10.10.4.17:37756] script '/var/www/html/bot/minik.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:15.783935 2025] [php:error] [pid 1768] [client 10.10.4.17:37762] script '/var/www/html/bot/asasx.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:16.223261 2025] [php:error] [pid 1770] [client 10.10.4.17:37770] script '/var/www/html/bot/nx.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:16.670490 2025] [php:error] [pid 1772] [client 10.10.4.17:37780] script '/var/www/html/bot/themes.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:17.098243 2025] [php:error] [pid 1773] [client 10.10.4.17:37788] script '/var/www/html/bot/acp.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:17.793378 2025] [php:error] [pid 1765] [client 10.10.4.17:37804] script '/var/www/html/bot/xpw.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:18.246046 2025] [php:error] [pid 1768] [client 10.10.4.17:37818] script '/var/www/html/bot/lufix.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:19.398831 2025] [php:error] [pid 1770] [client 10.10.4.17:37826] script '/var/www/html/bot/akp.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:19.991502 2025] [php:error] [pid 1772] [client 10.10.4.17:37842] script '/var/www/html/bot/cwsd.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:20.436711 2025] [php:error] [pid 1773] [client 10.10.4.17:37852] script '/var/www/html/bot/tll.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:21.384967 2025] [php:error] [pid 1765] [client 10.10.4.17:37854] script '/var/www/html/bot/Okxob.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:21.981109 2025] [php:error] [pid 1768] [client 10.10.4.17:37868] script '/var/www/html/bot/idea.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:22.422522 2025] [php:error] [pid 1770] [client 10.10.4.17:54782] script '/var/www/html/bot/pepe.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:22.855502 2025] [php:error] [pid 1772] [client 10.10.4.17:54798] script '/var/www/html/bot/v2.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:23.336363 2025] [php:error] [pid 1773] [client 10.10.4.17:54800] script '/var/www/html/bot/yca.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:23.799410 2025] [php:error] [pid 1765] [client 10.10.4.17:54814] script '/var/www/html/bot/lock360.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:24.667061 2025] [php:error] [pid 1768] [client 10.10.4.17:54828] script '/var/www/html/bot/ot.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:25.091640 2025] [php:error] [pid 1770] [client 10.10.4.17:54832] script '/var/www/html/bot/bolt.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:25.563049 2025] [php:error] [pid 1772] [client 10.10.4.17:54836] script '/var/www/html/bot/j.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:26.079516 2025] [php:error] [pid 1773] [client 10.10.4.17:54844] script '/var/www/html/bot/s.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:26.510045 2025] [php:error] [pid 1765] [client 10.10.4.17:54848] script '/var/www/html/bot/ucp.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:26.941526 2025] [php:error] [pid 1768] [client 10.10.4.17:54856] script '/var/www/html/bot/zse.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:27.408068 2025] [php:error] [pid 1770] [client 10.10.4.17:54864] script '/var/www/html/bot/0x.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:27.840077 2025] [php:error] [pid 1772] [client 10.10.4.17:54868] script '/var/www/html/bot/403.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:28.435988 2025] [php:error] [pid 1773] [client 10.10.4.17:54870] script '/var/www/html/bot/gfile.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:28.863585 2025] [php:error] [pid 1765] [client 10.10.4.17:54880] script '/var/www/html/bot/doc.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:29.624246 2025] [php:error] [pid 1768] [client 10.10.4.17:54886] script '/var/www/html/bot/orm.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:30.507848 2025] [php:error] [pid 1770] [client 10.10.4.17:54888] script '/var/www/html/bot/ay.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:30.960053 2025] [php:error] [pid 1772] [client 10.10.4.17:54902] script '/var/www/html/bot/buy.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:31.416169 2025] [php:error] [pid 1773] [client 10.10.4.17:54916] script '/var/www/html/bot/test.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:31.969378 2025] [php:error] [pid 1765] [client 10.10.4.17:54920] script '/var/www/html/bot/wsa.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:32.414340 2025] [php:error] [pid 1768] [client 10.10.4.17:52714] script '/var/www/html/bot/wolv.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:32.949088 2025] [php:error] [pid 1770] [client 10.10.4.17:52724] script '/var/www/html/bot/ea3f.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:33.600169 2025] [php:error] [pid 1772] [client 10.10.4.17:52740] script '/var/www/html/bot/price.php' not found or unable to stat
|
||||||
|
[Tue Dec 23 15:48:34.028002 2025] [php:error] [pid 1773] [client 10.10.4.17:52764] script '/var/www/html/bot/gmo.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:18.026109 2025] [php:error] [pid 1786] [client 10.10.4.17:56648] script '/var/www/html/bot/bless14.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:18.294361 2025] [php:error] [pid 1789] [client 10.10.4.17:56664] script '/var/www/html/bot/mini.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:18.543934 2025] [php:error] [pid 1797] [client 10.10.4.17:56668] script '/var/www/html/bot/s1.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:18.788945 2025] [php:error] [pid 8934] [client 10.10.4.17:56680] script '/var/www/html/bot/ma.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:19.044961 2025] [php:error] [pid 1795] [client 10.10.4.17:56684] script '/var/www/html/bot/lanz.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:19.296487 2025] [php:error] [pid 17237] [client 10.10.4.17:56700] script '/var/www/html/bot/xeeex.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:19.545755 2025] [php:error] [pid 1788] [client 10.10.4.17:56714] script '/var/www/html/bot/tons.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:19.795717 2025] [php:error] [pid 1786] [client 10.10.4.17:56730] script '/var/www/html/bot/wpup.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:20.041014 2025] [php:error] [pid 1789] [client 10.10.4.17:56738] script '/var/www/html/bot/xa.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:20.285981 2025] [php:error] [pid 1797] [client 10.10.4.17:56740] script '/var/www/html/bot/asf.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:20.633554 2025] [php:error] [pid 8934] [client 10.10.4.17:56746] script '/var/www/html/bot/f8.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:20.882522 2025] [php:error] [pid 1795] [client 10.10.4.17:56756] script '/var/www/html/bot/bras.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:21.142186 2025] [php:error] [pid 17237] [client 10.10.4.17:56772] script '/var/www/html/bot/an8.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:21.421353 2025] [php:error] [pid 1788] [client 10.10.4.17:56778] script '/var/www/html/bot/esp.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:21.668010 2025] [php:error] [pid 1786] [client 10.10.4.17:56792] script '/var/www/html/bot/21x.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:21.919537 2025] [php:error] [pid 1789] [client 10.10.4.17:56802] script '/var/www/html/bot/out.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:22.183568 2025] [php:error] [pid 1797] [client 10.10.4.17:56812] script '/var/www/html/bot/flower2.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:22.448782 2025] [php:error] [pid 8934] [client 10.10.4.17:56818] script '/var/www/html/bot/family.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:22.712643 2025] [php:error] [pid 1795] [client 10.10.4.17:56826] script '/var/www/html/bot/ws35.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:22.963148 2025] [php:error] [pid 17237] [client 10.10.4.17:56832] script '/var/www/html/bot/nox.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:23.217628 2025] [php:error] [pid 1788] [client 10.10.4.17:56836] script '/var/www/html/bot/ws13.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:23.463551 2025] [php:error] [pid 1786] [client 10.10.4.17:56840] script '/var/www/html/bot/ss1.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:23.709755 2025] [php:error] [pid 1789] [client 10.10.4.17:56854] script '/var/www/html/bot/ww7.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:23.982273 2025] [php:error] [pid 1797] [client 10.10.4.17:56868] script '/var/www/html/bot/w2025.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:24.232603 2025] [php:error] [pid 8934] [client 10.10.4.17:56872] script '/var/www/html/bot/lisa.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:24.499910 2025] [php:error] [pid 1795] [client 10.10.4.17:56888] script '/var/www/html/bot/000.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:24.749390 2025] [php:error] [pid 17237] [client 10.10.4.17:56900] script '/var/www/html/bot/xxxx.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:25.036795 2025] [php:error] [pid 1788] [client 10.10.4.17:56908] script '/var/www/html/bot/wen.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:25.317287 2025] [php:error] [pid 1786] [client 10.10.4.17:56910] script '/var/www/html/bot/cwclass.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:25.612755 2025] [php:error] [pid 1789] [client 10.10.4.17:56922] script '/var/www/html/bot/bless.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:25.863938 2025] [php:error] [pid 1797] [client 10.10.4.17:56924] script '/var/www/html/bot/rtx.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:26.125982 2025] [php:error] [pid 8934] [client 10.10.4.17:56064] script '/var/www/html/bot/bipas.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:26.382187 2025] [php:error] [pid 1795] [client 10.10.4.17:56072] script '/var/www/html/bot/conte.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:26.632141 2025] [php:error] [pid 17237] [client 10.10.4.17:56078] script '/var/www/html/bot/class3.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:26.881088 2025] [php:error] [pid 1788] [client 10.10.4.17:56086] script '/var/www/html/bot/blurbs18.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:27.196367 2025] [php:error] [pid 1786] [client 10.10.4.17:56092] script '/var/www/html/bot/claa.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:27.445838 2025] [php:error] [pid 1789] [client 10.10.4.17:56094] script '/var/www/html/bot/shoyo.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:27.693566 2025] [php:error] [pid 1797] [client 10.10.4.17:56104] script '/var/www/html/bot/flower.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:27.938608 2025] [php:error] [pid 8934] [client 10.10.4.17:56110] script '/var/www/html/bot/waq.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:28.448571 2025] [php:error] [pid 1795] [client 10.10.4.17:56116] script '/var/www/html/bot/wakak.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:28.700705 2025] [php:error] [pid 17237] [client 10.10.4.17:56132] script '/var/www/html/bot/adminfuns.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:28.967338 2025] [php:error] [pid 1789] [client 10.10.4.17:56160] script '/var/www/html/bot/wp-good.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:29.213791 2025] [php:error] [pid 1797] [client 10.10.4.17:56166] script '/var/www/html/bot/xse25.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:29.498485 2025] [php:error] [pid 8934] [client 10.10.4.17:56176] script '/var/www/html/bot/nox.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:29.746916 2025] [php:error] [pid 1795] [client 10.10.4.17:56180] script '/var/www/html/bot/file48.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:29.990986 2025] [php:error] [pid 17237] [client 10.10.4.17:56186] script '/var/www/html/bot/info.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:30.266690 2025] [php:error] [pid 1788] [client 10.10.4.17:56196] script '/var/www/html/bot/class9.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:30.541106 2025] [php:error] [pid 1786] [client 10.10.4.17:56212] script '/var/www/html/bot/la.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:30.793829 2025] [php:error] [pid 1789] [client 10.10.4.17:56220] script '/var/www/html/bot/bless11.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:31.043017 2025] [php:error] [pid 1797] [client 10.10.4.17:56232] script '/var/www/html/bot/403.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:31.292033 2025] [php:error] [pid 8934] [client 10.10.4.17:56246] script '/var/www/html/bot/ac.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:31.538180 2025] [php:error] [pid 1795] [client 10.10.4.17:56260] script '/var/www/html/bot/az.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:31.810772 2025] [php:error] [pid 17237] [client 10.10.4.17:56276] script '/var/www/html/bot/galex.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:32.072552 2025] [php:error] [pid 1788] [client 10.10.4.17:56290] script '/var/www/html/bot/xb.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:32.351452 2025] [php:error] [pid 1786] [client 10.10.4.17:56304] script '/var/www/html/bot/vx.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:32.596385 2025] [php:error] [pid 1789] [client 10.10.4.17:56312] script '/var/www/html/bot/rh.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:32.841146 2025] [php:error] [pid 1797] [client 10.10.4.17:56318] script '/var/www/html/bot/chosen.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:33.110515 2025] [php:error] [pid 8934] [client 10.10.4.17:56326] script '/var/www/html/bot/class.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:33.360506 2025] [php:error] [pid 1795] [client 10.10.4.17:56332] script '/var/www/html/bot/bless5.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:33.634325 2025] [php:error] [pid 17237] [client 10.10.4.17:56346] script '/var/www/html/bot/lock360.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:33.899674 2025] [php:error] [pid 1788] [client 10.10.4.17:56358] script '/var/www/html/bot/f35.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:34.154379 2025] [php:error] [pid 1786] [client 10.10.4.17:56370] script '/var/www/html/bot/ioxi-o1.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:34.401291 2025] [php:error] [pid 1789] [client 10.10.4.17:56374] script '/var/www/html/bot/ha.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:34.655543 2025] [php:error] [pid 1797] [client 10.10.4.17:56388] script '/var/www/html/bot/gg.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:34.901040 2025] [php:error] [pid 8934] [client 10.10.4.17:56400] script '/var/www/html/bot/ar.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:35.169188 2025] [php:error] [pid 1795] [client 10.10.4.17:56406] script '/var/www/html/bot/x.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:35.440664 2025] [php:error] [pid 17237] [client 10.10.4.17:56422] script '/var/www/html/bot/xx.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:35.685801 2025] [php:error] [pid 1788] [client 10.10.4.17:56438] script '/var/www/html/bot/gifclass4.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:35.936337 2025] [php:error] [pid 1786] [client 10.10.4.17:56452] script '/var/www/html/bot/radio.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:36.183293 2025] [php:error] [pid 1789] [client 10.10.4.17:57238] script '/var/www/html/bot/blurbs15.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:36.460787 2025] [php:error] [pid 1797] [client 10.10.4.17:57244] script '/var/www/html/bot/gifclass.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:36.768263 2025] [php:error] [pid 8934] [client 10.10.4.17:57260] script '/var/www/html/bot/security.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:37.037843 2025] [php:error] [pid 1795] [client 10.10.4.17:57262] script '/var/www/html/bot/ww1.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:37.284078 2025] [php:error] [pid 17237] [client 10.10.4.17:57278] script '/var/www/html/bot/alfa.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:37.538509 2025] [php:error] [pid 1788] [client 10.10.4.17:57286] script '/var/www/html/bot/manager.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:37.784133 2025] [php:error] [pid 1786] [client 10.10.4.17:57296] script '/var/www/html/bot/item.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:38.046975 2025] [php:error] [pid 1789] [client 10.10.4.17:57308] script '/var/www/html/bot/sx.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:38.310418 2025] [php:error] [pid 1797] [client 10.10.4.17:57318] script '/var/www/html/bot/inputs.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:38.647862 2025] [php:error] [pid 8934] [client 10.10.4.17:57330] script '/var/www/html/bot/about.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:38.898729 2025] [php:error] [pid 1795] [client 10.10.4.17:57346] script '/var/www/html/bot/classwithtostring.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:39.181793 2025] [php:error] [pid 17237] [client 10.10.4.17:57350] script '/var/www/html/bot/abcd.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:39.445565 2025] [php:error] [pid 1788] [client 10.10.4.17:57360] script '/var/www/html/bot/admin.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:39.717634 2025] [php:error] [pid 1786] [client 10.10.4.17:57364] script '/var/www/html/bot/w.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:40.017176 2025] [php:error] [pid 1789] [client 10.10.4.17:57376] script '/var/www/html/bot/404.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:40.264605 2025] [php:error] [pid 1797] [client 10.10.4.17:57392] script '/var/www/html/bot/ioxi-o.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:40.513479 2025] [php:error] [pid 8934] [client 10.10.4.17:57398] script '/var/www/html/bot/0x.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:40.769396 2025] [php:error] [pid 1795] [client 10.10.4.17:57406] script '/var/www/html/bot/css.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:41.019368 2025] [php:error] [pid 17237] [client 10.10.4.17:57410] script '/var/www/html/bot/222.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:41.271140 2025] [php:error] [pid 1788] [client 10.10.4.17:57422] script '/var/www/html/bot/click.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:41.549440 2025] [php:error] [pid 1786] [client 10.10.4.17:57434] script '/var/www/html/bot/install.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:41.810178 2025] [php:error] [pid 1789] [client 10.10.4.17:57436] script '/var/www/html/bot/simple.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:42.057835 2025] [php:error] [pid 1797] [client 10.10.4.17:57446] script '/var/www/html/bot/css.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:42.302115 2025] [php:error] [pid 8934] [client 10.10.4.17:57454] script '/var/www/html/bot/cong.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:42.561155 2025] [php:error] [pid 1795] [client 10.10.4.17:57462] script '/var/www/html/bot/class19.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:42.808403 2025] [php:error] [pid 17237] [client 10.10.4.17:57474] script '/var/www/html/bot/class20.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:43.053550 2025] [php:error] [pid 1788] [client 10.10.4.17:57480] script '/var/www/html/bot/admin.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:43.298185 2025] [php:error] [pid 1786] [client 10.10.4.17:57482] script '/var/www/html/bot/randkeyword.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:43.569908 2025] [php:error] [pid 1789] [client 10.10.4.17:57484] script '/var/www/html/bot/fwe.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:43.838090 2025] [php:error] [pid 1797] [client 10.10.4.17:57488] script '/var/www/html/bot/g.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:44.118875 2025] [php:error] [pid 8934] [client 10.10.4.17:57498] script '/var/www/html/bot/tx1.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:44.393159 2025] [php:error] [pid 1795] [client 10.10.4.17:57514] script '/var/www/html/bot/xv.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:44.731773 2025] [php:error] [pid 17237] [client 10.10.4.17:57530] script '/var/www/html/bot/fv.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:44.977169 2025] [php:error] [pid 1788] [client 10.10.4.17:57536] script '/var/www/html/bot/as.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:45.242532 2025] [php:error] [pid 1786] [client 10.10.4.17:57550] script '/var/www/html/bot/wsd.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:45.514958 2025] [php:error] [pid 1789] [client 10.10.4.17:57556] script '/var/www/html/bot/gtc.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:45.760913 2025] [php:error] [pid 1797] [client 10.10.4.17:57564] script '/var/www/html/bot/atx.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:46.016417 2025] [php:error] [pid 8934] [client 10.10.4.17:43418] script '/var/www/html/bot/z60.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:46.266503 2025] [php:error] [pid 1795] [client 10.10.4.17:43428] script '/var/www/html/bot/cheka.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:46.513992 2025] [php:error] [pid 17237] [client 10.10.4.17:43440] script '/var/www/html/bot/cabs.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:46.758408 2025] [php:error] [pid 1788] [client 10.10.4.17:43456] script '/var/www/html/bot/classgoto24.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:47.008541 2025] [php:error] [pid 1786] [client 10.10.4.17:43458] script '/var/www/html/bot/get.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:47.303499 2025] [php:error] [pid 1789] [client 10.10.4.17:43470] script '/var/www/html/bot/xtride.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:47.572339 2025] [php:error] [pid 1797] [client 10.10.4.17:43484] script '/var/www/html/bot/ws29.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:47.823987 2025] [php:error] [pid 8934] [client 10.10.4.17:43496] script '/var/www/html/bot/oo.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:48.068971 2025] [php:error] [pid 1795] [client 10.10.4.17:43504] script '/var/www/html/bot/s11.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:48.322249 2025] [php:error] [pid 17237] [client 10.10.4.17:43512] script '/var/www/html/bot/dir.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:48.570892 2025] [php:error] [pid 1788] [client 10.10.4.17:43526] script '/var/www/html/bot/aqw.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:48.820438 2025] [php:error] [pid 1786] [client 10.10.4.17:43530] script '/var/www/html/bot/ca1.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:49.147996 2025] [php:error] [pid 1789] [client 10.10.4.17:43546] script '/var/www/html/bot/abouta.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:49.407973 2025] [php:error] [pid 1797] [client 10.10.4.17:43554] script '/var/www/html/bot/fclas.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:49.653098 2025] [php:error] [pid 8934] [client 10.10.4.17:43560] script '/var/www/html/bot/bbn.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:49.920798 2025] [php:error] [pid 1795] [client 10.10.4.17:43566] script '/var/www/html/bot/class-t.api.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:50.172379 2025] [php:error] [pid 17237] [client 10.10.4.17:43578] script '/var/www/html/bot/xz89.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:50.453757 2025] [php:error] [pid 1788] [client 10.10.4.17:43588] script '/var/www/html/bot/wft.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:50.705824 2025] [php:error] [pid 1786] [client 10.10.4.17:43598] script '/var/www/html/bot/ws28.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:50.950937 2025] [php:error] [pid 1789] [client 10.10.4.17:43604] script '/var/www/html/bot/wsvvs.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:51.209408 2025] [php:error] [pid 1797] [client 10.10.4.17:43618] script '/var/www/html/bot/pn.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:51.473887 2025] [php:error] [pid 8934] [client 10.10.4.17:43630] script '/var/www/html/bot/fz.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:51.785856 2025] [php:error] [pid 1795] [client 10.10.4.17:43636] script '/var/www/html/bot/sang.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:52.063447 2025] [php:error] [pid 17237] [client 10.10.4.17:43638] script '/var/www/html/bot/0o0.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:52.318597 2025] [php:error] [pid 1788] [client 10.10.4.17:43650] script '/var/www/html/bot/zxl.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:52.563999 2025] [php:error] [pid 1786] [client 10.10.4.17:43660] script '/var/www/html/bot/zwso.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:52.840912 2025] [php:error] [pid 1789] [client 10.10.4.17:43676] script '/var/www/html/bot/2clas.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:53.090305 2025] [php:error] [pid 1797] [client 10.10.4.17:43684] script '/var/www/html/bot/pp.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:53.367193 2025] [php:error] [pid 8934] [client 10.10.4.17:43690] script '/var/www/html/bot/files.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:53.623782 2025] [php:error] [pid 1795] [client 10.10.4.17:43702] script '/var/www/html/bot/11.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:53.917090 2025] [php:error] [pid 17237] [client 10.10.4.17:43716] script '/var/www/html/bot/12.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:54.163107 2025] [php:error] [pid 1788] [client 10.10.4.17:43720] script '/var/www/html/bot/13.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:54.410148 2025] [php:error] [pid 1786] [client 10.10.4.17:43722] script '/var/www/html/bot/enclas.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:54.694546 2025] [php:error] [pid 1789] [client 10.10.4.17:43724] script '/var/www/html/bot/he.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:54.943822 2025] [php:error] [pid 1797] [client 10.10.4.17:43730] script '/var/www/html/bot/fffff.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:55.226786 2025] [php:error] [pid 8934] [client 10.10.4.17:43742] script '/var/www/html/bot/sid.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:55.517818 2025] [php:error] [pid 1795] [client 10.10.4.17:43746] script '/var/www/html/bot/ssss.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:55.851627 2025] [php:error] [pid 17237] [client 10.10.4.17:43756] script '/var/www/html/bot/f6.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:56.102478 2025] [php:error] [pid 1788] [client 10.10.4.17:54428] script '/var/www/html/bot/dex.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:56.407374 2025] [php:error] [pid 1786] [client 10.10.4.17:54436] script '/var/www/html/bot/10.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:56.692844 2025] [php:error] [pid 1789] [client 10.10.4.17:54448] script '/var/www/html/bot/asus.php' not found or unable to stat
|
||||||
|
[Sat Dec 27 19:38:56.974003 2025] [php:error] [pid 1797] [client 10.10.4.17:54456] script '/var/www/html/bot/vee.php' not found or unable to stat
|
||||||
0
logs/telegram_bot_err.log
Executable file
0
logs/telegram_bot_err.log
Executable file
134314
logs/telegram_bot_out.log
Executable file
134314
logs/telegram_bot_out.log
Executable file
File diff suppressed because it is too large
Load Diff
94
logs/translation-worker.out.log
Executable file
94
logs/translation-worker.out.log
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
|
Could not open input file: /var/www/html/bot/process_translation_queue.php
|
||||||
40
shared/api/php_errors.log
Executable file
40
shared/api/php_errors.log
Executable 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
78
shared/api/stats.php
Executable 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
200
shared/auth/jwt.php
Executable 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
50
shared/bootstrap.php
Executable 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
76
shared/database/connection.php
Executable 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();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user