Initial commit - Last War messaging system
50
.env
Executable file
@@ -0,0 +1,50 @@
|
||||
# Configuración de la aplicación
|
||||
APP_ENV=production
|
||||
APP_URL=https://ponsprueba.duckdns.org/
|
||||
TZ=America/Mexico_City
|
||||
|
||||
# Configuración de la base de datos
|
||||
DB_HOST=10.10.4.17
|
||||
DB_PORT=3391
|
||||
DB_NAME=lastwar2
|
||||
DB_USER=nickpons666
|
||||
DB_PASS=MiPo6425@@
|
||||
DB_DIALECT=mysql
|
||||
|
||||
# Configuración de JWT
|
||||
JWT_SECRET=19c5020fa8207d2c3b9e82f430784667e001f1eb733848922f7bcb9be98f93c2
|
||||
|
||||
# Configuración de Discord
|
||||
DISCORD_GUILD_ID=1338327171013541999
|
||||
DISCORD_CLIENT_ID=1385790344594985061
|
||||
DISCORD_CLIENT_SECRET=hK9SNiYdenHQVxakt8Mx3RoMkZ5oOJvk
|
||||
DISCORD_BOT_TOKEN=MTM4NTc5MDM0NDU5NDk4NTA2MQ.GvobiS.TRQM9dX7vDjmuGVa3Ckp6YRtGEWxdW0gBDbvCI
|
||||
|
||||
# Configuración de Telegram
|
||||
TELEGRAM_BOT_TOKEN=8469229183:AAEVIV5e7rjDXKNgFTX0dnCW6JWB88X4p2I
|
||||
TELEGRAM_WEBHOOK_TOKEN=webhook_secure_token_12345
|
||||
|
||||
LIBRETRANSLATE_URL=https://translate-pons.duckdns.org
|
||||
|
||||
#N8N_URL=https://n8n-pons.duckdns.org
|
||||
#N8N_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4MWY4YjU3YS0wMTg2LTQ1NTctOWZlMC1jYWUxNjZlYzZlMTkiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzU1OTMwO>
|
||||
|
||||
# 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"
|
||||
|
||||
# URL completa del webhook de n8n que procesa la cola de mensajes (process_queue_workflow).
|
||||
# La obtienes del nodo Webhook en tu flujo de n8n.
|
||||
#N8N_PROCESS_QUEUE_WEBHOOK_URL="https://n8n-pons.duckdns.org/webhooktest/ia"
|
||||
#N8N_IA_WEBHOOK_URL="https://n8n-pons.duckdns.org/webhook/ia"
|
||||
#N8N_IA_WEBHOOK_URL_DISCORD=https://n8n-pons.duckdns.org/webhook/42e803ae-8aee-4b1c-858a-6c6d3fbb6230
|
||||
|
||||
#Base de datos para la ia
|
||||
KB_DB_HOST=10.10.4.17
|
||||
KB_DB_PORT=3391
|
||||
KB_DB_NAME=lastwar_mysql
|
||||
KB_DB_USER=nickpons666
|
||||
KB_DB_PASS=MiPo6425@@
|
||||
|
||||
#Token de groq
|
||||
GROQ_API_KEY=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
|
||||
32
.env.example
Executable file
@@ -0,0 +1,32 @@
|
||||
# Aplicación
|
||||
APP_ENVIRONMENT=pruebas
|
||||
APP_URL=http://localhost
|
||||
|
||||
# Base de datos
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=bot
|
||||
DB_USER=root
|
||||
DB_PASS=
|
||||
|
||||
# Discord
|
||||
DISCORD_BOT_TOKEN=
|
||||
DISCORD_GUILD_ID=
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
|
||||
# Telegram
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_WEBHOOK_TOKEN=
|
||||
|
||||
# LibreTranslate (opcional)
|
||||
LIBRETRANSLATE_URL=http://localhost:5000
|
||||
|
||||
# n8n (opcional)
|
||||
N8N_URL=
|
||||
N8N_TOKEN=
|
||||
N8N_IA_WEBHOOK_URL=
|
||||
|
||||
# Seguridad
|
||||
JWT_SECRET=
|
||||
INTERNAL_API_KEY=
|
||||
469
DOCUMENTACION_COMPLEMENTO.md
Executable file
@@ -0,0 +1,469 @@
|
||||
# Complemento de Documentación - Funcionalidades Adicionales
|
||||
|
||||
Este documento complementa el archivo principal `DOCUMENTACION_SISTEMA.md` con funcionalidades adicionales descubiertas.
|
||||
|
||||
---
|
||||
|
||||
## 1. Sistema de Traducción
|
||||
|
||||
### 1.1 Proceso de Traducción Automática
|
||||
|
||||
El sistema tiene múltiples capas de traducción:
|
||||
|
||||
#### A) Traducción en Tiempo Real (durante envío)
|
||||
Cuando se envía un mensaje con el flag `data-translate="true"`, el sistema:
|
||||
1. Detecta el idioma del mensaje
|
||||
2. Traduce a todos los idiomas activos configurados
|
||||
3. Envía las traducciones junto con el mensaje original
|
||||
|
||||
**Archivos involucrados:**
|
||||
- `src/Translate.php` - Clase principal de traducción
|
||||
- `process_queue.php` - Procesa el envío y traducción
|
||||
|
||||
#### B) Cola de Traducción Asíncrona (`process_translation_queue.php`)
|
||||
Procesa traducciones de mensajes de usuarios en background:
|
||||
|
||||
```
|
||||
Flujo:
|
||||
1. Mensaje de usuario entra al sistema
|
||||
2. Se encola en tabla translation_queue
|
||||
3. Worker procesa la cola:
|
||||
- Detecta idioma origen
|
||||
- Traduce a todos los idiomas activos
|
||||
- Envía embed con traducciones
|
||||
4. Marca como completed/failed
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Usa `pcntl` para señales (SIGINT, SIGTERM)
|
||||
- Bloqueo de filas con `FOR UPDATE SKIP LOCKED`
|
||||
- Reintentos (attempts < 5)
|
||||
- Manejo de menciones de Discord (protege durante traducción)
|
||||
|
||||
**Tabla: `translation_queue`**
|
||||
- `id`, `platform`, `message_id`, `chat_id`, `user_id`
|
||||
- `text_to_translate`, `source_lang`, `target_lang`
|
||||
- `status` (pending/processing/completed/failed)
|
||||
- `attempts`, `error_message`, `created_at`, `processed_at`
|
||||
|
||||
#### C) Traducción Manual (Botones)
|
||||
|
||||
**Discord:**
|
||||
- Después de enviar una plantilla, se añaden botones de traducción
|
||||
- Botón: `translate_manual:{lang}` o `translate_template:{command}:{lang}`
|
||||
- Traduce el mensaje al idioma seleccionado
|
||||
|
||||
**Telegram:**
|
||||
- Botones inline con callback `translate:{message_id}:{lang}`
|
||||
- Traduce y envía como mensaje nuevo
|
||||
|
||||
### 1.2 Proxy de Traducción (`translate_proxy.php`)
|
||||
|
||||
API REST que permite traducciones externas:
|
||||
- Expone endpoints de LibreTranslate
|
||||
- Maneja CORS
|
||||
- Lee configuración de entorno
|
||||
|
||||
---
|
||||
|
||||
## 2. Sistema de Mensajería
|
||||
|
||||
### 2.1 Tipos de Programación
|
||||
|
||||
El sistema soporta múltiples tipos de envío:
|
||||
|
||||
| Tipo | Descripción | Campo |
|
||||
|------|-------------|-------|
|
||||
| `now` | Envío inmediato | send_time = NOW() |
|
||||
| `later` | Programado para fecha/hora específica | send_time = fecha futura |
|
||||
| `recurring` | Recurrente (días de la semana) | is_recurring=1 + recurring_days + recurring_time |
|
||||
|
||||
### 2.2 Estados de un Mensaje
|
||||
|
||||
| Estado | Significado |
|
||||
|--------|-------------|
|
||||
| `draft` | Guardado sin programar |
|
||||
| `pending` | Esperando fecha de envío |
|
||||
| `processing` | Siendo enviado en este momento |
|
||||
| `sent` | Enviado exitosamente |
|
||||
| `failed` | Error en el envío |
|
||||
| `cancelled` | Cancelado por usuario |
|
||||
| `disabled` | Deshabilitado temporalmente |
|
||||
|
||||
### 2.3 Acciones sobre Mensajes Programados (`schedule_actions.php`)
|
||||
|
||||
| Acción | Descripción |
|
||||
|--------|-------------|
|
||||
| `disable` | Deshabilita el mensaje (status = disabled) |
|
||||
| `enable` | Habilita mensaje deshabilitado (status = pending) |
|
||||
| `cancel` | Cancela el envío (status = cancelled) |
|
||||
| `retry` | Reintenta envío fallido (status = pending) |
|
||||
| `delete` | Elimina la programación completamente |
|
||||
|
||||
---
|
||||
|
||||
## 3. Plantillas y Comandos
|
||||
|
||||
### 3.1 Sistema de Comandos
|
||||
|
||||
Las plantillas pueden tener comandos asociados (sin #) que los usuarios pueden ejecutar en Discord/Telegram:
|
||||
|
||||
```
|
||||
Comando en Telegram/Discord: #Dia6
|
||||
└── Sistema busca en recurrent_messages WHERE telegram_command = 'Dia6'
|
||||
└── Envía el message_content convertido al formato de la plataforma
|
||||
```
|
||||
|
||||
### 3.2 Envío desde Plantillas (`enviar_plantilla.php`)
|
||||
|
||||
Página para enviar rápidamente una plantilla a múltiples destinatarios:
|
||||
- Seleccionar plantilla
|
||||
- Elegir plataforma (Discord/Telegram)
|
||||
- Seleccionar múltiples destinatarios
|
||||
- Programar o enviar inmediatamente
|
||||
|
||||
---
|
||||
|
||||
## 4. Galería de Imágenes
|
||||
|
||||
### 4.1 Gestión de Imágenes
|
||||
|
||||
**Ubicación:** `/galeria/`
|
||||
|
||||
**Operaciones:**
|
||||
- `upload.php` - Subir nuevas imágenes
|
||||
- `delete_image.php` - Eliminar imágenes
|
||||
- `rename_image.php` - Renombrar imágenes
|
||||
- `upload_editor_image.php` - Subir desde el editor de mensajes
|
||||
|
||||
### 4.2 Uso en Mensajes
|
||||
|
||||
Las imágenes se insertan en el editor HTML (Summernote):
|
||||
- Modal de galería en `create_message.php` y `recurrentes.php`
|
||||
- Las URLs se almacenan relativas: `galeria/nombre.jpg`
|
||||
- Al enviar, se convierten a URLs absolutas
|
||||
|
||||
---
|
||||
|
||||
## 5. Conversores de Formato
|
||||
|
||||
El sistema necesita convertir HTML a los formatos de cada plataforma:
|
||||
|
||||
### 5.1 Discord (`discord/converters/HtmlToDiscordMarkdownConverter.php`)
|
||||
- Convierte etiquetas HTML a Markdown de Discord
|
||||
- Maneja imágenes, negritas, énfasis, etc.
|
||||
|
||||
### 5.2 Telegram (`telegram/converters/HtmlToTelegramHtmlConverter.php`)
|
||||
- Convierte HTML a formato HTML de Telegram
|
||||
- Soporta parse_mode=HTML
|
||||
|
||||
### 5.3 Factory (`common/helpers/converter_factory.php`)
|
||||
- Patrón Factory para obtener el conversor correcto según plataforma
|
||||
|
||||
---
|
||||
|
||||
## 6. Remitentes (Senders)
|
||||
|
||||
### 6.1 DiscordSender (`discord/DiscordSender.php`)
|
||||
- Envía mensajes via API REST de Discord
|
||||
- Divide mensajes largos (límite 2000 caracteres)
|
||||
- Maneja imágenes como adjuntos o embeds
|
||||
- Envía botones interactivos
|
||||
|
||||
### 6.2 TelegramSender (`telegram/TelegramSender.php`)
|
||||
- Envía mensajes via API de Telegram Bot
|
||||
- Maneja múltiples partes (texto + imágenes)
|
||||
- Soporta botones inline
|
||||
- Reply markup
|
||||
|
||||
---
|
||||
|
||||
## 7. Autenticación y Sesiones
|
||||
|
||||
### 7.1 Sistema de Login (`login.php` + `includes/auth.php`)
|
||||
- Autenticación con username/password
|
||||
- Hash de contraseñas con `password_hash()`
|
||||
- Sesiones PHP con verificación de rol
|
||||
|
||||
### 7.2 Verificación de Sesión (`includes/session_check.php`)
|
||||
- Se incluye en todas las páginas protegidas
|
||||
- Verifica `$_SESSION['user_id']` y `$_SESSION['role']`
|
||||
- Redirige a login si no hay sesión
|
||||
|
||||
### 7.3 Perfil de Usuario (`profile.php`)
|
||||
- Permite cambiar contraseña
|
||||
- Información de cuenta
|
||||
|
||||
---
|
||||
|
||||
## 8. Utilidades y Helpers
|
||||
|
||||
### 8.1 Helpers Principales
|
||||
|
||||
| Archivo | Función |
|
||||
|---------|---------|
|
||||
| `includes/url_helper.php` | Funciones de URL (site_url, asset, url) |
|
||||
| `includes/logger.php` | Función `custom_log()` para logs |
|
||||
| `includes/activity_logger.php` | Registro de actividades de usuarios |
|
||||
| `includes/emojis.php` | Utilidades de emojis |
|
||||
| `includes/schedule_helpers.php` | Cálculo de próximas fechas de envío |
|
||||
| `includes/error_handler.php` | Manejo de errores |
|
||||
|
||||
### 8.2 Rutas de Archivos Comunes
|
||||
|
||||
```
|
||||
includes/
|
||||
├── session_check.php → Verifica sesión en cada página
|
||||
├── db.php → Conexión PDO a MySQL
|
||||
├── auth.php → Funciones de autenticación
|
||||
├── logger.php → custom_log()
|
||||
├── activity_logger.php → log_activity()
|
||||
├── message_handler.php → Procesa create/edit de mensajes
|
||||
├── schedule_actions.php → Disable/enable/cancel/delete mensajes
|
||||
├── recurrent_message_handler.php → CRUD plantillas
|
||||
└── get_gallery.php → Obtiene imágenes de galeria/
|
||||
|
||||
common/helpers/
|
||||
├── sender_factory.php → Factory para obtener sender correcto
|
||||
├── converter_factory.php → Factory para obtener conversor
|
||||
├── schedule_helpers.php → calculateNextSendTime()
|
||||
├── url_helper.php → Funciones de URL
|
||||
└── emojis.php → Utilidades de emojis
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Workers y Procesos en Background
|
||||
|
||||
### 9.1 process_queue.php
|
||||
Procesa mensajes programados cada minuto (cron):
|
||||
- Busca schedules con status='pending' y send_time <= ahora
|
||||
- Marca como 'processing'
|
||||
- Envía mensaje
|
||||
- Registra en sent_messages
|
||||
- Si es recurrente, calcula próximo envío
|
||||
|
||||
### 9.2 process_translation_queue.php
|
||||
Worker de traducción asíncrono:
|
||||
- Corre como proceso daemon
|
||||
- Procesa translation_queue
|
||||
- Envía traducciones a Discord/Telegram
|
||||
|
||||
### 9.3 discord_bot.php
|
||||
Bot de Discord en tiempo real:
|
||||
- Corre como proceso largo (php discord_bot.php)
|
||||
- Escucha eventos via WebSocket
|
||||
- No usa cron, responde inmediatamente
|
||||
|
||||
### 9.4 run_manual_translation.php
|
||||
Script CLI para traducciones manuales:
|
||||
- Uso: `php run_manual_translation.php <msgId> <targetLang> <userId> <channelId>`
|
||||
- Traduce plantillas bajo demanda
|
||||
|
||||
---
|
||||
|
||||
## 10. Configuración de Webhooks
|
||||
|
||||
### 10.1 Telegram Webhook
|
||||
- **Archivo:** `telegram_bot_webhook.php` o `telegram/webhook/telegram_bot_webhook.php`
|
||||
- **URL:** `https://tu-dominio.com/telegram_bot_webhook.php?auth_token=TOKEN`
|
||||
- **Set webhook:** `set_webhook.php`
|
||||
|
||||
### 10.2 Endpoints de Configuración
|
||||
|
||||
| Archivo | Función |
|
||||
|---------|---------|
|
||||
| `set_webhook.php` | Configura webhook de Telegram |
|
||||
| `configure_webhook.php` | UI para configurar webhooks |
|
||||
| `check_webhook.php` | Verifica estado del webhook |
|
||||
|
||||
---
|
||||
|
||||
## 11. Páginas de Administración Adicionales
|
||||
|
||||
### 11.1 Actividades (`admin/activity.php`)
|
||||
Muestra el log de actividades de la tabla `activity_log`:
|
||||
- Inicios/cierres de sesión
|
||||
- Creación/eliminación de mensajes
|
||||
- Actualizaciones de usuarios
|
||||
- Eliminación de imágenes
|
||||
|
||||
### 11.2 Test de Conexión (`admin/test_discord_connection.php`)
|
||||
Permite probar la conexión con Discord:
|
||||
- Verifica token del bot
|
||||
- Prueba envío de mensaje de prueba
|
||||
|
||||
### 11.3 Comandos (`admin/comandos.php`)
|
||||
Lista los comandos disponibles en el sistema.
|
||||
|
||||
### 11.4 Opciones de Traducción Discord (`discord/admin/discord_translation_options.php`)
|
||||
Configuración específica de traducción para Discord.
|
||||
|
||||
---
|
||||
|
||||
## 12. Utilidades Varias
|
||||
|
||||
### 12.1 Cambio de Idioma (`change_language.php`)
|
||||
Permite cambiar el idioma de la interfaz de la aplicación.
|
||||
|
||||
### 12.2 Log Frontend (`log_frontend.php`)
|
||||
Muestra los logs de JavaScript del lado del cliente.
|
||||
|
||||
### 12.3 Verificador de Contraseña (`verify_password.php`)
|
||||
API para verificar contraseñas (probablemente usado en algún proceso de autenticación externo).
|
||||
|
||||
### 12.4 Verificador Directo (`direct_check.php`)
|
||||
Verificación directa de algún estado del sistema.
|
||||
|
||||
### 12.5 Limpiar OPCache (`clear_opcache.php`)
|
||||
Utilidad para limpiar el OPCache de PHP.
|
||||
|
||||
### 12.6 Reset de Estado (`reset_status.php`)
|
||||
Reinicia estados de mensajes (útil para debugging).
|
||||
|
||||
---
|
||||
|
||||
## 13. Variables de Entorno Completas
|
||||
|
||||
```env
|
||||
# Aplicación
|
||||
APP_ENVIRONMENT=pruebas # o 'reod'
|
||||
APP_URL=https://bot.tudominio.com
|
||||
|
||||
# Base de datos
|
||||
DB_HOST=10.10.4.17
|
||||
DB_PORT=3390
|
||||
DB_NAME=bot
|
||||
DB_USER=root
|
||||
DB_PASS=***
|
||||
DB_DIALECT=mysql
|
||||
|
||||
# Discord
|
||||
DISCORD_BOT_TOKEN=***
|
||||
DISCORD_GUILD_ID=***
|
||||
DISCORD_CLIENT_ID=***
|
||||
DISCORD_CLIENT_SECRET=***
|
||||
|
||||
# Telegram
|
||||
TELEGRAM_BOT_TOKEN=***
|
||||
TELEGRAM_WEBHOOK_TOKEN=***
|
||||
|
||||
# LibreTranslate
|
||||
LIBRETRANSLATE_URL=http://libretranslate:5000
|
||||
|
||||
# n8n (Automatización)
|
||||
N8N_URL=https://n8n.tudominio.com
|
||||
N8N_TOKEN=***
|
||||
N8N_IA_WEBHOOK_URL=***
|
||||
N8N_IA_WEBHOOK_URL_DISCORD=***
|
||||
N8N_PROCESS_QUEUE_WEBHOOK_URL=***
|
||||
|
||||
# Seguridad
|
||||
JWT_SECRET=***
|
||||
INTERNAL_API_KEY=***
|
||||
|
||||
# Docker
|
||||
DOCKER_CONTAINER=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Estructura de la Base de Datos (Tablas Completas)
|
||||
|
||||
```sql
|
||||
-- Tablas principales
|
||||
users -- Usuarios del sistema
|
||||
recipients -- Canales/usuarios de Discord/Telegram
|
||||
messages -- Contenido de mensajes
|
||||
schedules -- Programaciones de envío
|
||||
recurrent_messages -- Plantillas de mensajes
|
||||
sent_messages -- Registro de mensajes enviados
|
||||
activity_log -- Log de actividades
|
||||
|
||||
-- Configuración
|
||||
settings -- Configuraciones generales
|
||||
supported_languages -- Idiomas para traducción
|
||||
telegram_bot_messages -- Mensaje de bienvenida Telegram
|
||||
telegram_bot_interactions -- Interacciones de usuarios Telegram
|
||||
telegram_welcome_messages -- Mensajes de bienvenida por grupo
|
||||
command_locks -- Bloqueos de comandos (evitar duplicados)
|
||||
|
||||
-- Traducción
|
||||
translation_queue -- Cola de traducciones
|
||||
languages -- Tabla legacy de idiomas
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Flujo Completo: Desde la Creación hasta el Envío
|
||||
|
||||
```
|
||||
1. USUARIO CREA MENSAJE
|
||||
create_message.php
|
||||
└─> Editor HTML (Summernote)
|
||||
└─> Selecciona plataforma, destinatario, programación
|
||||
|
||||
2. PROCESAMIENTO
|
||||
includes/message_handler.php
|
||||
└─> Valida datos
|
||||
└─> Inserta en tabla 'messages'
|
||||
└─> Inserta en tabla 'schedules'
|
||||
└─> Si 'enviar ahora' → ejecuta process_queue.php
|
||||
|
||||
3. COLA DE PROCESAMIENTO (cron cada minuto)
|
||||
process_queue.php
|
||||
└─> Busca schedules pending con send_time <= ahora
|
||||
└─> Marca como 'processing'
|
||||
└─> Convierte HTML → Formato plataforma
|
||||
└─> Envía vía DiscordSender o TelegramSender
|
||||
└─> Registra en 'sent_messages'
|
||||
└─> Si recurrente → calcula próximo envío
|
||||
└─> Marca 'sent' o 'failed'
|
||||
|
||||
4. TRADUCCIÓN (opcional)
|
||||
a) Automática: Se encola en translation_queue
|
||||
└─> process_translation_queue.php
|
||||
|
||||
b) Manual: Botones en mensaje enviado
|
||||
└─> Usuario hace clic → traduce solo ese idioma
|
||||
|
||||
5. RESULTADO
|
||||
└─> Usuario puede ver en:
|
||||
- scheduled_messages.php (pendientes)
|
||||
- sent_messages.php (enviados)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Resumen de Rutas del Menú
|
||||
|
||||
```
|
||||
├── INICIO (index.php)
|
||||
│ └── Dashboard con mensajes programados
|
||||
│
|
||||
├── MENSAJES
|
||||
│ ├── Crear Mensaje (create_message.php)
|
||||
│ ├── Programados (scheduled_messages.php)
|
||||
│ ├── Plantillas (recurrentes.php)
|
||||
│ └── Enviados (sent_messages.php)
|
||||
│
|
||||
├── GALERÍA (gallery.php)
|
||||
│
|
||||
├── ADMIN (solo admins)
|
||||
│ ├── Usuarios (admin/users.php)
|
||||
│ ├── Destinatarios (admin/recipients.php)
|
||||
│ ├── Idiomas (admin/languages.php)
|
||||
│ ├── Comandos (admin/comandos.php)
|
||||
│ ├── Telegram Config (telegram/admin/telegram_welcome.php)
|
||||
│ ├── Interacciones Bot (telegram/admin/telegram_bot_interactions.php)
|
||||
│ ├── Chat Telegram (telegram/admin/chat_telegram.php)
|
||||
│ ├── Actividad (admin/activity.php)
|
||||
│ └── Test (admin/test_discord_connection.php)
|
||||
│
|
||||
├── PERFIL (profile.php)
|
||||
└── CERRAR SESIÓN (logout.php)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Este complemento junto con el documento principal `DOCUMENTACION_SISTEMA.md` proporcionan una visión completa del sistema.
|
||||
873
DOCUMENTACION_SISTEMA.md
Executable file
@@ -0,0 +1,873 @@
|
||||
# Documentación Técnica del Sistema de Bots de Mensajería
|
||||
|
||||
## 1. Visión General del Sistema
|
||||
|
||||
Este es un **sistema de mensajería multiplataforma** desarrollado en PHP que permite gestionar y enviar notificaciones automatizadas a través de **Discord** y **Telegram**. El sistema incluye:
|
||||
|
||||
- Programación de mensajes (una vez o recurrentes)
|
||||
- Traducción automática mediante LibreTranslate
|
||||
- Galería de imágenes para usar en mensajes
|
||||
- Plantillas de mensajes recurrentes
|
||||
- Panel de administración completo
|
||||
- Integración con n8n para automatización/IA
|
||||
|
||||
---
|
||||
|
||||
## 2. Tecnologías y Herramientas Utilizadas
|
||||
|
||||
### Backend
|
||||
- **PHP 8.3+** - Lenguaje principal del servidor
|
||||
- **MySQL 9.5+** - Base de datos relacional
|
||||
- **Composer** - Gestión de dependencias PHP
|
||||
|
||||
### APIs y Servicios Externos
|
||||
- **Discord API v10** - Bot de Discord (librería discord-php)
|
||||
- **Telegram Bot API** - Envío de mensajes via HTTP
|
||||
- **LibreTranslate** - Servicio de traducción automática
|
||||
- **n8n** - Plataforma de automatización (webhooks para IA)
|
||||
|
||||
### Frontend
|
||||
- **Bootstrap 5.3** - Framework CSS responsivo
|
||||
- **Bootstrap Icons** - Iconos
|
||||
- **Summernote** - Editor WYSIWYG para mensajes HTML
|
||||
- **jQuery** - Manipulación del DOM
|
||||
|
||||
### Infraestructura
|
||||
- **Zone Horaria**: America/Mexico_City (UTC-6)
|
||||
- **Entornos**: pruebas, reod (producción)
|
||||
- **Sistema de Logs**: Monolog + logs nativos PHP
|
||||
|
||||
---
|
||||
|
||||
## 3. Estructura de Archivos del Proyecto
|
||||
|
||||
```
|
||||
/var/www/html/bot/
|
||||
├── index.php # Dashboard principal
|
||||
├── login.php # Página de inicio de sesión
|
||||
├── logout.php # Cerrar sesión
|
||||
├── profile.php # Perfil del usuario
|
||||
│
|
||||
├── create_message.php # Crear nuevo mensaje
|
||||
├── edit_message.php # Editar mensaje existente
|
||||
├── scheduled_messages.php # Ver mensajes programados
|
||||
├── sent_messages.php # Ver mensajes enviados
|
||||
├── recurrentes.php # Gestionar plantillas recurrentes
|
||||
├── preview_message.php # Previsualizar mensaje
|
||||
├── enviar_plantilla.php # Enviar plantilla a usuarios
|
||||
│
|
||||
├── gallery.php # Galería de imágenes
|
||||
├── upload.php # Subir imágenes
|
||||
├── delete_image.php # Eliminar imágenes
|
||||
├── rename_image.php # Renombrar imágenes
|
||||
├── upload_editor_image.php # Subir imágenes desde editor
|
||||
│
|
||||
├── change_language.php # Cambiar idioma de la interfaz
|
||||
│
|
||||
├── discord_bot.php # Bot de Discord (proceso largo)
|
||||
├── telegram_bot_webhook.php # Webhook de Telegram
|
||||
├── process_queue.php # Procesador de cola de mensajes
|
||||
│
|
||||
├── config/
|
||||
│ └── config.php # Configuración principal
|
||||
│
|
||||
├── includes/ # Archivos incluidos
|
||||
│ ├── db.php # Conexión a base de datos
|
||||
│ ├── session_check.php # Verificación de sesión
|
||||
│ ├── auth.php # Autenticación
|
||||
│ ├── logger.php # Funciones de logging
|
||||
│ ├── activity_logger.php # Registro de actividades
|
||||
│ ├── message_handler.php # Procesamiento de mensajes
|
||||
│ ├── message_handler_edit.php
|
||||
│ ├── schedule_helpers.php # Funciones de programación
|
||||
│ ├── schedule_actions.php # Acciones de programación
|
||||
│ ├── recurrent_message_handler.php
|
||||
│ ├── translation_helper.php # Helper de traducción
|
||||
│ ├── url_helper.php # Helper de URLs
|
||||
│ ├── emojis.php # Emojis disponibles
|
||||
│ ├── get_gallery.php # Obtener imágenes galería
|
||||
│ ├── error_handler.php # Manejo de errores
|
||||
│ ├── telegram_actions.php # Acciones de Telegram
|
||||
│ ├── discord_actions.php # Acciones de Discord
|
||||
│ ├── tren_handler.php # Manejo de eventos de tren
|
||||
│ ├── command_locker.php # Bloqueo de comandos
|
||||
│ └── get_chat_history.php # Historial de chat
|
||||
│
|
||||
├── src/ # Clases principales
|
||||
│ ├── Translate.php # Clase de traducción
|
||||
│ ├── TelegramSender.php # Envío a Telegram
|
||||
│ ├── DiscordSender.php # Envío a Discord
|
||||
│ ├── HtmlToTelegramHtmlConverter.php
|
||||
│ ├── HtmlToDiscordMarkdownConverter.php
|
||||
│ ├── TranslationWorker.php # Worker de traducción
|
||||
│ ├── TranslationCache.php # Cache de traducciones
|
||||
│ ├── CommandLocker.php # Bloqueo de comandos
|
||||
│ └── TranslationWorkerPool.php
|
||||
│
|
||||
├── discord/ # Módulo Discord
|
||||
│ ├── DiscordSender.php
|
||||
│ ├── converters/
|
||||
│ │ └── HtmlToDiscordMarkdownConverter.php
|
||||
│ └── actions/
|
||||
│ └── discord_actions.php
|
||||
│
|
||||
├── telegram/ # Módulo Telegram
|
||||
│ ├── TelegramSender.php
|
||||
│ ├── converters/
|
||||
│ │ └── HtmlToTelegramHtmlConverter.php
|
||||
│ ├── actions/
|
||||
│ │ ├── telegram_actions.php
|
||||
│ │ └── send_telegram_reply.php
|
||||
│ ├── admin/
|
||||
│ │ ├── telegram_bot_interactions.php
|
||||
│ │ ├── telegram_welcome.php
|
||||
│ │ └── chat_telegram.php
|
||||
│ └── webhook/
|
||||
│ └── telegram_bot_webhook.php
|
||||
│
|
||||
├── admin/ # Panel de Administración
|
||||
│ ├── users.php # Gestión de usuarios
|
||||
│ ├── recipients.php # Gestión de destinatarios
|
||||
│ ├── languages.php # Gestión de idiomas
|
||||
│ ├── comandos.php # Gestión de comandos
|
||||
│ ├── activity.php # Ver actividad del sistema
|
||||
│ ├── test_discord_connection.php
|
||||
│ ├── set_test_webhook.php
|
||||
│ ├── sync_languages.php # Sincronizar idiomas
|
||||
│ ├── get_user_groups.php
|
||||
│ ├── update_language_status.php
|
||||
│ └── update_language_flag.php
|
||||
│
|
||||
├── templates/ # Plantillas UI
|
||||
│ ├── header.php # Encabezado con menú
|
||||
│ └── footer.php # Pie de página
|
||||
│
|
||||
├── galeria/ # Imágenes del sistema
|
||||
│
|
||||
├── db/
|
||||
│ └── bot.sql # Esquema de base de datos
|
||||
│
|
||||
├── logs/ # Archivos de log
|
||||
│ ├── discord_bot.log
|
||||
│ ├── telegram_bot_webhook.log
|
||||
│ ├── process_queue.log
|
||||
│ └── php_errors.log
|
||||
│
|
||||
├── vendor/ # Dependencias Composer
|
||||
└── config/ # Configuraciones adicionales
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Estructura de la Base de Datos
|
||||
|
||||
### Tablas Principales
|
||||
|
||||
#### `users` - Usuarios del sistema
|
||||
- `id` (INT, PK)
|
||||
- `username` (VARCHAR)
|
||||
- `password` (VARCHAR, hashed)
|
||||
- `role` (ENUM: 'user', 'admin')
|
||||
- `telegram_chat_id` (VARCHAR, nullable)
|
||||
- `created_at` (TIMESTAMP)
|
||||
|
||||
#### `recipients` - Destinatarios (canales/usuarios)
|
||||
- `id` (INT, PK)
|
||||
- `platform_id` (BIGINT) - ID en Discord/Telegram
|
||||
- `name` (VARCHAR)
|
||||
- `type` (ENUM: 'channel', 'user')
|
||||
- `platform` (ENUM: 'discord', 'telegram')
|
||||
- `language_code` (VARCHAR, default 'es')
|
||||
- `chat_mode` (VARCHAR) - Modo de chat: agent, bot, ia
|
||||
- `created_at` (TIMESTAMP)
|
||||
|
||||
#### `messages` - Contenido de mensajes
|
||||
- `id` (INT, PK)
|
||||
- `user_id` (INT, FK)
|
||||
- `content` (TEXT) - HTML del mensaje
|
||||
- `created_at` (TIMESTAMP)
|
||||
|
||||
#### `schedules` - Programaciones de envío
|
||||
- `id` (INT, PK)
|
||||
- `message_id` (INT, FK)
|
||||
- `recipient_id` (INT, FK)
|
||||
- `send_time` (DATETIME)
|
||||
- `status` (ENUM: 'draft', 'pending', 'processing', 'sent', 'failed')
|
||||
- `is_recurring` (BOOLEAN)
|
||||
- `recurring_days` (VARCHAR) - Días separados por coma
|
||||
- `recurring_time` (TIME)
|
||||
- `sent_at` (DATETIME)
|
||||
- `error_message` (TEXT)
|
||||
- `created_at` (TIMESTAMP)
|
||||
|
||||
#### `recurrent_messages` - Plantillas de mensajes
|
||||
- `id` (INT, PK)
|
||||
- `name` (VARCHAR)
|
||||
- `message_content` (TEXT)
|
||||
- `telegram_command` (VARCHAR) - Comando sin #
|
||||
- `created_at` (TIMESTAMP)
|
||||
- `updated_at` (TIMESTAMP)
|
||||
|
||||
#### `sent_messages` - Mensajes enviados
|
||||
- `id` (INT, PK)
|
||||
- `schedule_id` (INT, FK)
|
||||
- `recipient_id` (INT, FK)
|
||||
- `platform_message_id` (VARCHAR) - ID del mensaje en plataforma
|
||||
- `message_count` (INT)
|
||||
- `sent_at` (DATETIME)
|
||||
|
||||
#### `supported_languages` - Idiomas disponibles
|
||||
- `id` (INT, PK)
|
||||
- `language_code` (VARCHAR) - Código (es, pt, en, etc.)
|
||||
- `language_name` (VARCHAR) - Nombre del idioma
|
||||
- `flag_emoji` (VARCHAR) - Emoji de bandera
|
||||
- `is_active` (BOOLEAN) - Si está activo para traducción
|
||||
- `created_at` (TIMESTAMP)
|
||||
- `updated_at` (TIMESTAMP)
|
||||
|
||||
#### `telegram_bot_messages` - Configuración de Bienvenida Telegram
|
||||
- `id` (INT, PK)
|
||||
- `message_text` (TEXT) - Mensaje de bienvenida (soporta {user_name})
|
||||
- `button_text` (VARCHAR) - Texto del botón
|
||||
- `group_invite_link` (VARCHAR) - Enlace de invitación al grupo
|
||||
- `is_active` (BOOLEAN) - Si el mensaje está activo
|
||||
- `register_users` (BOOLEAN) - Si se registran automáticamente los usuarios
|
||||
- `updated_at` (TIMESTAMP)
|
||||
|
||||
#### `telegram_bot_interactions` - Registro de Interacciones Telegram
|
||||
- `id` (INT, PK)
|
||||
- `user_id` (BIGINT) - ID del usuario en Telegram
|
||||
- `username` (VARCHAR) - Username de Telegram
|
||||
- `first_name` (VARCHAR) - Nombre del usuario
|
||||
- `last_name` (VARCHAR) - Apellido del usuario
|
||||
- `interaction_type` (VARCHAR) - Tipo: 'message', 'start', etc.
|
||||
- `interaction_date` (TIMESTAMP)
|
||||
|
||||
#### `settings` - Configuraciones Generales
|
||||
- `setting_key` (VARCHAR, PK)
|
||||
- `setting_value` (VARCHAR)
|
||||
|
||||
#### `telegram_welcome_messages` - Mensajes de Bienvenida por Grupo
|
||||
- `id` (INT, PK)
|
||||
- `chat_id` (BIGINT) - ID del grupo de Telegram
|
||||
- `welcome_message` (TEXT) - Mensaje personalizado
|
||||
- `button_text` (VARCHAR)
|
||||
- `group_invite_link` (VARCHAR)
|
||||
- `is_active` (BOOLEAN)
|
||||
- `created_at` (TIMESTAMP)
|
||||
- `updated_at` (TIMESTAMP)
|
||||
- `language_code` (VARCHAR)
|
||||
- `language_name` (VARCHAR)
|
||||
- `flag_emoji` (VARCHAR)
|
||||
- `is_active` (BOOLEAN)
|
||||
- `created_at` (TIMESTAMP)
|
||||
|
||||
#### `translation_queue` - Cola de traducciones
|
||||
- `id` (INT, PK)
|
||||
- `platform` (VARCHAR)
|
||||
- `message_id` (BIGINT)
|
||||
- `chat_id` (BIGINT)
|
||||
- `user_id` (BIGINT)
|
||||
- `text_to_translate` (TEXT)
|
||||
- `source_lang` (VARCHAR)
|
||||
- `target_lang` (VARCHAR)
|
||||
- `status` (ENUM: 'pending', 'completed', 'failed')
|
||||
- `created_at` (TIMESTAMP)
|
||||
|
||||
#### `activity_log` - Registro de actividades
|
||||
- `id` (INT, PK)
|
||||
- `user_id` (INT)
|
||||
- `username` (VARCHAR)
|
||||
- `action` (VARCHAR)
|
||||
- `details` (TEXT)
|
||||
- `timestamp` (DATETIME)
|
||||
|
||||
#### `telegram_interactions` - Interacciones Telegram
|
||||
- `id` (INT, PK)
|
||||
- `chat_id` (BIGINT)
|
||||
- `message_text` (TEXT)
|
||||
- `direction` (ENUM: 'in', 'out')
|
||||
- `language_code` (VARCHAR)
|
||||
- `created_at` (TIMESTAMP)
|
||||
|
||||
#### `command_locks` - Bloqueos de comandos
|
||||
- `id` (INT, PK)
|
||||
- `chat_id` (BIGINT)
|
||||
- `command` (VARCHAR)
|
||||
- `type` (ENUM: 'command', 'translation')
|
||||
- `data` (JSON)
|
||||
- `message_id` (BIGINT)
|
||||
- `status` (ENUM: 'processing', 'completed', 'failed')
|
||||
- `created_at` (TIMESTAMP)
|
||||
- `expires_at` (DATETIME)
|
||||
|
||||
---
|
||||
|
||||
## 5. Análisis del Menú y Rutas
|
||||
|
||||
### 5.1 Menú Principal (templates/header.php)
|
||||
|
||||
```
|
||||
├── INICIO (index.php)
|
||||
│ └── Dashboard - Vista de mensajes programados del usuario
|
||||
│
|
||||
├── MENSAJES (dropdown)
|
||||
│ ├── Crear Mensaje (create_message.php)
|
||||
│ │ ├── Carga plantillas
|
||||
│ │ ├── Editor HTML (Summernote)
|
||||
│ │ ├── Selección de plataforma (Discord/Telegram)
|
||||
│ │ ├── Selección de destinatario
|
||||
│ │ ├── Programación (ahora/fecha/recurrente)
|
||||
│ │ └── Envío → includes/message_handler.php
|
||||
│ │
|
||||
│ ├── Programados (scheduled_messages.php)
|
||||
│ │ └── Lista de mensajes pendientes/enviados
|
||||
│ │
|
||||
│ ├── Plantillas (recurrentes.php)
|
||||
│ │ ├── Crear plantilla
|
||||
│ │ ├── Editor HTML
|
||||
│ │ ├── Comando Telegram (sin #)
|
||||
│ │ └── includes/recurrent_message_handler.php
|
||||
│ │
|
||||
│ └── Enviados (sent_messages.php)
|
||||
│ └── Historial de mensajes enviados
|
||||
│
|
||||
├── GALERÍA (gallery.php)
|
||||
│ ├── Ver todas las imágenes
|
||||
│ ├── Subir imagen → upload.php
|
||||
│ ├── Renombrar → rename_image.php
|
||||
│ └── Eliminar → delete_image.php
|
||||
│
|
||||
├── ADMIN (solo admins)
|
||||
│ ├── Gestión
|
||||
│ │ ├── Usuarios (admin/users.php)
|
||||
│ │ │ ├── Crear usuario
|
||||
│ │ │ ├── Editar usuario
|
||||
│ │ │ ├── Asignar rol (user/admin)
|
||||
│ │ │ └── Vincular Telegram
|
||||
│ │ │
|
||||
│ │ └── Destinatarios (admin/recipients.php)
|
||||
│ │ ├── Listar canales/usuarios Discord
|
||||
│ │ ├── Listar canales/usuarios Telegram
|
||||
│ │ ├── Editar idioma
|
||||
│ │ └── Eliminar destinatario
|
||||
│ │
|
||||
│ ├── Configuración
|
||||
│ │ ├── Idiomas (admin/languages.php)
|
||||
│ │ │ ├── Lista de idiomas soportados
|
||||
│ │ │ ├── Activar/desactivar idioma
|
||||
│ │ │ ├── Editar bandera emoji
|
||||
│ │ │ └── Sincronizar con LibreTranslate
|
||||
│ │ │
|
||||
│ │ └── Comandos (admin/comandos.php)
|
||||
│ │ └── Lista de comandos disponibles
|
||||
│ │
|
||||
│ ├── Bots (Telegram)
|
||||
│ │ ├── Config (telegram/admin/telegram_welcome.php)
|
||||
│ │ │ └── Configurar mensaje de bienvenida
|
||||
│ │ │
|
||||
│ │ ├── Interacciones Bot (telegram/admin/telegram_bot_interactions.php)
|
||||
│ │ │ └── Ver interacciones de usuarios
|
||||
│ │ │
|
||||
│ │ └── Chat Telegram (telegram/admin/chat_telegram.php)
|
||||
│ │ └── Ver historial de chat
|
||||
│ │
|
||||
│ └── Monitoreo
|
||||
│ ├── Actividad (admin/activity.php)
|
||||
│ │ └── Log de actividades del sistema
|
||||
│ │
|
||||
│ └── Test (admin/test_discord_connection.php)
|
||||
│ └── Probar conexión con Discord
|
||||
│
|
||||
├── PERFIL (profile.php)
|
||||
│ └── Configuración de cuenta de usuario
|
||||
│
|
||||
└── CERRAR SESIÓN (logout.php)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Flujo de Funcionamiento
|
||||
|
||||
### 6.1 Creación y Envío de Mensaje
|
||||
|
||||
```
|
||||
1. Usuario inicia sesión (login.php)
|
||||
└── Valida credenciales → includes/auth.php
|
||||
└── Crea sesión PHP
|
||||
|
||||
2. Usuario crea mensaje (create_message.php)
|
||||
├── Selecciona plantilla (opcional)
|
||||
├── Escribe/edita contenido HTML (Summernote)
|
||||
├── Selecciona plataforma (Discord/Telegram)
|
||||
├── Selecciona destinatario(s)
|
||||
├── Selecciona programación:
|
||||
│ ├── Enviar ahora
|
||||
│ ├── Programar para fecha/hora
|
||||
│ └── Programación recurrente (días + hora)
|
||||
└── Envía formulario
|
||||
|
||||
3. Procesamiento (includes/message_handler.php)
|
||||
├── Valida datos
|
||||
├── Inserta mensaje en tabla `messages`
|
||||
├── Inserta programación en tabla `schedules`
|
||||
└── Si es "enviar ahora":
|
||||
└── Ejecuta process_queue.php en background
|
||||
|
||||
4. Cola de procesamiento (process_queue.php)
|
||||
├── Se ejecuta cada minuto (cron job)
|
||||
├── Busca mensajes pending con send_time <= ahora
|
||||
├── Marca como 'processing'
|
||||
├── Convierte HTML a formato de plataforma:
|
||||
│ ├── Discord → DiscordSender + HtmlToDiscordMarkdownConverter
|
||||
│ └── Telegram → TelegramSender + HtmlToTelegramHtmlConverter
|
||||
├── Envía mensaje a API de plataforma
|
||||
├── Registra en `sent_messages`
|
||||
├── Si es recurrente, calcula próximo envío
|
||||
└── Marca como 'sent' o 'failed'
|
||||
|
||||
5. Usuario puede ver:
|
||||
├── Mensajes programados (scheduled_messages.php)
|
||||
├── Mensajes enviados (sent_messages.php)
|
||||
└── Plantillas guardadas (recurrentes.php)
|
||||
```
|
||||
|
||||
### 6.2 Flujo de Traducción Automática
|
||||
|
||||
```
|
||||
1. Mensaje se crea con atributo data-translate="true"
|
||||
→ Se encola para traducción
|
||||
|
||||
2. Translation Worker (process_translation_queue.php)
|
||||
├── Detecta idioma original (LibreTranslate /detect)
|
||||
├── Traduce a cada idioma activo (LibreTranslate /translate)
|
||||
├── Guarda en translation_queue
|
||||
└── Envía resultado a usuario
|
||||
|
||||
3. Botones de traducción en Telegram
|
||||
├── Se añaden después de enviar mensaje
|
||||
├── Callback: translate:{message_id}:{lang}
|
||||
└── Usuario puede traducir manualmente
|
||||
|
||||
4. Botones de traducción en Discord
|
||||
├── Se añaden después de enviar mensaje
|
||||
├── Botones con estilo: translate_manual:{lang}
|
||||
└── Traducción inline en canal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Bots de Mensajería
|
||||
|
||||
### 7.1 Bot de Discord (discord_bot.php)
|
||||
|
||||
**Tecnologías:**
|
||||
- Librería `discord-php` (v7+)
|
||||
- WebSockets para eventos en tiempo real
|
||||
- API REST para envío de mensajes
|
||||
|
||||
**Eventos manejados:**
|
||||
|
||||
| Evento | Acción |
|
||||
|--------|--------|
|
||||
| `GUILD_MEMBER_ADD` | Registra nuevo miembro en recipients |
|
||||
| `MESSAGE_CREATE` | Procesa mensajes entrantes |
|
||||
| `INTERACTION_CREATE` | Maneja clics en botones |
|
||||
|
||||
**Comandos disponibles:**
|
||||
|
||||
| Comando | Descripción |
|
||||
|---------|-------------|
|
||||
| `#comando` | Envía plantilla de mensaje |
|
||||
| `/comandos` | Lista de comandos disponibles |
|
||||
| `/setlang XX` | Establece idioma del usuario |
|
||||
| `/bienvenida` | Envía mensaje de bienvenida |
|
||||
| `/agente [prompt]` | Envía a n8n para IA |
|
||||
| `!ping` | Responde "pong!" |
|
||||
|
||||
**Modos de Chat:**
|
||||
- `agent`: Menú de selección (bot/IA)
|
||||
- `bot`: Comandos normales
|
||||
- `ia`: Todo se envía a n8n
|
||||
|
||||
**Traducción:**
|
||||
- Detecta idioma automáticamente
|
||||
- Encola traducciones para todos los idiomas activos
|
||||
- Botones para traducir manualmente
|
||||
|
||||
### 7.2 Bot de Telegram (telegram_bot_webhook.php)
|
||||
|
||||
**Tecnologías:**
|
||||
- API de Webhooks de Telegram
|
||||
- cURL para llamadas HTTP
|
||||
|
||||
**Flujo de un mensaje:**
|
||||
|
||||
```
|
||||
1. Telegram envía Update al webhook
|
||||
└── Autenticación por auth_token
|
||||
|
||||
2. Determina tipo de chat:
|
||||
├── Chat privado → Modo agente/IA
|
||||
│ ├── Registro de nuevo usuario
|
||||
│ ├── Menú de bienvenida
|
||||
│ └── Modos: agent, bot, ia
|
||||
│
|
||||
└── Grupo/Canal → Lógica de bot
|
||||
├── Nuevos miembros → Se registran
|
||||
├── Comandos (/) → Se procesan
|
||||
├── #comando → Envía plantilla
|
||||
└── Mensajes normales → Traducción
|
||||
|
||||
3. Comandos disponibles:
|
||||
├── /setlang XX - Idioma
|
||||
├── /bienvenida - Mensaje de bienvenida
|
||||
├── /comandos - Lista de comandos
|
||||
└── /agente - Cambiar a modo IA
|
||||
|
||||
4. Traducción:
|
||||
├── Automática en cola
|
||||
└── Botones inline para traducir
|
||||
|
||||
### 7.3 Registro de Usuarios y Mensajes de Bienvenida
|
||||
|
||||
#### Discord - Registro de Nuevos Miembros
|
||||
|
||||
Cuando un nuevo miembro se une al servidor de Discord, el bot ejecuta el evento `GUILD_MEMBER_ADD`:
|
||||
|
||||
```
|
||||
Evento: GUILD_MEMBER_ADD
|
||||
└── Se ejecuta en: discord_bot.php (líneas 53-66)
|
||||
│
|
||||
├── 1. Obtiene datos del miembro:
|
||||
│ ├── platform_id = member.id (ID de Discord)
|
||||
│ ├── name = member.user.username
|
||||
│ └── platform = 'discord'
|
||||
│
|
||||
├── 2. Inserta/actualiza en tabla 'recipients':
|
||||
│ └── INSERT INTO recipients (platform_id, name, type, platform, language_code)
|
||||
│ VALUES (?, ?, 'user', 'discord', 'es')
|
||||
│ ON DUPLICATE KEY UPDATE name = VALUES(name)
|
||||
│
|
||||
└── 3. Queda registrado para recibir mensajes
|
||||
```
|
||||
|
||||
**Ubicación en código:** `discord_bot.php:53-66`
|
||||
|
||||
#### Telegram - Registro de Nuevos Usuarios
|
||||
|
||||
Cuando un usuario interactúa por primera vez con el bot de Telegram:
|
||||
|
||||
```
|
||||
Evento: Primer mensaje / interacción
|
||||
└── Se ejecuta en: telegram_bot_webhook.php (líneas 99-142)
|
||||
│
|
||||
├── 1. Verifica configuración de registro:
|
||||
│ └── SELECT * FROM telegram_bot_messages WHERE id = 1
|
||||
│ └── register_users = 1 (habilitado por defecto)
|
||||
│
|
||||
├── 2. Obtiene datos del usuario:
|
||||
│ ├── platform_id = from.id
|
||||
│ ├── name = first_name + last_name
|
||||
│ ├── language_code = from.language_code
|
||||
│ └── platform = 'telegram'
|
||||
│
|
||||
├── 3. Inserta en tabla 'recipients':
|
||||
│ └── INSERT INTO recipients (platform_id, name, type, platform, language_code, chat_mode)
|
||||
│ VALUES (?, ?, 'user', 'telegram', ?, 'agent')
|
||||
│ ON DUPLICATE KEY UPDATE name = VALUES(name)
|
||||
│
|
||||
└── 4. Si está activo, envía mensaje de bienvenida:
|
||||
├── Lee configuración: telegram_bot_messages
|
||||
├── Reemplaza {user_name} con el nombre del usuario
|
||||
├── Envía botón con enlace al grupo
|
||||
└── Registra interacción en telegram_bot_interactions
|
||||
```
|
||||
|
||||
**Ubicación en código:** `telegram_bot_webhook.php:99-142`
|
||||
|
||||
#### Telegram - Nuevos Miembros en Grupos
|
||||
|
||||
Cuando un nuevo miembro se une a un grupo de Telegram:
|
||||
|
||||
```
|
||||
Evento: new_chat_members (en grupos)
|
||||
└── Se ejecuta en: telegram_bot_webhook.php (líneas 222-234)
|
||||
│
|
||||
├── 1. Detecta nuevos miembros:
|
||||
│ └── message['new_chat_members']
|
||||
│
|
||||
├── 2. Para cada nuevo miembro (si no es bot):
|
||||
│ ├── Registra en recipients
|
||||
│ └── Language code del usuario
|
||||
│
|
||||
└── 3. No envía bienvenida automática (solo en chat privado)
|
||||
```
|
||||
|
||||
**Ubicación en código:** `telegram_bot_webhook.php:222-234`
|
||||
|
||||
#### Mensaje de Bienvenida - Configuración
|
||||
|
||||
Los mensajes de bienvenida se configuran desde el panel de admin:
|
||||
|
||||
**Para Telegram:**
|
||||
- **Ubicación**: `telegram/admin/telegram_bot_interactions.php`
|
||||
- **Tabla**: `telegram_bot_messages`
|
||||
- **Campos configurables**:
|
||||
- `message_text`: Texto del mensaje (soporta `{user_name}`)
|
||||
- `button_text`: Texto del botón de invitación
|
||||
- `group_invite_link`: Enlace al grupo de Telegram
|
||||
- `is_active`: Habilitar/deshabilitar mensaje
|
||||
- `register_users`: Registrar automáticamente usuarios
|
||||
|
||||
**Ejemplo de mensaje de bienvenida:**
|
||||
```
|
||||
¡Hola {user_name}! 👋
|
||||
|
||||
Puedes usar /comandos para obtener una lista de comandos con
|
||||
la información que tenemos sobre el juego y solicitarla con el #comando.
|
||||
|
||||
También puedes usar /agente, para interactuar con los comandos
|
||||
o con la AI que tenemos.
|
||||
|
||||
Gracias por interactuar con nuestro bot de REOD.
|
||||
Únete a nuestro grupo principal para mantenerte actualizado.
|
||||
```
|
||||
|
||||
**Tabla de Interacciones:**
|
||||
- Los usuarios quedan registrados en `telegram_bot_interactions`
|
||||
- Tipos de interacción: 'message', 'start', 'callback', etc.
|
||||
- Permite hacer auditoría de uso del bot
|
||||
|
||||
---
|
||||
|
||||
## 8. Páginas Principales y sus Archivos
|
||||
|
||||
### index.php (Dashboard)
|
||||
- **Propósito**: Mostrar mensajes programados del usuario
|
||||
- **Archivos incluidos**:
|
||||
- `includes/session_check.php`
|
||||
- `includes/db.php`
|
||||
- `templates/header.php`
|
||||
- `templates/footer.php`
|
||||
- **Datos**: Query de `schedules` + `messages` + `recipients`
|
||||
|
||||
### create_message.php (Crear Mensaje)
|
||||
- **Propósito**: Formulario para crear nuevo mensaje
|
||||
- **Archivos incluidos**:
|
||||
- `includes/session_check.php`
|
||||
- `includes/db.php`
|
||||
- `templates/header.php`
|
||||
- `templates/footer.php`
|
||||
- **Funcionalidades**:
|
||||
- Carga plantillas de `recurrent_messages`
|
||||
- Editor Summernote
|
||||
- Galería de imágenes (modal)
|
||||
- Selección de plataforma/destinatario
|
||||
- Programación (ahora/fecha/recurrente)
|
||||
|
||||
### scheduled_messages.php
|
||||
- **Propósito**: Lista de mensajes programados
|
||||
- **Procesa**: Tabla `schedules` con JOINs
|
||||
|
||||
### recurrentes.php (Plantillas)
|
||||
- **Propósito**: Gestionar plantillas de mensajes
|
||||
- **Archivos incluidos**:
|
||||
- `includes/recurrent_message_handler.php`
|
||||
- **Funcionalidades**:
|
||||
- CRUD de plantillas
|
||||
- Comando Telegram asociado
|
||||
- Preview de contenido
|
||||
|
||||
### gallery.php
|
||||
- **Propósito**: Gestión de imágenes
|
||||
- **Archivos**: `upload.php`, `delete_image.php`, `rename_image.php`
|
||||
- **Carpeta**: `/galeria/`
|
||||
|
||||
### admin/users.php
|
||||
- **Propósito**: Gestión de usuarios del sistema
|
||||
- **Acceso**: Solo admins (`role = 'admin'`)
|
||||
- **Funcionalidades**:
|
||||
- Crear/editar usuarios
|
||||
- Asignar roles
|
||||
- Vincular Telegram
|
||||
|
||||
### admin/recipients.php
|
||||
- **Propósito**: Gestión de destinatarios
|
||||
- **Plataformas**: Discord + Telegram
|
||||
- **Tipos**: Canales + Usuarios
|
||||
|
||||
### admin/languages.php
|
||||
- **Propósito**: Configurar idiomas de traducción
|
||||
- **Proveedor**: LibreTranslate
|
||||
- **Funcionalidades**:
|
||||
- Activar/desactivar idiomas
|
||||
- Editar emoji de bandera
|
||||
- Sincronización con API
|
||||
|
||||
### telegram/admin/telegram_bot_interactions.php
|
||||
- **Propósito**: Configurar mensaje de bienvenida de Telegram
|
||||
- **Tabla**: `telegram_bot_messages`
|
||||
- **Funcionalidades**:
|
||||
- Editar mensaje de bienvenida
|
||||
- Configurar botón de invitación
|
||||
- Establecer enlace al grupo
|
||||
- Activar/desactivar registro automático de usuarios
|
||||
|
||||
### telegram/admin/telegram_welcome.php
|
||||
- **Propósito**: Configurar mensajes de bienvenida por grupo
|
||||
- **Tabla**: `telegram_welcome_messages`
|
||||
- **Funcionalidades**:
|
||||
- Configurar mensaje por cada grupo
|
||||
- Personalizar botón e invitación
|
||||
|
||||
### telegram/admin/chat_telegram.php
|
||||
- **Propósito**: Ver historial de interacciones con el bot
|
||||
- **Tabla**: `telegram_bot_interactions`
|
||||
- **Funcionalidades**:
|
||||
- Lista de usuarios que han interactuado
|
||||
- Tipo de interacción (message, start, etc.)
|
||||
- Fecha de última interacción
|
||||
|
||||
### admin/activity.php
|
||||
- **Propósito**: Ver registro de actividades del sistema
|
||||
- **Tabla**: `activity_log`
|
||||
- **Acciones registradas**:
|
||||
- Inicio/cierre de sesión
|
||||
- Creación de mensajes
|
||||
- Eliminación de mensajes
|
||||
- Actualizaciones de usuarios
|
||||
- Eliminación de imágenes
|
||||
|
||||
---
|
||||
|
||||
## 9. Variables de Entorno (.env)
|
||||
|
||||
```env
|
||||
# Entorno
|
||||
APP_ENVIRONMENT=pruebas # o 'reod' para producción
|
||||
|
||||
# Base de datos
|
||||
DB_HOST=10.10.4.17
|
||||
DB_PORT=3390
|
||||
DB_NAME=bot
|
||||
DB_USER=root
|
||||
DB_PASS=*****
|
||||
|
||||
# Discord
|
||||
DISCORD_BOT_TOKEN=*****
|
||||
DISCORD_GUILD_ID=*****
|
||||
DISCORD_CLIENT_ID=*****
|
||||
DISCORD_CLIENT_SECRET=*****
|
||||
|
||||
# Telegram
|
||||
TELEGRAM_BOT_TOKEN=*****
|
||||
TELEGRAM_WEBHOOK_TOKEN=*****
|
||||
|
||||
# Traducción
|
||||
LIBRETRANSLATE_URL=http://libretranslate:5000
|
||||
|
||||
# n8n (Automatización/IA)
|
||||
N8N_URL=*****
|
||||
N8N_TOKEN=*****
|
||||
N8N_IA_WEBHOOK_URL=*****
|
||||
N8N_IA_WEBHOOK_URL_DISCORD=*****
|
||||
N8N_PROCESS_QUEUE_WEBHOOK_URL=*****
|
||||
|
||||
# Seguridad
|
||||
JWT_SECRET=*****
|
||||
APP_URL=https://tu-dominio.com
|
||||
|
||||
# API Interna
|
||||
INTERNAL_API_KEY=*****
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Configuración de Producción
|
||||
|
||||
### Cron Jobs Recomendados
|
||||
|
||||
```bash
|
||||
# Procesar cola de mensajes cada minuto
|
||||
* * * * * /usr/bin/php /var/www/html/bot/process_queue.php
|
||||
|
||||
# Limpiar logs antiguos (semanalmente)
|
||||
0 0 * * 0 find /var/www/html/bot/logs -name "*.log" -mtime +30 -delete
|
||||
|
||||
# Reiniciar bots si fallan (cada 5 minutos)
|
||||
*/5 * * * * pgrep -f "discord_bot.php" || /usr/bin/php /var/www/html/bot/discord_bot.php >> /var/www/html/bot/logs/discord_bot.out.log 2>&1 &
|
||||
```
|
||||
|
||||
### Webhooks
|
||||
|
||||
```
|
||||
# Telegram Webhook
|
||||
https://tu-dominio.com/telegram_bot_webhook.php?auth_token=TOKEN
|
||||
|
||||
# n8n (procesamiento)
|
||||
URL configurable desde admin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Funcionalidades por Archivo
|
||||
|
||||
| Archivo | Función Principal |
|
||||
|---------|-------------------|
|
||||
| `discord_bot.php` | Bot de Discord en tiempo real |
|
||||
| `telegram_bot_webhook.php` | Recibe mensajes de Telegram |
|
||||
| `process_queue.php` | Procesa mensajes programados |
|
||||
| `includes/message_handler.php` | Maneja creación/edición de mensajes |
|
||||
| `src/Translate.php` | Wrapper para LibreTranslate |
|
||||
| `discord/DiscordSender.php` | Envía mensajes a Discord |
|
||||
| `telegram/TelegramSender.php` | Envía mensajes a Telegram |
|
||||
| `common/helpers/converter_factory.php` | Convierte HTML entre formatos |
|
||||
|
||||
---
|
||||
|
||||
## 12. Resumen de Flujos
|
||||
|
||||
### Flujo Completo de un Mensaje Programado
|
||||
|
||||
```
|
||||
Usuario → create_message.php → message_handler.php
|
||||
→ schedules (DB) → process_queue.php (cron)
|
||||
→ DiscordSender/TelegramSender → API Plataforma
|
||||
→ sent_messages (DB) → (si recurrente) → recalcular próximo
|
||||
```
|
||||
|
||||
### Flujo de Traducción
|
||||
|
||||
```
|
||||
Mensaje con data-translate="true"
|
||||
→ translation_queue (DB)
|
||||
→ TranslationWorker
|
||||
→ LibreTranslate
|
||||
→ Envío a usuario
|
||||
```
|
||||
|
||||
### Flujo de Bot (Discord/Telegram)
|
||||
|
||||
```
|
||||
Plataforma → Webhook/Bot Event
|
||||
→ Verificar usuario
|
||||
→ Procesar comando/traducción
|
||||
→ Responder
|
||||
→ Actualizar DB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Conclusiones
|
||||
|
||||
El sistema está bien estructurado y cumple con los siguientes objetivos:
|
||||
|
||||
1. **Multiplataforma**: Soporta Discord y Telegram desde una sola interfaz
|
||||
2. **Flexible**: Permite programación simple o recurrente
|
||||
3. **Traducible**: Integración con LibreTranslate para automático y manual
|
||||
4. **Escalable**: Uso de colas y workers para procesamiento asíncrono
|
||||
5. **Administrable**: Panel completo para gestión de usuarios, destinatarios e idiomas
|
||||
6. **Integrado**: Conexión con n8n para automatización avanzada e IA
|
||||
|
||||
El código utiliza patrones modernos de PHP (PDO, Composer, Namespaces) y sigue una estructura modular que facilita el mantenimiento y expansión futura.
|
||||
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Last War - Sistema de Mensajería Multiplataforma
|
||||
|
||||
Sistema de mensajería automatizada para Discord y Telegram con traducción automática y asistente IA.
|
||||
|
||||
## Características
|
||||
|
||||
- **Discord Bot**: Envío de mensajes, traducción automática, comandos (#lista)
|
||||
- **Telegram Bot**: Webhook para mensajes, traducción con botones inline
|
||||
- **Traducción Automática**: LibreTranslate con detección de idioma
|
||||
- **Asistente IA**: Integración con Groq para respuestas inteligentes
|
||||
- **Panel de Administración**: Gestiona usuarios, mensajes, plantillas y configuración
|
||||
|
||||
## Requisitos
|
||||
|
||||
- PHP 8.3+
|
||||
- MySQL 8.0+
|
||||
- Composer
|
||||
- Servidor web (Apache/Nginx)
|
||||
|
||||
## Instalación
|
||||
|
||||
1. Clonar el repositorio
|
||||
2. Instalar dependencias: `composer install`
|
||||
3. Configurar `.env` con las variables de entorno
|
||||
4. Importar estructura de base de datos
|
||||
5. Configurar webhooks de Telegram y Discord
|
||||
|
||||
## Variables de Entorno
|
||||
|
||||
```env
|
||||
# Base de datos
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=lastwar
|
||||
DB_USER=root
|
||||
DB_PASS=
|
||||
|
||||
# Telegram
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
|
||||
# Discord
|
||||
DISCORD_BOT_TOKEN=
|
||||
|
||||
# LibreTranslate
|
||||
LIBRETRANSLATE_URL=http://localhost:5000
|
||||
|
||||
# IA (Groq)
|
||||
GROQ_API_KEY=
|
||||
|
||||
# Knowledge Base
|
||||
KB_DB_HOST=
|
||||
KB_DB_PORT=
|
||||
KB_DB_NAME=
|
||||
KB_DB_USER=
|
||||
KB_DB_PASS=
|
||||
```
|
||||
|
||||
## Comandos
|
||||
|
||||
### Telegram
|
||||
- `#lista` - Enviar plantilla de lista
|
||||
- `hola` - Mostrar botones de traducción
|
||||
|
||||
### Discord
|
||||
- `#lista` - Enviar plantilla de lista
|
||||
- `/comandos` - Ver comandos disponibles
|
||||
- `/agente` - Activar modo IA
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
├── admin/ # Panel de administración
|
||||
├── discord/ # Archivos de Discord
|
||||
├── includes/ # Funciones principales
|
||||
├── src/ # Clases (IA, Translate)
|
||||
├── telegram/ # Archivos de Telegram
|
||||
├── templates/ # Plantillas HTML
|
||||
├── logs/ # Logs del sistema
|
||||
└── *.php # Archivos principales
|
||||
```
|
||||
|
||||
## Licencia
|
||||
|
||||
MIT
|
||||
161
admin/comandos.php
Executable file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
$pageTitle = 'Gestión de Comandos';
|
||||
|
||||
$templates = [];
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
$stmt = $pdo->query("SELECT id, name, telegram_command FROM recurrent_messages ORDER BY name");
|
||||
$templates = $stmt->fetchAll();
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-terminal"></i> Gestión de Comandos</h2>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Los comandos se usan en Discord y Telegram anteponiendo <code>#</code> al nombre del comando.
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<?php if (empty($templates)): ?>
|
||||
<p class="text-muted text-center py-4">No hay plantillas con comandos</p>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Nombre</th>
|
||||
<th>Comando</th>
|
||||
<th>Uso</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($templates as $template): ?>
|
||||
<tr>
|
||||
<td><?= $template['id'] ?></td>
|
||||
<td><?= htmlspecialchars($template['name']) ?></td>
|
||||
<td>
|
||||
<?php if ($template['telegram_command']): ?>
|
||||
<code>#<?= htmlspecialchars($template['telegram_command']) ?></code>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">Sin comando</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary">Discord</span>
|
||||
<span class="badge bg-info">Telegram</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0"><i class="bi bi-discord"></i> Comandos de Discord</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Comando</th>
|
||||
<th>Descripción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>#comando</code></td>
|
||||
<td>Envía la plantilla asociada</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/comandos</code></td>
|
||||
<td>Lista de comandos disponibles</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/setlang [código]</code></td>
|
||||
<td>Establece el idioma del usuario</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/bienvenida</code></td>
|
||||
<td>Envía mensaje de bienvenida</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/agente</code></td>
|
||||
<td>Cambia a modo IA</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0"><i class="bi bi-telegram"></i> Comandos de Telegram</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Comando</th>
|
||||
<th>Descripción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>#comando</code></td>
|
||||
<td>Envía la plantilla asociada</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/start</code></td>
|
||||
<td>Inicia el bot</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/comandos</code></td>
|
||||
<td>Lista de comandos disponibles</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/setlang [código]</code></td>
|
||||
<td>Establece el idioma del usuario</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/bienvenida</code></td>
|
||||
<td>Envía mensaje de bienvenida</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/agente</code></td>
|
||||
<td>Cambia a modo IA</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
253
admin/ia_agent.php
Executable file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
$pageTitle = 'Configuración del Agente IA';
|
||||
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
|
||||
require_once __DIR__ . '/../src/IA/Agent.php';
|
||||
$agent = new \IA\Agent();
|
||||
|
||||
$message = '';
|
||||
$messageType = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['update_kb_config'])) {
|
||||
$kbConfigKeys = [
|
||||
'kb_db_host' => 'kb_host',
|
||||
'kb_db_port' => 'kb_port',
|
||||
'kb_db_name' => 'kb_dbname',
|
||||
'kb_db_user' => 'kb_user',
|
||||
];
|
||||
|
||||
$saved = true;
|
||||
foreach ($kbConfigKeys as $dbKey => $postKey) {
|
||||
$value = $_POST[$postKey] ?? '';
|
||||
if (!$agent->updateConfig($dbKey, $value)) {
|
||||
$saved = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($_POST['kb_pass'])) {
|
||||
if (!$agent->updateConfig('kb_db_pass', $_POST['kb_pass'])) {
|
||||
$saved = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($saved) {
|
||||
$message = 'Configuración de base de datos actualizada correctamente.';
|
||||
$messageType = 'success';
|
||||
$agent = new \IA\Agent();
|
||||
$config = $agent->getAllConfig();
|
||||
} else {
|
||||
$message = 'Error al guardar la configuración.';
|
||||
$messageType = 'danger';
|
||||
}
|
||||
} elseif (isset($_POST['update_config'])) {
|
||||
$key = $_POST['config_key'] ?? '';
|
||||
$value = $_POST['config_value'] ?? '';
|
||||
|
||||
if (!empty($key) && $agent->updateConfig($key, $value)) {
|
||||
$message = 'Configuración actualizada correctamente.';
|
||||
$messageType = 'success';
|
||||
$agent = new \IA\Agent();
|
||||
} else {
|
||||
$message = 'Error al actualizar la configuración.';
|
||||
$messageType = 'danger';
|
||||
}
|
||||
} elseif (isset($_POST['update_config_model']) && isset($_POST['config_value_model'])) {
|
||||
$key = 'ai_model';
|
||||
$value = $_POST['config_value_model'] ?? '';
|
||||
|
||||
if (!empty($value) && $agent->updateConfig($key, $value)) {
|
||||
$message = 'Modelo de IA actualizado correctamente.';
|
||||
$messageType = 'success';
|
||||
$agent = new \IA\Agent();
|
||||
} else {
|
||||
$message = 'Error al actualizar el modelo.';
|
||||
$messageType = 'danger';
|
||||
}
|
||||
} elseif (isset($_POST['test_connection'])) {
|
||||
$result = $agent->testKbConnection();
|
||||
$message = $result['message'];
|
||||
$messageType = $result['success'] ? 'success' : 'danger';
|
||||
} elseif (isset($_POST['test_question'])) {
|
||||
$question = $_POST['test_question'] ?? '';
|
||||
if (!empty($question)) {
|
||||
$response = $agent->generateResponse($question);
|
||||
$testResponse = $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$config = $agent->getAllConfig();
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-robot"></i> Configuración del Agente IA</h2>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($message)): ?>
|
||||
<div class="alert alert-<?= $messageType ?> alert-dismissible fade show" role="alert">
|
||||
<?= htmlspecialchars($message) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0"><i class="bi bi-database"></i> Conexión a Knowledge Base</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Host</label>
|
||||
<input type="text" name="kb_host" class="form-control" value="<?= htmlspecialchars($config['kb_db_host'] ?? $_ENV['KB_DB_HOST'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Puerto</label>
|
||||
<input type="text" name="kb_port" class="form-control" value="<?= htmlspecialchars($config['kb_db_port'] ?? $_ENV['KB_DB_PORT'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Base de Datos</label>
|
||||
<input type="text" name="kb_dbname" class="form-control" value="<?= htmlspecialchars($config['kb_db_name'] ?? $_ENV['KB_DB_NAME'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Usuario</label>
|
||||
<input type="text" name="kb_user" class="form-control" value="<?= htmlspecialchars($config['kb_db_user'] ?? $_ENV['KB_DB_USER'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contraseña</label>
|
||||
<input type="password" name="kb_pass" class="form-control" value="<?= htmlspecialchars($config['kb_db_pass'] ?? '') ?>" placeholder="Dejar vacío para mantener actual">
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button type="submit" name="update_kb_config" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Guardar
|
||||
</button>
|
||||
<button type="submit" name="test_connection" class="btn btn-outline-primary">
|
||||
<i class="bi bi-plug"></i> Probar Conexión
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0"><i class="bi bi-cpu"></i> Configuración de IA</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Habilitar Knowledge Base</label>
|
||||
<select name="config_value" class="form-select">
|
||||
<option value="1" <?= ($config['kb_enabled'] ?? '1') === '1' ? 'selected' : '' ?>>Habilitado</option>
|
||||
<option value="0" <?= ($config['kb_enabled'] ?? '1') === '0' ? 'selected' : '' ?>>Deshabilitado</option>
|
||||
</select>
|
||||
<input type="hidden" name="config_key" value="kb_enabled">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Modelo de IA (Groq)</label>
|
||||
<select name="config_value_model" class="form-select">
|
||||
<option value="llama-3.1-8b-instant" <?= ($config['ai_model'] ?? 'llama-3.1-8b-instant') === 'llama-3.1-8b-instant' ? 'selected' : '' ?>>Llama 3.1 8B (Rápido - Recomendado)</option>
|
||||
<option value="mixtral-8x7b-32768" <?= ($config['ai_model'] ?? '') === 'mixtral-8x7b-32768' ? 'selected' : '' ?>>Mixtral 8x7B</option>
|
||||
<option value="llama3-70b-8192" <?= ($config['ai_model'] ?? '') === 'llama3-70b-8192' ? 'selected' : '' ?>>Llama 3 70B</option>
|
||||
</select>
|
||||
<small class="text-muted">Solo modelos gratuitos. Si se agotan los tokens, cambiará automáticamente al siguiente.</small>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" name="update_config" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Guardar Configuración
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0"><i class="bi bi-chat-left-text"></i> Prompt del Sistema</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Instrucciones para el agente</label>
|
||||
<textarea name="config_value" class="form-control" rows="6"><?= htmlspecialchars($config['system_prompt'] ?? '') ?></textarea>
|
||||
<small class="text-muted">Instrucciones que seguirá el agente al responder.</small>
|
||||
<input type="hidden" name="config_key" value="system_prompt">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" name="update_config" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Guardar Prompt
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0"><i class="bi bi-sliders"></i> Parámetros Adicionales</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Máximo de resultados de KB</label>
|
||||
<input type="number" name="config_value" class="form-control" value="<?= htmlspecialchars($config['kb_max_results'] ?? '5') ?>" min="1" max="20">
|
||||
<small class="text-muted">Cantidad de artículos a buscar en la base de conocimientos.</small>
|
||||
<input type="hidden" name="config_key" value="kb_max_results">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Máximo de caracteres en respuesta</label>
|
||||
<input type="number" name="config_value2" class="form-control" value="<?= htmlspecialchars($config['response_max_length'] ?? '1500') ?>" min="100" max="4000">
|
||||
<small class="text-muted">Límite de caracteres en las respuestas del agente.</small>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" name="update_config" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Guardar Parámetros
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0"><i class="bi bi-chat-dots"></i> Prueba del Agente</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-question-circle"></i></span>
|
||||
<input type="text" name="test_question" class="form-control" placeholder="Escribe una pregunta para probar el agente..." value="<?= htmlspecialchars($_POST['test_question'] ?? '') ?>">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-send"></i> Enviar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if (isset($testResponse)): ?>
|
||||
<div class="mt-3">
|
||||
<label class="form-label text-muted">Respuesta del agente:</label>
|
||||
<div class="p-3 bg-light rounded border">
|
||||
<pre class="mb-0 white-space: pre-wrap;"><?= htmlspecialchars($testResponse) ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
465
admin/languages.php
Executable file
@@ -0,0 +1,465 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/activity_logger.php';
|
||||
require_once __DIR__ . '/../includes/env_loader.php';
|
||||
require_once __DIR__ . '/../src/Translate.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
$pageTitle = 'Gestión de Idiomas';
|
||||
|
||||
$languages = [];
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
$stmt = $pdo->query("SELECT * FROM supported_languages ORDER BY language_name");
|
||||
$languages = $stmt->fetchAll();
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
$syncMessage = '';
|
||||
$syncError = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'toggle_status') {
|
||||
$id = (int) $_POST['id'];
|
||||
$stmt = $pdo->prepare("UPDATE supported_languages SET is_active = NOT is_active WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
logActivity(getCurrentUserId(), 'toggle_language', "Idioma ID: $id");
|
||||
header('Location: languages.php');
|
||||
exit;
|
||||
|
||||
} elseif ($action === 'update_flag') {
|
||||
$id = (int) $_POST['id'];
|
||||
$flag = $_POST['flag_emoji'];
|
||||
$stmt = $pdo->prepare("UPDATE supported_languages SET flag_emoji = ? WHERE id = ?");
|
||||
$stmt->execute([$flag, $id]);
|
||||
header('Location: languages.php');
|
||||
exit;
|
||||
|
||||
} elseif ($action === 'add') {
|
||||
$code = $_POST['language_code'];
|
||||
$name = $_POST['language_name'];
|
||||
$flag = $_POST['flag_emoji'];
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO supported_languages (language_code, language_name, flag_emoji, is_active) VALUES (?, ?, ?, FALSE)");
|
||||
$stmt->execute([$code, $name, $flag]);
|
||||
logActivity(getCurrentUserId(), 'add_language', "Idioma agregado: $name");
|
||||
header('Location: languages.php');
|
||||
exit;
|
||||
|
||||
} elseif ($action === 'sync_libretranslate') {
|
||||
try {
|
||||
$translator = new src\Translate();
|
||||
$ltLanguages = $translator->getSupportedLanguages();
|
||||
|
||||
if (empty($ltLanguages)) {
|
||||
$syncError = "No se pudieron obtener los idiomas de LibreTranslate. Verifica que el servicio esté corriendo en: " . ($_ENV['LIBRETRANSLATE_URL'] ?? 'http://localhost:5000');
|
||||
} else {
|
||||
$added = 0;
|
||||
foreach ($ltLanguages as $ltLang) {
|
||||
$code = $ltLang['code'];
|
||||
$name = $ltLang['name'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id FROM supported_languages WHERE language_code = ?");
|
||||
$stmt->execute([$code]);
|
||||
|
||||
if (!$stmt->fetch()) {
|
||||
$flag = getFlagForLanguage($code);
|
||||
$stmt = $pdo->prepare("INSERT INTO supported_languages (language_code, language_name, flag_emoji, is_active) VALUES (?, ?, ?, FALSE)");
|
||||
$stmt->execute([$code, $name, $flag]);
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
|
||||
$syncMessage = "Se sincronizaron $added idiomas desde LibreTranslate";
|
||||
logActivity(getCurrentUserId(), 'sync_languages', "Sincronizados $added idiomas desde LibreTranslate");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$syncError = "Error al conectar con LibreTranslate: " . $e->getMessage() . ". Verifica que el servicio esté configurado correctamente en el archivo .env";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Array completo de banderas con variantes regionales
|
||||
$availableFlags = [
|
||||
// Español - Variantes
|
||||
['code' => 'es-MX', 'flag' => '🇲🇽', 'name' => 'Español (México)'],
|
||||
['code' => 'es-ES', 'flag' => '🇪🇸', 'name' => 'Español (España)'],
|
||||
['code' => 'es-AR', 'flag' => '🇦🇷', 'name' => 'Español (Argentina)'],
|
||||
['code' => 'es-CO', 'flag' => '🇨🇴', 'name' => 'Español (Colombia)'],
|
||||
['code' => 'es-CL', 'flag' => '🇨🇱', 'name' => 'Español (Chile)'],
|
||||
['code' => 'es-PE', 'flag' => '🇵🇪', 'name' => 'Español (Perú)'],
|
||||
['code' => 'es-VE', 'flag' => '🇻🇪', 'name' => 'Español (Venezuela)'],
|
||||
|
||||
// Inglés - Variantes
|
||||
['code' => 'en-US', 'flag' => '🇺🇸', 'name' => 'English (USA)'],
|
||||
['code' => 'en-GB', 'flag' => '🇬🇧', 'name' => 'English (UK)'],
|
||||
['code' => 'en-CA', 'flag' => '🇨🇦', 'name' => 'English (Canada)'],
|
||||
['code' => 'en-AU', 'flag' => '🇦🇺', 'name' => 'English (Australia)'],
|
||||
|
||||
// Portugués - Variantes
|
||||
['code' => 'pt-BR', 'flag' => '🇧🇷', 'name' => 'Português (Brasil)'],
|
||||
['code' => 'pt-PT', 'flag' => '🇵🇹', 'name' => 'Português (Portugal)'],
|
||||
|
||||
// Francés - Variantes
|
||||
['code' => 'fr-FR', 'flag' => '🇫🇷', 'name' => 'Français (France)'],
|
||||
['code' => 'fr-CA', 'flag' => '🇨🇦', 'name' => 'Français (Canada)'],
|
||||
|
||||
// Alemán
|
||||
['code' => 'de', 'flag' => '🇩🇪', 'name' => 'Deutsch'],
|
||||
|
||||
// Italiano
|
||||
['code' => 'it', 'flag' => '🇮🇹', 'name' => 'Italiano'],
|
||||
|
||||
// Ruso
|
||||
['code' => 'ru', 'flag' => '🇷🇺', 'name' => 'Русский'],
|
||||
|
||||
// Chino - Variantes
|
||||
['code' => 'zh-CN', 'flag' => '🇨🇳', 'name' => '中文 (简体)'],
|
||||
['code' => 'zh-TW', 'flag' => '🇹🇼', 'name' => '中文 (繁體)'],
|
||||
|
||||
// Japonés
|
||||
['code' => 'ja', 'flag' => '🇯🇵', 'name' => '日本語'],
|
||||
|
||||
// Coreano
|
||||
['code' => 'ko', 'flag' => '🇰🇷', 'name' => '한국어'],
|
||||
|
||||
// Árabe
|
||||
['code' => 'ar', 'flag' => '🇸🇦', 'name' => 'العربية'],
|
||||
|
||||
// Hindi
|
||||
['code' => 'hi', 'flag' => '🇮🇳', 'name' => 'हिन्दी'],
|
||||
|
||||
// Holandés
|
||||
['code' => 'nl', 'flag' => '🇳🇱', 'name' => 'Nederlands'],
|
||||
|
||||
// Polaco
|
||||
['code' => 'pl', 'flag' => '🇵🇱', 'name' => 'Polski'],
|
||||
|
||||
// Turco
|
||||
['code' => 'tr', 'flag' => '🇹🇷', 'name' => 'Türkçe'],
|
||||
|
||||
// Sueco
|
||||
['code' => 'sv', 'flag' => '🇸🇪', 'name' => 'Svenska'],
|
||||
|
||||
// Danés
|
||||
['code' => 'da', 'flag' => '🇩🇰', 'name' => 'Dansk'],
|
||||
|
||||
// Finés
|
||||
['code' => 'fi', 'flag' => '🇫🇮', 'name' => 'Suomi'],
|
||||
|
||||
// Noruego
|
||||
['code' => 'no', 'flag' => '🇳🇴', 'name' => 'Norsk'],
|
||||
|
||||
// Checo
|
||||
['code' => 'cs', 'flag' => '🇨🇿', 'name' => 'Čeština'],
|
||||
|
||||
// Griego
|
||||
['code' => 'el', 'flag' => '🇬🇷', 'name' => 'Ελληνικά'],
|
||||
|
||||
// Hebreo
|
||||
['code' => 'he', 'flag' => '🇮🇱', 'name' => 'עברית'],
|
||||
|
||||
// Tailandés
|
||||
['code' => 'th', 'flag' => '🇹🇭', 'name' => 'ไทย'],
|
||||
|
||||
// Vietnamita
|
||||
['code' => 'vi', 'flag' => '🇻🇳', 'name' => 'Tiếng Việt'],
|
||||
|
||||
// Indonesio
|
||||
['code' => 'id', 'flag' => '🇮🇩', 'name' => 'Bahasa Indonesia'],
|
||||
|
||||
// Malayo
|
||||
['code' => 'ms', 'flag' => '🇲🇾', 'name' => 'Bahasa Melayu'],
|
||||
|
||||
// Ucraniano
|
||||
['code' => 'uk', 'flag' => '🇺🇦', 'name' => 'Українська'],
|
||||
|
||||
// Catalán
|
||||
['code' => 'ca', 'flag' => '🇪🇸', 'name' => 'Català'],
|
||||
|
||||
// Gallego
|
||||
['code' => 'gl', 'flag' => '🇪🇸', 'name' => 'Galego'],
|
||||
|
||||
// Rumano
|
||||
['code' => 'ro', 'flag' => '🇷🇴', 'name' => 'Română'],
|
||||
|
||||
// Húngaro
|
||||
['code' => 'hu', 'flag' => '🇭🇺', 'name' => 'Magyar'],
|
||||
|
||||
// Búlgaro
|
||||
['code' => 'bg', 'flag' => '🇧🇬', 'name' => 'Български'],
|
||||
|
||||
// Otros países importantes
|
||||
['code' => 'other', 'flag' => '🇦🇹', 'name' => 'Austria'],
|
||||
['code' => 'other', 'flag' => '🇧🇪', 'name' => 'Bélgica'],
|
||||
['code' => 'other', 'flag' => '🇨🇭', 'name' => 'Suiza'],
|
||||
['code' => 'other', 'flag' => '🇮🇪', 'name' => 'Irlanda'],
|
||||
['code' => 'other', 'flag' => '🇳🇿', 'name' => 'Nueva Zelanda'],
|
||||
['code' => 'other', 'flag' => '🇿🇦', 'name' => 'Sudáfrica'],
|
||||
['code' => 'other', 'flag' => '🇪🇬', 'name' => 'Egipto'],
|
||||
['code' => 'other', 'flag' => '🇮🇷', 'name' => 'Irán'],
|
||||
['code' => 'other', 'flag' => '🇵🇰', 'name' => 'Pakistán'],
|
||||
['code' => 'other', 'flag' => '🇧🇩', 'name' => 'Bangladesh'],
|
||||
['code' => 'other', 'flag' => '🇵🇭', 'name' => 'Filipinas'],
|
||||
['code' => 'other', 'flag' => '🇸🇬', 'name' => 'Singapur'],
|
||||
['code' => 'other', 'flag' => '🇭🇰', 'name' => 'Hong Kong'],
|
||||
['code' => 'other', 'flag' => '🇲🇴', 'name' => 'Macao'],
|
||||
];
|
||||
|
||||
function getFlagForLanguage(string $code): string {
|
||||
// Por defecto usar México para español
|
||||
$flags = [
|
||||
'en' => '🇺🇸', // USA para inglés general
|
||||
'es' => '🇲🇽', // México para español (como pidió el usuario)
|
||||
'pt' => '🇧🇷', // Brasil para portugués
|
||||
'fr' => '🇫🇷', // Francia
|
||||
'de' => '🇩🇪', // Alemania
|
||||
'it' => '🇮🇹', // Italia
|
||||
'ru' => '🇷🇺', // Rusia
|
||||
'zh' => '🇨🇳', // China
|
||||
'ja' => '🇯🇵', // Japón
|
||||
'ko' => '🇰🇷', // Corea del Sur
|
||||
'ar' => '🇸🇦', // Arabia Saudita
|
||||
'hi' => '🇮🇳', // India
|
||||
'nl' => '🇳🇱', // Países Bajos
|
||||
'pl' => '🇵🇱', // Polonia
|
||||
'tr' => '🇹🇷', // Turquía
|
||||
'sv' => '🇸🇪', // Suecia
|
||||
'da' => '🇩🇰', // Dinamarca
|
||||
'fi' => '🇫🇮', // Finlandia
|
||||
'no' => '🇳🇴', // Noruega
|
||||
'cs' => '🇨🇿', // República Checa
|
||||
'el' => '🇬🇷', // Grecia
|
||||
'he' => '🇮🇱', // Israel
|
||||
'th' => '🇹🇭', // Tailandia
|
||||
'vi' => '🇻🇳', // Vietnam
|
||||
'id' => '🇮🇩', // Indonesia
|
||||
'ms' => '🇲🇾', // Malasia
|
||||
'uk' => '🇺🇦', // Ucrania
|
||||
'ca' => '🇪🇸', // España (Cataluña)
|
||||
'gl' => '🇪🇸', // España (Galicia)
|
||||
'ro' => '🇷🇴', // Rumania
|
||||
'hu' => '🇭🇺', // Hungría
|
||||
'bg' => '🇧🇬', // Bulgaria
|
||||
];
|
||||
return $flags[$code] ?? '🌐';
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-translate"></i> Gestión de Idiomas</h2>
|
||||
<div>
|
||||
<form method="POST" class="d-inline">
|
||||
<input type="hidden" name="action" value="sync_libretranslate">
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
<i class="bi bi-cloud-download"></i> Sincronizar con LibreTranslate
|
||||
</button>
|
||||
</form>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#languageModal">
|
||||
<i class="bi bi-plus-circle"></i> Nuevo Idioma
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($syncMessage): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($syncMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($syncError): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($syncError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bandera</th>
|
||||
<th>Código</th>
|
||||
<th>Nombre</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($languages as $lang): ?>
|
||||
<tr>
|
||||
<td style="font-size: 1.5rem;"><?= htmlspecialchars($lang['flag_emoji']) ?></td>
|
||||
<td><code><?= htmlspecialchars($lang['language_code']) ?></code></td>
|
||||
<td><?= htmlspecialchars($lang['language_name']) ?></td>
|
||||
<td>
|
||||
<?php if ($lang['is_active']): ?>
|
||||
<span class="badge bg-success">Activo</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">Inactivo</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" class="d-inline">
|
||||
<input type="hidden" name="action" value="toggle_status">
|
||||
<input type="hidden" name="id" value="<?= $lang['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-<?= $lang['is_active'] ? 'warning' : 'success' ?>">
|
||||
<i class="bi bi-<?= $lang['is_active'] ? 'pause' : 'play' ?>-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#flagModal<?= $lang['id'] ?>">
|
||||
<i class="bi bi-flag"></i> Cambiar
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Modal Selector de Banderas -->
|
||||
<div class="modal fade" id="flagModal<?= $lang['id'] ?>" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="update_flag">
|
||||
<input type="hidden" name="id" value="<?= $lang['id'] ?>">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Seleccionar Bandera - <?= htmlspecialchars($lang['language_name']) ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Bandera actual</label>
|
||||
<div class="display-4"><?= htmlspecialchars($lang['flag_emoji']) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Seleccionar nueva bandera</label>
|
||||
<div class="flag-selector" style="max-height: 400px; overflow-y: auto;">
|
||||
<div class="row g-2">
|
||||
<?php foreach ($availableFlags as $flag): ?>
|
||||
<div class="col-6 col-md-4 col-lg-3">
|
||||
<label class="d-block p-2 border rounded cursor-pointer text-center" style="cursor: pointer; <?= $flag['flag'] === $lang['flag_emoji'] ? 'background-color: #e3f2fd; border-color: #2196f3;' : '' ?>" onclick="selectFlag<?= $lang['id'] ?>('<?= $flag['flag'] ?>')">
|
||||
<input type="radio" name="flag_emoji" value="<?= $flag['flag'] ?>" class="d-none" <?= $flag['flag'] === $lang['flag_emoji'] ? 'checked' : '' ?>>
|
||||
<span style="font-size: 2rem;"><?= $flag['flag'] ?></span>
|
||||
<div class="small text-muted mt-1"><?= htmlspecialchars($flag['name']) ?></div>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="form-label">O escribir emoji manualmente</label>
|
||||
<input type="text" name="flag_emoji_custom" id="customFlag<?= $lang['id'] ?>" class="form-control" value="<?= htmlspecialchars($lang['flag_emoji']) ?>" maxlength="10" placeholder="🇲🇽">
|
||||
<small class="text-muted">Puedes copiar y pegar cualquier emoji de bandera aquí</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function selectFlag<?= $lang['id'] ?>(flag) {
|
||||
document.getElementById('customFlag<?= $lang['id'] ?>').value = flag;
|
||||
}
|
||||
</script>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Nuevo Idioma -->
|
||||
<div class="modal fade" id="languageModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Nuevo Idioma</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Código de idioma (ej: ca, gl)</label>
|
||||
<input type="text" name="language_code" class="form-control" required maxlength="10">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre del idioma</label>
|
||||
<input type="text" name="language_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Seleccionar bandera</label>
|
||||
<div class="flag-selector-new" style="max-height: 300px; overflow-y: auto;">
|
||||
<div class="row g-2">
|
||||
<?php foreach ($availableFlags as $flag): ?>
|
||||
<div class="col-4">
|
||||
<label class="d-block p-2 border rounded cursor-pointer text-center" style="cursor: pointer;" onclick="selectNewFlag('<?= $flag['flag'] ?>')">
|
||||
<input type="radio" name="flag_emoji" value="<?= $flag['flag'] ?>" class="d-none">
|
||||
<span style="font-size: 1.5rem;"><?= $flag['flag'] ?></span>
|
||||
<div class="small text-muted" style="font-size: 0.75rem;"><?= htmlspecialchars($flag['name']) ?></div>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="form-label">O escribir emoji manualmente</label>
|
||||
<input type="text" id="newFlagInput" name="flag_emoji" class="form-control" maxlength="10" placeholder="🇲🇽">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">Agregar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function selectNewFlag(flag) {
|
||||
document.getElementById('newFlagInput').value = flag;
|
||||
// Marcar visualmente la selección
|
||||
document.querySelectorAll('.flag-selector-new label').forEach(function(label) {
|
||||
label.style.backgroundColor = '';
|
||||
label.style.borderColor = '';
|
||||
});
|
||||
event.currentTarget.style.backgroundColor = '#e3f2fd';
|
||||
event.currentTarget.style.borderColor = '#2196f3';
|
||||
}
|
||||
|
||||
// Estilo hover para las banderas
|
||||
document.querySelectorAll('.flag-selector label, .flag-selector-new label').forEach(function(label) {
|
||||
label.addEventListener('mouseenter', function() {
|
||||
if (!this.querySelector('input:checked')) {
|
||||
this.style.backgroundColor = '#f5f5f5';
|
||||
}
|
||||
});
|
||||
label.addEventListener('mouseleave', function() {
|
||||
if (!this.querySelector('input:checked')) {
|
||||
this.style.backgroundColor = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
273
admin/recipients.php
Executable file
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/activity_logger.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
$pageTitle = 'Gestión de Destinatarios';
|
||||
|
||||
$recipients = [];
|
||||
$languages = [];
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM recipients ORDER BY platform, name");
|
||||
$recipients = $stmt->fetchAll();
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM supported_languages ORDER BY language_name");
|
||||
$languages = $stmt->fetchAll();
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'add') {
|
||||
$platformId = $_POST['platform_id'];
|
||||
$name = $_POST['name'];
|
||||
$type = $_POST['type'];
|
||||
$platform = $_POST['platform'];
|
||||
$languageCode = $_POST['language_code'];
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO recipients (platform_id, name, type, platform, language_code, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
$stmt->execute([$platformId, $name, $type, $platform, $languageCode]);
|
||||
|
||||
logActivity(getCurrentUserId(), 'add_recipient', "Destinatario agregado: $name ($platform)");
|
||||
header('Location: recipients.php');
|
||||
exit;
|
||||
|
||||
} elseif ($action === 'update_language') {
|
||||
$recipientId = (int) $_POST['recipient_id'];
|
||||
$languageCode = $_POST['language_code'];
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET language_code = ? WHERE id = ?");
|
||||
$stmt->execute([$languageCode, $recipientId]);
|
||||
|
||||
header('Location: recipients.php');
|
||||
exit;
|
||||
|
||||
} elseif ($action === 'delete') {
|
||||
$recipientId = (int) $_POST['recipient_id'];
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM recipients WHERE id = ?");
|
||||
$stmt->execute([$recipientId]);
|
||||
|
||||
logActivity(getCurrentUserId(), 'delete_recipient', "Destinatario eliminado ID: $recipientId");
|
||||
header('Location: recipients.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-person-check"></i> Gestión de Destinatarios</h2>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addRecipientModal">
|
||||
<i class="bi bi-plus-circle"></i> Nuevo Destinatario
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#discord" data-bs-toggle="tab">Discord</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#telegram" data-bs-toggle="tab">Telegram</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="discord">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<?php $discordRecipients = array_filter($recipients, fn($r) => $r['platform'] === 'discord'); ?>
|
||||
<?php if (empty($discordRecipients)): ?>
|
||||
<p class="text-muted">No hay destinatarios de Discord</p>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Platform ID</th>
|
||||
<th>Nombre</th>
|
||||
<th>Tipo</th>
|
||||
<th>Idioma</th>
|
||||
<th>Creado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($discordRecipients as $recipient): ?>
|
||||
<tr>
|
||||
<td><?= $recipient['id'] ?></td>
|
||||
<td><code><?= $recipient['platform_id'] ?></code></td>
|
||||
<td><?= htmlspecialchars($recipient['name']) ?></td>
|
||||
<td><?= $recipient['type'] ?></td>
|
||||
<td>
|
||||
<form method="POST" class="d-inline">
|
||||
<input type="hidden" name="action" value="update_language">
|
||||
<input type="hidden" name="recipient_id" value="<?= $recipient['id'] ?>">
|
||||
<select name="language_code" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">
|
||||
<?php foreach ($languages as $lang): ?>
|
||||
<option value="<?= $lang['language_code'] ?>" <?= $recipient['language_code'] === $lang['language_code'] ? 'selected' : '' ?>>
|
||||
<?= $lang['flag_emoji'] ?> <?= $lang['language_name'] ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
</td>
|
||||
<td><?= date('d/m/Y', strtotime($recipient['created_at'])) ?></td>
|
||||
<td>
|
||||
<form method="POST" onsubmit="return confirm('¿Eliminar?');" class="d-inline">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="recipient_id" value="<?= $recipient['id'] ?>">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="telegram">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<?php $telegramRecipients = array_filter($recipients, fn($r) => $r['platform'] === 'telegram'); ?>
|
||||
<?php if (empty($telegramRecipients)): ?>
|
||||
<p class="text-muted">No hay destinatarios de Telegram</p>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Platform ID</th>
|
||||
<th>Nombre</th>
|
||||
<th>Tipo</th>
|
||||
<th>Idioma</th>
|
||||
<th>Creado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($telegramRecipients as $recipient): ?>
|
||||
<tr>
|
||||
<td><?= $recipient['id'] ?></td>
|
||||
<td><code><?= $recipient['platform_id'] ?></code></td>
|
||||
<td><?= htmlspecialchars($recipient['name']) ?></td>
|
||||
<td><?= $recipient['type'] ?></td>
|
||||
<td>
|
||||
<form method="POST" class="d-inline">
|
||||
<input type="hidden" name="action" value="update_language">
|
||||
<input type="hidden" name="recipient_id" value="<?= $recipient['id'] ?>">
|
||||
<select name="language_code" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">
|
||||
<?php foreach ($languages as $lang): ?>
|
||||
<option value="<?= $lang['language_code'] ?>" <?= $recipient['language_code'] === $lang['language_code'] ? 'selected' : '' ?>>
|
||||
<?= $lang['flag_emoji'] ?> <?= $lang['language_name'] ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
</td>
|
||||
<td><?= date('d/m/Y', strtotime($recipient['created_at'])) ?></td>
|
||||
<td>
|
||||
<form method="POST" onsubmit="return confirm('¿Eliminar?');" class="d-inline">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="recipient_id" value="<?= $recipient['id'] ?>">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para agregar destinatario -->
|
||||
<div class="modal fade" id="addRecipientModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Nuevo Destinatario</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plataforma</label>
|
||||
<select name="platform" class="form-select" required>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ID en la plataforma</label>
|
||||
<input type="text" name="platform_id" class="form-control" required placeholder="Ej: 123456789">
|
||||
<small class="text-muted">ID del canal/usuario en Discord o Telegram</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tipo</label>
|
||||
<select name="type" class="form-select" required>
|
||||
<option value="channel">Canal</option>
|
||||
<option value="user">Usuario</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Idioma</label>
|
||||
<select name="language_code" class="form-select">
|
||||
<?php foreach ($languages as $lang): ?>
|
||||
<option value="<?= $lang['language_code'] ?>">
|
||||
<?= $lang['flag_emoji'] ?> <?= $lang['language_name'] ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Agregar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
178
admin/test_discord_connection.php
Executable file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/env_loader.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
$pageTitle = 'Test de Conexión Discord';
|
||||
|
||||
$results = [];
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'test') {
|
||||
$token = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||
|
||||
if (empty($token)) {
|
||||
$error = 'Token de bot no configurado';
|
||||
} else {
|
||||
$ch = curl_init('https://discord.com/api/v10/users/@me');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bot ' . $token]);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$userData = json_decode($response, true);
|
||||
|
||||
$results['user'] = $userData;
|
||||
$results['http_code'] = $httpCode;
|
||||
|
||||
if ($httpCode === 200) {
|
||||
$guildId = $_ENV['DISCORD_GUILD_ID'] ?? getenv('DISCORD_GUILD_ID');
|
||||
|
||||
if ($guildId) {
|
||||
$ch = curl_init("https://discord.com/api/v10/guilds/{$guildId}");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bot ' . $token]);
|
||||
$guildResponse = curl_exec($ch);
|
||||
$guildCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$results['guild'] = json_decode($guildResponse, true);
|
||||
$results['guild_code'] = $guildCode;
|
||||
}
|
||||
|
||||
$ch = curl_init('https://discord.com/api/v10/channels');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bot ' . $token]);
|
||||
$channelsResponse = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$results['channels'] = json_decode($channelsResponse, true);
|
||||
}
|
||||
|
||||
if ($_POST['test_message'] ?? false) {
|
||||
$channelId = $_POST['test_channel_id'] ?? '';
|
||||
|
||||
if ($channelId) {
|
||||
$testMessage = $_POST['test_message_text'] ?? '✅ Prueba de conexión desde el sistema de mensajería';
|
||||
|
||||
$ch = curl_init("https://discord.com/api/v10/channels/{$channelId}/messages");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $token,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['content' => $testMessage]));
|
||||
$msgResponse = curl_exec($ch);
|
||||
$msgCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$results['test_message'] = json_decode($msgResponse, true);
|
||||
$results['test_message_code'] = $msgCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-discord"></i> Test de Conexión Discord</h2>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($results)): ?>
|
||||
<?php if ($results['http_code'] === 200): ?>
|
||||
<div class="alert alert-success">
|
||||
<i class="bi bi-check-circle"></i> <strong>Conexión exitosa!</strong> El bot está conectado como <strong><?= htmlspecialchars($results['user']['username']) ?></strong>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-x-circle"></i> <strong>Error de conexión:</strong> Código HTTP <?= $results['http_code'] ?>
|
||||
<?php if (isset($results['user']['message'])): ?>
|
||||
<br><?= htmlspecialchars($results['user']['message']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($results['guild'])): ?>
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Servidor</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($results['guild_code'] === 200): ?>
|
||||
<p><strong>Nombre:</strong> <?= htmlspecialchars($results['guild']['name']) ?></p>
|
||||
<p><strong>ID:</strong> <?= htmlspecialchars($results['guild']['id']) ?></p>
|
||||
<p><strong>Miembros:</strong> <?= $results['guild']['approximate_member_count'] ?? 'N/A' ?></p>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning">Error al obtener servidor: Código <?= $results['guild_code'] ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($results['test_message'])): ?>
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Prueba de Envío</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($results['test_message_code'] === 200): ?>
|
||||
<div class="alert alert-success"><i class="bi bi-check-circle"></i> Mensaje enviado correctamente</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-danger">Error al enviar mensaje: Código <?= $results['test_message_code'] ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Probar Conexión</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="test">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-plug"></i> Verificar Conexión
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Enviar Mensaje de Prueba</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="test">
|
||||
<input type="hidden" name="test_message" value="1">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ID del Canal</label>
|
||||
<input type="text" name="test_channel_id" class="form-control" placeholder="123456789012345678">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mensaje</label>
|
||||
<textarea name="test_message_text" class="form-control" rows="3">✅ Prueba de conexión desde el sistema de mensajería</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-send"></i> Enviar Mensaje
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
137
admin/users.php
Executable file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
$pageTitle = 'Gestión de Usuarios';
|
||||
|
||||
$users = getAllUsers();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
$role = $_POST['role'] ?? 'user';
|
||||
|
||||
if ($username && $password) {
|
||||
$userId = registerUser($username, $password, $role);
|
||||
if ($userId) {
|
||||
logActivity(getCurrentUserId(), 'create_user', "Usuario creado: $username");
|
||||
header('Location: users.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'El usuario ya existe';
|
||||
}
|
||||
}
|
||||
|
||||
} elseif ($action === 'delete') {
|
||||
$userId = (int) $_POST['user_id'];
|
||||
if ($userId !== getCurrentUserId()) {
|
||||
deleteUser($userId);
|
||||
logActivity(getCurrentUserId(), 'delete_user', "Usuario eliminado ID: $userId");
|
||||
header('Location: users.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'No puedes eliminarte a ti mismo';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-people"></i> Gestión de Usuarios</h2>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#userModal">
|
||||
<i class="bi bi-plus-circle"></i> Nuevo Usuario
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Usuario</th>
|
||||
<th>Rol</th>
|
||||
<th>Telegram</th>
|
||||
<th>Creado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr>
|
||||
<td><?= $user['id'] ?></td>
|
||||
<td><?= htmlspecialchars($user['username']) ?></td>
|
||||
<td>
|
||||
<span class="badge bg-<?= $user['role'] === 'admin' ? 'danger' : 'primary' ?>">
|
||||
<?= $user['role'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= $user['telegram_chat_id'] ? htmlspecialchars($user['telegram_chat_id']) : '-' ?></td>
|
||||
<td><?= date('d/m/Y', strtotime($user['created_at'])) ?></td>
|
||||
<td>
|
||||
<?php if ($user['id'] !== getCurrentUserId()): ?>
|
||||
<form method="POST" onsubmit="return confirm('¿Eliminar este usuario?');" class="d-inline">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="user_id" value="<?= $user['id'] ?>">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="userModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Nuevo Usuario</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Usuario</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contraseña</label>
|
||||
<input type="password" name="password" class="form-control" required minlength="6">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Rol</label>
|
||||
<select name="role" class="form-select">
|
||||
<option value="user">Usuario</option>
|
||||
<option value="admin">Administrador</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">Crear</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
357
admin_send_message.php
Executable file
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/includes/message_handler.php';
|
||||
require_once __DIR__ . '/common/helpers/sender_factory.php';
|
||||
require_once __DIR__ . '/common/helpers/converter_factory.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
function getTranslationButtons(PDO $pdo, string $text): array
|
||||
{
|
||||
$stmt = $pdo->query("SELECT language_code, flag_emoji FROM supported_languages WHERE is_active = 1");
|
||||
$languages = $stmt->fetchAll();
|
||||
|
||||
if (count($languages) <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'telegram' => buildTelegramTranslationButtons($pdo, $languages, $text),
|
||||
'discord' => buildDiscordTranslationButtons($languages, $text)
|
||||
];
|
||||
}
|
||||
|
||||
function buildTelegramTranslationButtons(PDO $pdo, array $languages, string $text): array
|
||||
{
|
||||
if (count($languages) <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Guardar texto en la base de datos con hash consistente
|
||||
$textHash = md5($text);
|
||||
$stmt = $pdo->prepare("INSERT INTO translation_cache (text_hash, original_text) VALUES (?, ?) ON DUPLICATE KEY UPDATE original_text = VALUES(original_text)");
|
||||
$stmt->execute([$textHash, $text]);
|
||||
|
||||
$buttons = [];
|
||||
|
||||
foreach ($languages as $lang) {
|
||||
$buttons[] = [
|
||||
'text' => $lang['flag_emoji'] . ' ' . strtoupper($lang['language_code']),
|
||||
'callback_data' => 'translate:' . $lang['language_code'] . ':' . $textHash
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'inline_keyboard' => array_chunk($buttons, 3)
|
||||
];
|
||||
}
|
||||
|
||||
function buildDiscordTranslationButtons(array $languages, string $text): array
|
||||
{
|
||||
$buttons = [];
|
||||
|
||||
$textHash = md5($text);
|
||||
|
||||
foreach ($languages as $lang) {
|
||||
$buttons[] = [
|
||||
'label' => $lang['flag_emoji'] . ' ' . strtoupper($lang['language_code']),
|
||||
'custom_id' => 'translate_' . $lang['language_code'] . ':' . $textHash,
|
||||
'style' => 1
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'type' => 1,
|
||||
'components' => array_slice($buttons, 0, 5)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$pageTitle = 'Enviar Mensaje Directo';
|
||||
|
||||
$recipients = [];
|
||||
$galleryImages = [];
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
$stmt = $pdo->query("SELECT * FROM recipients ORDER BY platform, name");
|
||||
$recipients = $stmt->fetchAll();
|
||||
|
||||
// Cargar imágenes de la galería
|
||||
$galleryPath = __DIR__ . '/galeria';
|
||||
if (is_dir($galleryPath)) {
|
||||
$files = scandir($galleryPath);
|
||||
foreach ($files as $file) {
|
||||
if (in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
$galleryImages[] = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
$success = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$content = $_POST['content'];
|
||||
$recipientId = $_POST['recipient_id'];
|
||||
|
||||
if ($content && $recipientId) {
|
||||
$messageId = createMessage([
|
||||
'user_id' => getCurrentUserId(),
|
||||
'content' => $content
|
||||
]);
|
||||
|
||||
// Obtener hora actual de MySQL para sincronización
|
||||
$pdo = getDbConnection();
|
||||
$stmt = $pdo->query("SELECT NOW() as now");
|
||||
$now = $stmt->fetch()['now'];
|
||||
|
||||
$scheduleId = createSchedule([
|
||||
'message_id' => $messageId,
|
||||
'recipient_id' => $recipientId,
|
||||
'send_time' => $now,
|
||||
'status' => 'pending'
|
||||
]);
|
||||
|
||||
// Procesar el mensaje inmediatamente
|
||||
// Obtener datos del schedule recién creado
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT s.*, m.content, r.platform_id, r.platform, r.name as recipient_name
|
||||
FROM schedules s
|
||||
JOIN messages m ON s.message_id = m.id
|
||||
JOIN recipients r ON s.recipient_id = r.id
|
||||
WHERE s.id = ?
|
||||
");
|
||||
$stmt->execute([$scheduleId]);
|
||||
$schedule = $stmt->fetch();
|
||||
|
||||
$results = ['processed' => 0, 'sent' => 0, 'failed' => 0];
|
||||
|
||||
if ($schedule) {
|
||||
$stmt = $pdo->prepare("UPDATE schedules SET status = 'processing' WHERE id = ?");
|
||||
$stmt->execute([$schedule['id']]);
|
||||
|
||||
try {
|
||||
$sender = \Common\Helpers\SenderFactory::create($schedule['platform']);
|
||||
|
||||
// Obtener botones de traducción (convertir HTML a texto plano)
|
||||
$plainText = html_entity_decode(strip_tags($schedule['content']), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$plainText = preg_replace('/\s+/', ' ', $plainText); // Normalizar espacios
|
||||
$translationButtons = getTranslationButtons($pdo, $plainText);
|
||||
|
||||
// Parsear el contenido HTML en segmentos manteniendo el orden
|
||||
$segments = $sender->parseContent($schedule['content']);
|
||||
|
||||
$messageCount = 0;
|
||||
|
||||
// Enviar cada segmento en el orden correcto
|
||||
foreach ($segments as $segment) {
|
||||
if ($segment['type'] === 'text') {
|
||||
// Convertir el texto al formato de la plataforma
|
||||
$textContent = \Common\Helpers\ConverterFactory::convert($schedule['platform'], $segment['content']);
|
||||
|
||||
if (!empty(trim($textContent))) {
|
||||
// Agregar botones de traducción al último segmento de texto
|
||||
$buttons = null;
|
||||
if ($segment === end($segments)) {
|
||||
$buttons = $schedule['platform'] === 'telegram'
|
||||
? $translationButtons['telegram']
|
||||
: $translationButtons['discord'];
|
||||
}
|
||||
|
||||
if ($schedule['platform'] === 'telegram') {
|
||||
$sender->sendMessage($schedule['platform_id'], $textContent, $buttons);
|
||||
} else {
|
||||
$sender->sendMessage($schedule['platform_id'], $textContent, null, $buttons);
|
||||
}
|
||||
$messageCount++;
|
||||
}
|
||||
} elseif ($segment['type'] === 'image') {
|
||||
$imagePath = $segment['src'];
|
||||
|
||||
// Quitar parámetros de URL si los hay
|
||||
$imgPath = parse_url($imagePath, PHP_URL_PATH) ?: $imagePath;
|
||||
|
||||
if (file_exists($imgPath)) {
|
||||
// Es un archivo local
|
||||
$sender->sendMessageWithAttachments($schedule['platform_id'], '', [$imgPath]);
|
||||
$messageCount++;
|
||||
} elseif (strpos($imagePath, 'http') === 0) {
|
||||
// Es una URL remota
|
||||
$embed = ['image' => ['url' => $imagePath]];
|
||||
$sender->sendMessage($schedule['platform_id'], '', $embed);
|
||||
$messageCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO sent_messages (schedule_id, recipient_id, platform_message_id, message_count, sent_at)
|
||||
VALUES (?, ?, '', ?, NOW())
|
||||
");
|
||||
$stmt->execute([$schedule['id'], $schedule['recipient_id'], $messageCount]);
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE schedules SET status = 'sent', sent_at = NOW() WHERE id = ?");
|
||||
$stmt->execute([$schedule['id']]);
|
||||
|
||||
$results['sent']++;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$stmt = $pdo->prepare("UPDATE schedules SET status = 'failed', error_message = ? WHERE id = ?");
|
||||
$stmt->execute([$e->getMessage(), $schedule['id']]);
|
||||
$results['failed']++;
|
||||
}
|
||||
|
||||
$results['processed']++;
|
||||
}
|
||||
|
||||
$success = "Mensaje enviado. Procesados: {$results['processed']}, Enviados: {$results['sent']}, Fallidos: {$results['failed']}";
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-send"></i> Enviar Mensaje Directo</h2>
|
||||
</div>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plataforma</label>
|
||||
<select name="platform" id="platformSelect" class="form-select" required>
|
||||
<option value="">-- Seleccionar --</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Destinatario</label>
|
||||
<select name="recipient_id" id="recipientSelect" class="form-select" required disabled>
|
||||
<option value="">Selecciona una plataforma primero</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mensaje</label>
|
||||
<textarea name="content" id="messageContent" class="form-control" rows="10" required></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-send"></i> Enviar Ahora
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Modal de Galería -->
|
||||
<div class="modal fade" id="galleryModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-images"></i> Galería de Imágenes</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<?php if (empty($galleryImages)): ?>
|
||||
<div class="col-12 text-center text-muted py-5">
|
||||
<i class="bi bi-images" style="font-size: 3rem;"></i>
|
||||
<p class="mt-3">No hay imágenes en la galería</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($galleryImages as $image): ?>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="card h-100 cursor-pointer" onclick="insertImage('galeria/<?= urlencode($image) ?>')" style="cursor: pointer;">
|
||||
<img src="galeria/<?= urlencode($image) ?>" class="card-img-top" alt="<?= htmlspecialchars($image) ?>" style="height: 120px; object-fit: cover;">
|
||||
<div class="card-body p-2">
|
||||
<small class="text-muted text-truncate d-block"><?= htmlspecialchars($image) ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
const recipients = <?= json_encode($recipients) ?>;
|
||||
|
||||
$(document).ready(function() {
|
||||
// Inicializar Summernote con botón de galería personalizado
|
||||
$('#messageContent').summernote({
|
||||
height: 300,
|
||||
toolbar: [
|
||||
['style', ['bold', 'italic', 'underline', 'clear']],
|
||||
['font', ['strikethrough', 'superscript', 'subscript']],
|
||||
['fontsize', ['fontsize']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['height', ['height']],
|
||||
['insert', ['link', 'video', 'gallery']],
|
||||
['view', ['fullscreen', 'codeview']]
|
||||
],
|
||||
buttons: {
|
||||
gallery: function() {
|
||||
return $.summernote.ui.button({
|
||||
contents: '<i class="bi bi-images"></i> Galería',
|
||||
tooltip: 'Insertar imagen desde galería',
|
||||
click: function() {
|
||||
$('#galleryModal').modal('show');
|
||||
}
|
||||
}).render();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function insertImage(url) {
|
||||
const imgTag = '<img src="' + url + '" style="max-width: 100%; height: auto;">';
|
||||
$('#messageContent').summernote('editor.pasteHTML', imgTag);
|
||||
$('#galleryModal').modal('hide');
|
||||
}
|
||||
|
||||
document.getElementById('platformSelect').addEventListener('change', function() {
|
||||
const platform = this.value;
|
||||
const select = document.getElementById('recipientSelect');
|
||||
|
||||
select.innerHTML = '<option value="">-- Seleccionar --</option>';
|
||||
|
||||
if (platform) {
|
||||
const filtered = recipients.filter(r => r.platform === platform);
|
||||
filtered.forEach(r => {
|
||||
const option = document.createElement('option');
|
||||
option.value = r.id;
|
||||
option.textContent = r.name + ' (' + r.type + ')';
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.disabled = false;
|
||||
} else {
|
||||
select.disabled = true;
|
||||
select.innerHTML = '<option value="">Selecciona una plataforma primero</option>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
106
chat_telegram.php
Executable file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
$pageTitle = 'Chat Telegram';
|
||||
|
||||
$interactions = [];
|
||||
$selectedUser = $_GET['user_id'] ?? null;
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
|
||||
if ($selectedUser) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT * FROM telegram_bot_interactions
|
||||
WHERE user_id = ?
|
||||
ORDER BY interaction_date DESC
|
||||
LIMIT 100
|
||||
");
|
||||
$stmt->execute([$selectedUser]);
|
||||
} else {
|
||||
$stmt = $pdo->query("
|
||||
SELECT user_id, username, first_name, last_name,
|
||||
COUNT(*) as total_interactions,
|
||||
MAX(interaction_date) as last_interaction
|
||||
FROM telegram_bot_interactions
|
||||
GROUP BY user_id, username, first_name, last_name
|
||||
ORDER BY last_interaction DESC
|
||||
LIMIT 50
|
||||
");
|
||||
}
|
||||
$interactions = $stmt->fetchAll();
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-telegram"></i> Chat Telegram</h2>
|
||||
</div>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Usuarios</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<?php if (empty($interactions)): ?>
|
||||
<p class="text-muted p-3">No hay interacciones</p>
|
||||
<?php else: ?>
|
||||
<div class="list-group list-group-flush">
|
||||
<?php if ($selectedUser): ?>
|
||||
<a href="chat_telegram.php" class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-arrow-left"></i> Volver a lista
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?php foreach ($interactions as $user): ?>
|
||||
<a href="chat_telegram.php?user_id=<?= $user['user_id'] ?>" class="list-group-item list-group-item-action">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1"><?= htmlspecialchars($user['first_name'] ?? 'Usuario') ?></h6>
|
||||
<small><?= $user['total_interactions'] ?></small>
|
||||
</div>
|
||||
<small class="text-muted">@<?= htmlspecialchars($user['username'] ?? 'sin username') ?></small>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Historial de Mensajes</h5>
|
||||
</div>
|
||||
<div class="card-body" style="max-height: 500px; overflow-y: auto;">
|
||||
<?php if ($selectedUser && !empty($interactions)): ?>
|
||||
<?php foreach ($interactions as $msg): ?>
|
||||
<div class="mb-3 p-2 <?= $msg['interaction_type'] === 'in' ? 'bg-light' : 'bg-white' ?> rounded">
|
||||
<small class="text-muted">
|
||||
<?= date('d/m/Y H:i:s', strtotime($msg['interaction_date'])) ?>
|
||||
- <?= $msg['interaction_type'] === 'in' ? '📥 Usuario' : '📤 Bot' ?>
|
||||
</small>
|
||||
<p class="mb-0 mt-1"><?= htmlspecialchars($msg['interaction_type'] ?? '') ?></p>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="text-muted text-center">Selecciona un usuario para ver el historial</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
66
check_webhook.php
Executable file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/includes/env_loader.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
$pageTitle = 'Verificar Webhook';
|
||||
|
||||
$results = [];
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'check_telegram') {
|
||||
$token = $_ENV['TELEGRAM_BOT_TOKEN'] ?? getenv('TELEGRAM_BOT_TOKEN');
|
||||
|
||||
$ch = curl_init('https://api.telegram.org/bot' . $token . '/getWebhookInfo');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$results['telegram'] = json_decode($response, true);
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-webcam"></i> Verificar Webhook</h2>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="mb-4">
|
||||
<input type="hidden" name="action" value="check_telegram">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-search"></i> Verificar Webhook de Telegram
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?php if (isset($results['telegram'])): ?>
|
||||
<?php if ($results['telegram']['ok']): ?>
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Estado del Webhook de Telegram</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($results['telegram']['result']['url'])): ?>
|
||||
<p><strong>URL:</strong> <?= htmlspecialchars($results['telegram']['result']['url']) ?></p>
|
||||
<p><strong>Tiene webhook:</strong> ✅ Sí</p>
|
||||
<p><strong>Último error:</strong> <?= $results['telegram']['result']['last_error_message'] ?? 'Ninguno' ?></p>
|
||||
<p><strong>Última sincronización:</strong> <?= date('d/m/Y H:i:s', $results['telegram']['result']['last_synchronize_ok_date'] ?? 0) ?></p>
|
||||
<?php else: ?>
|
||||
<p class="text-muted">No hay webhook configurado</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-danger">
|
||||
Error: <?= htmlspecialchars($results['telegram']['description'] ?? 'Unknown error') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
27
common/helpers/converter_factory.php
Executable file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Helpers;
|
||||
|
||||
require_once __DIR__ . '/../../discord/converters/HtmlToDiscordMarkdownConverter.php';
|
||||
require_once __DIR__ . '/../../telegram/converters/HtmlToTelegramHtmlConverter.php';
|
||||
|
||||
use Discord\Converters\HtmlToDiscordMarkdownConverter;
|
||||
use Telegram\Converters\HtmlToTelegramHtmlConverter;
|
||||
|
||||
class ConverterFactory
|
||||
{
|
||||
public static function create(string $platform): object
|
||||
{
|
||||
return match ($platform) {
|
||||
'discord' => new HtmlToDiscordMarkdownConverter(),
|
||||
'telegram' => new HtmlToTelegramHtmlConverter(),
|
||||
default => throw new \InvalidArgumentException("Plataforma no soportada: $platform"),
|
||||
};
|
||||
}
|
||||
|
||||
public static function convert(string $platform, string $html): string
|
||||
{
|
||||
$converter = self::create($platform);
|
||||
return $converter->convert($html);
|
||||
}
|
||||
}
|
||||
26
common/helpers/sender_factory.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Helpers;
|
||||
|
||||
require_once __DIR__ . '/../../discord/DiscordSender.php';
|
||||
require_once __DIR__ . '/../../telegram/TelegramSender.php';
|
||||
|
||||
use Discord\DiscordSender;
|
||||
use Telegram\TelegramSender;
|
||||
|
||||
class SenderFactory
|
||||
{
|
||||
public static function create(string $platform): object
|
||||
{
|
||||
return match ($platform) {
|
||||
'discord' => new DiscordSender(),
|
||||
'telegram' => new TelegramSender(),
|
||||
default => throw new \InvalidArgumentException("Plataforma no soportada: $platform"),
|
||||
};
|
||||
}
|
||||
|
||||
public static function getPlatforms(): array
|
||||
{
|
||||
return ['discord', 'telegram'];
|
||||
}
|
||||
}
|
||||
17
composer.json
Executable file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "lastwar/bot",
|
||||
"description": "Sistema de Mensajería Discord & Telegram",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"team-reflex/discord-php": "^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Discord\\": "discord/",
|
||||
"Telegram\\": "telegram/",
|
||||
"Common\\": "common/",
|
||||
"src\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
2360
composer.lock
generated
Executable file
90
configure_webhook.php
Executable file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/includes/env_loader.php';
|
||||
|
||||
requireAdmin();
|
||||
|
||||
$pageTitle = 'Configurar Webhooks';
|
||||
|
||||
$results = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'set_telegram') {
|
||||
$token = $_ENV['TELEGRAM_BOT_TOKEN'] ?? getenv('TELEGRAM_BOT_TOKEN');
|
||||
$webhookUrl = $_POST['webhook_url'];
|
||||
$webhookToken = $_ENV['TELEGRAM_WEBHOOK_TOKEN'] ?? getenv('TELEGRAM_WEBHOOK_TOKEN');
|
||||
|
||||
$fullUrl = $webhookUrl . '?auth_token=' . $webhookToken;
|
||||
|
||||
$ch = curl_init('https://api.telegram.org/bot' . $token . '/setWebhook');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, ['url' => $fullUrl]);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$results['telegram_set'] = json_decode($response, true);
|
||||
}
|
||||
}
|
||||
|
||||
$currentUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-gear"></i> Configurar Webhooks</h2>
|
||||
</div>
|
||||
|
||||
<?php if (isset($results['telegram_set'])): ?>
|
||||
<?php if ($results['telegram_set']['ok']): ?>
|
||||
<div class="alert alert-success">Webhook de Telegram configurado correctamente</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-danger">Error: <?= htmlspecialchars($results['telegram_set']['description'] ?? 'Unknown error') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0"><i class="bi bi-telegram"></i> Webhook de Telegram</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="set_telegram">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">URL del webhook</label>
|
||||
<input type="text" name="webhook_url" class="form-control" value="<?= $currentUrl ?>/telegram_bot_webhook.php" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Guardar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Información</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Los webhooks permiten que Telegram y Discord envíen eventos a tu aplicación en tiempo real.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Telegram:</strong> Procesa mensajes y comandos</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
421
create_message.php
Executable file
@@ -0,0 +1,421 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
checkSession();
|
||||
require_once __DIR__ . '/includes/message_handler.php';
|
||||
require_once __DIR__ . '/includes/schedule_helpers.php';
|
||||
require_once __DIR__ . '/common/helpers/sender_factory.php';
|
||||
require_once __DIR__ . '/common/helpers/converter_factory.php';
|
||||
|
||||
function getTranslationButtons(PDO $pdo, string $text): array
|
||||
{
|
||||
$stmt = $pdo->query("SELECT language_code, flag_emoji FROM supported_languages WHERE is_active = 1");
|
||||
$languages = $stmt->fetchAll();
|
||||
|
||||
if (count($languages) <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'telegram' => buildTelegramTranslationButtons($pdo, $languages, $text),
|
||||
'discord' => buildDiscordTranslationButtons($languages, $text)
|
||||
];
|
||||
}
|
||||
|
||||
function buildTelegramTranslationButtons(PDO $pdo, array $languages, string $text): array
|
||||
{
|
||||
if (count($languages) <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Guardar texto en la base de datos con hash consistente
|
||||
$textHash = md5($text);
|
||||
$stmt = $pdo->prepare("INSERT INTO translation_cache (text_hash, original_text) VALUES (?, ?) ON DUPLICATE KEY UPDATE original_text = VALUES(original_text)");
|
||||
$stmt->execute([$textHash, $text]);
|
||||
|
||||
$buttons = [];
|
||||
|
||||
foreach ($languages as $lang) {
|
||||
$buttons[] = [
|
||||
'text' => $lang['flag_emoji'] . ' ' . strtoupper($lang['language_code']),
|
||||
'callback_data' => 'translate:' . $lang['language_code'] . ':' . $textHash
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'inline_keyboard' => array_chunk($buttons, 3)
|
||||
];
|
||||
}
|
||||
|
||||
function buildDiscordTranslationButtons(array $languages, string $text): array
|
||||
{
|
||||
$buttons = [];
|
||||
|
||||
$textHash = md5($text);
|
||||
|
||||
foreach ($languages as $lang) {
|
||||
$buttons[] = [
|
||||
'label' => $lang['flag_emoji'] . ' ' . strtoupper($lang['language_code']),
|
||||
'custom_id' => 'translate_' . $lang['language_code'] . ':' . $textHash,
|
||||
'style' => 1
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'type' => 1,
|
||||
'components' => array_slice($buttons, 0, 5)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$pageTitle = 'Crear Mensaje - Sistema de Mensajería';
|
||||
|
||||
$recipients = [];
|
||||
$templates = [];
|
||||
$galleryImages = [];
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM recipients ORDER BY platform, name");
|
||||
$recipients = $stmt->fetchAll();
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM recurrent_messages ORDER BY name");
|
||||
$templates = $stmt->fetchAll();
|
||||
|
||||
// Cargar imágenes de la galería
|
||||
$galleryPath = __DIR__ . '/galeria';
|
||||
if (is_dir($galleryPath)) {
|
||||
$files = scandir($galleryPath);
|
||||
foreach ($files as $file) {
|
||||
if (in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
$galleryImages[] = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create') {
|
||||
$result = handleCreateMessage($_POST);
|
||||
|
||||
if ($result['success']) {
|
||||
// Si es "enviar ahora", procesar inmediatamente
|
||||
if ($_POST['send_type'] === 'now') {
|
||||
$scheduleId = $result['schedule_id'];
|
||||
|
||||
// Obtener datos del schedule
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT s.*, m.content, r.platform_id, r.platform, r.name as recipient_name
|
||||
FROM schedules s
|
||||
JOIN messages m ON s.message_id = m.id
|
||||
JOIN recipients r ON s.recipient_id = r.id
|
||||
WHERE s.id = ?
|
||||
");
|
||||
$stmt->execute([$scheduleId]);
|
||||
$schedule = $stmt->fetch();
|
||||
|
||||
if ($schedule) {
|
||||
$stmt = $pdo->prepare("UPDATE schedules SET status = 'processing' WHERE id = ?");
|
||||
$stmt->execute([$schedule['id']]);
|
||||
|
||||
try {
|
||||
$sender = \Common\Helpers\SenderFactory::create($schedule['platform']);
|
||||
|
||||
// Obtener botones de traducción (convertir HTML a texto plano)
|
||||
$plainText = html_entity_decode(strip_tags($schedule['content']), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$plainText = preg_replace('/\s+/', ' ', $plainText);
|
||||
$translationButtons = getTranslationButtons($pdo, $plainText);
|
||||
|
||||
$segments = $sender->parseContent($schedule['content']);
|
||||
$messageCount = 0;
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
if ($segment['type'] === 'text') {
|
||||
$textContent = \Common\Helpers\ConverterFactory::convert($schedule['platform'], $segment['content']);
|
||||
if (!empty(trim($textContent))) {
|
||||
// Agregar botones de traducción al último segmento de texto
|
||||
$buttons = null;
|
||||
if ($segment === end($segments)) {
|
||||
$buttons = $schedule['platform'] === 'telegram'
|
||||
? $translationButtons['telegram']
|
||||
: $translationButtons['discord'];
|
||||
}
|
||||
|
||||
if ($schedule['platform'] === 'telegram') {
|
||||
$sender->sendMessage($schedule['platform_id'], $textContent, $buttons);
|
||||
} else {
|
||||
$sender->sendMessage($schedule['platform_id'], $textContent, null, $buttons);
|
||||
}
|
||||
$messageCount++;
|
||||
}
|
||||
} elseif ($segment['type'] === 'image') {
|
||||
$imagePath = $segment['src'];
|
||||
$imgPath = parse_url($imagePath, PHP_URL_PATH) ?: $imagePath;
|
||||
|
||||
if (file_exists($imgPath)) {
|
||||
$sender->sendMessageWithAttachments($schedule['platform_id'], '', [$imgPath]);
|
||||
$messageCount++;
|
||||
} elseif (strpos($imagePath, 'http') === 0) {
|
||||
$embed = ['image' => ['url' => $imagePath]];
|
||||
$sender->sendMessage($schedule['platform_id'], '', $embed);
|
||||
$messageCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO sent_messages (schedule_id, recipient_id, platform_message_id, message_count, sent_at)
|
||||
VALUES (?, ?, '', ?, NOW())
|
||||
");
|
||||
$stmt->execute([$schedule['id'], $schedule['recipient_id'], $messageCount]);
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE schedules SET status = 'sent', sent_at = NOW() WHERE id = ?");
|
||||
$stmt->execute([$schedule['id']]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$stmt = $pdo->prepare("UPDATE schedules SET status = 'failed', error_message = ? WHERE id = ?");
|
||||
$stmt->execute([$e->getMessage(), $schedule['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: scheduled_messages.php?success=1');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['error'];
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-plus-circle"></i> Crear Mensaje</h2>
|
||||
<div class="btn-group">
|
||||
<a href="preview_message.php" class="btn btn-outline-secondary" target="_blank">
|
||||
<i class="bi bi-eye"></i> Previsualizar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" id="messageForm">
|
||||
<input type="hidden" name="action" value="create">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Contenido del Mensaje</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plantilla (opcional)</label>
|
||||
<select class="form-select" id="templateSelect">
|
||||
<option value="">-- Seleccionar plantilla --</option>
|
||||
<?php foreach ($templates as $template): ?>
|
||||
<option value="<?= htmlspecialchars($template['message_content']) ?>">
|
||||
<?= htmlspecialchars($template['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mensaje</label>
|
||||
<textarea name="content" id="messageContent" class="form-control" rows="12"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Destinatario</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plataforma</label>
|
||||
<select name="platform" id="platformSelect" class="form-select" required>
|
||||
<option value="">-- Seleccionar --</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Destinatario</label>
|
||||
<select name="recipient_id" id="recipientSelect" class="form-select" required disabled>
|
||||
<option value="">Selecciona una plataforma primero</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Programación</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tipo de envío</label>
|
||||
<select name="send_type" id="sendType" class="form-select" required>
|
||||
<option value="now">Enviar ahora</option>
|
||||
<option value="later">Programar para después</option>
|
||||
<option value="recurring">Recurrente</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="datetimeField" style="display: none;">
|
||||
<label class="form-label">Fecha y hora</label>
|
||||
<input type="datetime-local" name="send_datetime" class="form-control">
|
||||
</div>
|
||||
|
||||
<div id="recurringFields" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Días</label>
|
||||
<select name="recurring_days" class="form-select">
|
||||
<option value="monday">Lunes</option>
|
||||
<option value="tuesday">Martes</option>
|
||||
<option value="wednesday">Miércoles</option>
|
||||
<option value="thursday">Jueves</option>
|
||||
<option value="friday">Viernes</option>
|
||||
<option value="saturday">Sábado</option>
|
||||
<option value="sunday">Domingo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hora</label>
|
||||
<input type="time" name="recurring_time" class="form-control" value="09:00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-send"></i> Enviar
|
||||
</button>
|
||||
<a href="index.php" class="btn btn-outline-secondary">Cancelar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Modal de Galería -->
|
||||
<div class="modal fade" id="galleryModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-images"></i> Galería de Imágenes</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<?php if (empty($galleryImages)): ?>
|
||||
<div class="col-12 text-center text-muted py-5">
|
||||
<i class="bi bi-images" style="font-size: 3rem;"></i>
|
||||
<p class="mt-3">No hay imágenes en la galería</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($galleryImages as $image): ?>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="card h-100 cursor-pointer" onclick="insertImage('galeria/<?= urlencode($image) ?>')" style="cursor: pointer;">
|
||||
<img src="galeria/<?= urlencode($image) ?>" class="card-img-top" alt="<?= htmlspecialchars($image) ?>" style="height: 120px; object-fit: cover;">
|
||||
<div class="card-body p-2">
|
||||
<small class="text-muted text-truncate d-block"><?= htmlspecialchars($image) ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
const recipients = <?= json_encode($recipients) ?>;
|
||||
|
||||
$(document).ready(function() {
|
||||
// Inicializar Summernote con botón de galería personalizado
|
||||
$('#messageContent').summernote({
|
||||
height: 350,
|
||||
toolbar: [
|
||||
['style', ['bold', 'italic', 'underline', 'clear']],
|
||||
['font', ['strikethrough', 'superscript', 'subscript']],
|
||||
['fontsize', ['fontsize']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['height', ['height']],
|
||||
['insert', ['link', 'video', 'gallery']], // 'gallery' es nuestro botón personalizado
|
||||
['view', ['fullscreen', 'codeview']]
|
||||
],
|
||||
buttons: {
|
||||
gallery: function() {
|
||||
return $.summernote.ui.button({
|
||||
contents: '<i class="bi bi-images"></i> Galería',
|
||||
tooltip: 'Insertar imagen desde galería',
|
||||
click: function() {
|
||||
$('#galleryModal').modal('show');
|
||||
}
|
||||
}).render();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function insertImage(url) {
|
||||
const imgTag = '<img src="' + url + '" style="max-width: 100%; height: auto;">';
|
||||
$('#messageContent').summernote('editor.pasteHTML', imgTag);
|
||||
$('#galleryModal').modal('hide');
|
||||
}
|
||||
|
||||
document.getElementById('templateSelect').addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
$('#messageContent').summernote('code', this.value);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('platformSelect').addEventListener('change', function() {
|
||||
const platform = this.value;
|
||||
const recipientSelect = document.getElementById('recipientSelect');
|
||||
|
||||
recipientSelect.innerHTML = '<option value="">-- Seleccionar --</option>';
|
||||
|
||||
if (platform) {
|
||||
const filtered = recipients.filter(r => r.platform === platform);
|
||||
filtered.forEach(r => {
|
||||
const option = document.createElement('option');
|
||||
option.value = r.id;
|
||||
option.textContent = r.name + ' (' + r.type + ')';
|
||||
recipientSelect.appendChild(option);
|
||||
});
|
||||
recipientSelect.disabled = false;
|
||||
} else {
|
||||
recipientSelect.disabled = true;
|
||||
recipientSelect.innerHTML = '<option value="">Selecciona una plataforma primero</option>';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('sendType').addEventListener('change', function() {
|
||||
const datetimeField = document.getElementById('datetimeField');
|
||||
const recurringFields = document.getElementById('recurringFields');
|
||||
|
||||
datetimeField.style.display = this.value === 'later' ? 'block' : 'none';
|
||||
recurringFields.style.display = this.value === 'recurring' ? 'block' : 'none';
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
217
db/bot.sql
Executable file
@@ -0,0 +1,217 @@
|
||||
-- Sistema de Mensajería Discord & Telegram
|
||||
-- Base de datos: lastwar2
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS lastwar2 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE lastwar2;
|
||||
|
||||
-- 1. Usuarios del sistema
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role ENUM('user', 'admin') DEFAULT 'user',
|
||||
telegram_chat_id VARCHAR(50) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_username (username)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 2. Destinatarios
|
||||
CREATE TABLE IF NOT EXISTS recipients (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
platform_id BIGINT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type ENUM('channel', 'user') NOT NULL,
|
||||
platform ENUM('discord', 'telegram') NOT NULL,
|
||||
language_code VARCHAR(10) DEFAULT 'es',
|
||||
chat_mode VARCHAR(20) DEFAULT 'agent',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_platform_recipient (platform, platform_id),
|
||||
INDEX idx_platform (platform),
|
||||
INDEX idx_type (type)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 3. Contenido de mensajes
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 4. Programaciones de envío
|
||||
CREATE TABLE IF NOT EXISTS schedules (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
message_id INT NOT NULL,
|
||||
recipient_id INT NOT NULL,
|
||||
send_time DATETIME NOT NULL,
|
||||
status ENUM('draft', 'pending', 'processing', 'sent', 'failed', 'cancelled', 'disabled') DEFAULT 'pending',
|
||||
is_recurring BOOLEAN DEFAULT FALSE,
|
||||
recurring_days VARCHAR(50) NULL,
|
||||
recurring_time TIME NULL,
|
||||
sent_at DATETIME NULL,
|
||||
error_message TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_send_time (send_time),
|
||||
INDEX idx_message (message_id),
|
||||
INDEX idx_recipient (recipient_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 5. Plantillas de mensajes recurrentes
|
||||
CREATE TABLE IF NOT EXISTS recurrent_messages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
message_content TEXT NOT NULL,
|
||||
telegram_command VARCHAR(50) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_command (telegram_command)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 6. Mensajes enviados (historial)
|
||||
CREATE TABLE IF NOT EXISTS sent_messages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
schedule_id INT NOT NULL,
|
||||
recipient_id INT NOT NULL,
|
||||
platform_message_id VARCHAR(100) NULL,
|
||||
message_count INT DEFAULT 1,
|
||||
sent_at DATETIME NOT NULL,
|
||||
user_id INT NULL,
|
||||
INDEX idx_sent_at (sent_at)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 7. Idiomas soportados para traducción
|
||||
CREATE TABLE IF NOT EXISTS supported_languages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
language_code VARCHAR(10) NOT NULL,
|
||||
language_name VARCHAR(50) NOT NULL,
|
||||
flag_emoji VARCHAR(10) DEFAULT '',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_lang_code (language_code)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 8. Configuración de mensaje de bienvenida Telegram
|
||||
CREATE TABLE IF NOT EXISTS telegram_bot_messages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
message_text TEXT,
|
||||
button_text VARCHAR(100) NULL,
|
||||
group_invite_link VARCHAR(500) NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
register_users BOOLEAN DEFAULT TRUE,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 9. Registro de interacciones de usuarios Telegram
|
||||
CREATE TABLE IF NOT EXISTS telegram_bot_interactions (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
username VARCHAR(100) NULL,
|
||||
first_name VARCHAR(100) NULL,
|
||||
last_name VARCHAR(100) NULL,
|
||||
interaction_type VARCHAR(50) NOT NULL,
|
||||
interaction_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_date (interaction_date)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 10. Mensajes de bienvenida por grupo Telegram
|
||||
CREATE TABLE IF NOT EXISTS telegram_welcome_messages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
chat_id BIGINT NOT NULL,
|
||||
welcome_message TEXT,
|
||||
button_text VARCHAR(100) NULL,
|
||||
group_invite_link VARCHAR(500) NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
language_code VARCHAR(10) DEFAULT 'es',
|
||||
language_name VARCHAR(50) DEFAULT 'Español',
|
||||
flag_emoji VARCHAR(10) DEFAULT '🇪🇸',
|
||||
UNIQUE KEY unique_chat_id (chat_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 11. Cola de traducciones
|
||||
CREATE TABLE IF NOT EXISTS translation_queue (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
platform VARCHAR(20) NOT NULL,
|
||||
message_id BIGINT NULL,
|
||||
chat_id BIGINT NOT NULL,
|
||||
user_id BIGINT NULL,
|
||||
text_to_translate TEXT NOT NULL,
|
||||
source_lang VARCHAR(10) NOT NULL,
|
||||
target_lang VARCHAR(10) NULL,
|
||||
status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
|
||||
attempts INT DEFAULT 0,
|
||||
error_message TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at DATETIME NULL,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 12. Log de actividades
|
||||
CREATE TABLE IF NOT EXISTS activity_log (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
details TEXT,
|
||||
timestamp DATETIME NOT NULL,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_timestamp (timestamp)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 13. Bloqueos de comandos
|
||||
CREATE TABLE IF NOT EXISTS command_locks (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
chat_id BIGINT NOT NULL,
|
||||
command VARCHAR(100) NOT NULL,
|
||||
type ENUM('command', 'translation') DEFAULT 'command',
|
||||
data JSON NULL,
|
||||
message_id BIGINT NULL,
|
||||
status ENUM('processing', 'completed', 'failed') DEFAULT 'processing',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NULL,
|
||||
INDEX idx_chat_command (chat_id, command),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 14. Configuración general
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
setting_key VARCHAR(100) PRIMARY KEY,
|
||||
setting_value TEXT
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Insertar idiomas por defecto
|
||||
INSERT INTO supported_languages (language_code, language_name, flag_emoji, is_active) VALUES
|
||||
('es', 'Español', '🇪🇸', TRUE),
|
||||
('en', 'English', '🇬🇧', TRUE),
|
||||
('pt', 'Português', '🇧🇷', TRUE),
|
||||
('fr', 'Français', '🇫🇷', TRUE),
|
||||
('de', 'Deutsch', '🇩🇪', FALSE),
|
||||
('it', 'Italiano', '🇮🇹', FALSE),
|
||||
('ru', 'Русский', '🇷🇺', FALSE),
|
||||
('zh', '中文', '🇨🇳', FALSE),
|
||||
('ja', '日本語', '🇯🇵', FALSE),
|
||||
('ko', '한국어', '🇰🇷', FALSE)
|
||||
ON DUPLICATE KEY UPDATE language_name = VALUES(language_name);
|
||||
|
||||
-- Insertar configuración por defecto de Telegram
|
||||
INSERT INTO telegram_bot_messages (id, message_text, button_text, is_active, register_users) VALUES
|
||||
(1, '¡Hola {user_name}! 👋\n\nUsa /comandos para ver los comandos disponibles.\n\nTambién puedes usar /agente para interactuar con la IA.', 'Unirse al grupo', TRUE, TRUE)
|
||||
ON DUPLICATE KEY UPDATE message_text = VALUES(message_text);
|
||||
|
||||
-- Insertar usuario admin (password: admin123)
|
||||
INSERT INTO users (username, password, role) VALUES
|
||||
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin')
|
||||
ON DUPLICATE KEY UPDATE username = VALUES(username);
|
||||
|
||||
-- Insertar configuración inicial
|
||||
INSERT INTO settings (setting_key, setting_value) VALUES
|
||||
('app_name', 'Sistema de Mensajería'),
|
||||
('default_language', 'es'),
|
||||
('timezone', 'America/Mexico_City')
|
||||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value);
|
||||
414
discord/DiscordSender.php
Executable file
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
|
||||
namespace Discord;
|
||||
|
||||
class DiscordSender
|
||||
{
|
||||
private string $token;
|
||||
private string $guildId;
|
||||
private string $baseUrl = 'https://discord.com/api/v10';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->token = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||
$this->guildId = $_ENV['DISCORD_GUILD_ID'] ?? getenv('DISCORD_GUILD_ID');
|
||||
}
|
||||
|
||||
public function sendMessage(string $channelId, string $content, ?array $embed = null, ?array $buttons = null): array
|
||||
{
|
||||
$channelId = $this->resolveUserToDmChannel($channelId);
|
||||
|
||||
$data = ['content' => $content];
|
||||
|
||||
if ($embed) {
|
||||
$data['embeds'] = [$embed];
|
||||
}
|
||||
|
||||
if ($buttons) {
|
||||
// Construir componentes correctamente
|
||||
$components = [];
|
||||
|
||||
foreach ($buttons as $row) {
|
||||
if (isset($row['components']) && is_array($row['components'])) {
|
||||
$componentRow = [
|
||||
'type' => 1,
|
||||
'components' => []
|
||||
];
|
||||
|
||||
foreach ($row['components'] as $btn) {
|
||||
$componentRow['components'][] = [
|
||||
'type' => 2,
|
||||
'style' => isset($btn['style']) ? intval($btn['style']) : 1,
|
||||
'label' => $btn['label'],
|
||||
'custom_id' => $btn['custom_id']
|
||||
];
|
||||
}
|
||||
|
||||
$components[] = $componentRow;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($components)) {
|
||||
$data['components'] = $components;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->request("POST", "/channels/{$channelId}/messages", $data);
|
||||
}
|
||||
|
||||
private function cleanComponents(array $components): array
|
||||
{
|
||||
$clean = [];
|
||||
|
||||
foreach ($components as $row) {
|
||||
$cleanRow = [];
|
||||
|
||||
if (isset($row['components'])) {
|
||||
$cleanComponents = [];
|
||||
foreach ($row['components'] as $btn) {
|
||||
$cleanBtn = [
|
||||
'type' => 2,
|
||||
'style' => $btn['style'] ?? 1,
|
||||
'label' => $btn['label'],
|
||||
'custom_id' => $btn['custom_id']
|
||||
];
|
||||
$cleanComponents[] = $cleanBtn;
|
||||
}
|
||||
$cleanRow = [
|
||||
'type' => 1,
|
||||
'components' => $cleanComponents
|
||||
];
|
||||
} else {
|
||||
$cleanRow = $row;
|
||||
}
|
||||
|
||||
$clean[] = $cleanRow;
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
public function sendMessageWithImages(string $channelId, string $content, array $images, ?array $buttons = null): array
|
||||
{
|
||||
$channelId = $this->resolveUserToDmChannel($channelId);
|
||||
|
||||
$result = null;
|
||||
|
||||
if (!empty($images)) {
|
||||
// Verificar si las imágenes son locales o URLs
|
||||
$localImages = [];
|
||||
$remoteImages = [];
|
||||
|
||||
foreach ($images as $imageUrl) {
|
||||
if (strpos($imageUrl, 'http') === 0) {
|
||||
// Es una URL remota
|
||||
$remoteImages[] = $imageUrl;
|
||||
} elseif (file_exists($imageUrl)) {
|
||||
// Es un archivo local
|
||||
$localImages[] = $imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Enviar imágenes locales como adjuntos
|
||||
if (!empty($localImages)) {
|
||||
$result = $this->sendMessageWithAttachments($channelId, $content, $localImages);
|
||||
} else {
|
||||
$result = $this->sendMessage($channelId, $content, null, $buttons);
|
||||
}
|
||||
|
||||
// Enviar imágenes remotas como embeds
|
||||
foreach ($remoteImages as $imageUrl) {
|
||||
$embed = [
|
||||
'image' => ['url' => $imageUrl]
|
||||
];
|
||||
$result = $this->sendMessage($channelId, '', $embed, $buttons);
|
||||
$buttons = null; // Solo enviar botones en el primer mensaje
|
||||
}
|
||||
} else {
|
||||
$result = $this->sendMessage($channelId, $content, null, $buttons);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar contenido con texto e imágenes en el orden correcto
|
||||
* Divide el contenido en segmentos y los envía manteniendo el orden
|
||||
*/
|
||||
public function sendContentWithOrderedImages(string $channelId, array $segments): void
|
||||
{
|
||||
$channelId = $this->resolveUserToDmChannel($channelId);
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
if ($segment['type'] === 'text') {
|
||||
// Enviar texto
|
||||
if (!empty(trim($segment['content']))) {
|
||||
$this->sendMessage($channelId, $segment['content']);
|
||||
}
|
||||
} elseif ($segment['type'] === 'image') {
|
||||
// Enviar imagen
|
||||
$imagePath = $segment['src'];
|
||||
|
||||
if (strpos($imagePath, 'http') === 0) {
|
||||
// URL remota - enviar como embed
|
||||
$embed = ['image' => ['url' => $imagePath]];
|
||||
$this->sendMessage($channelId, '', $embed);
|
||||
} elseif (file_exists($imagePath)) {
|
||||
// Archivo local - enviar como adjunto
|
||||
$this->sendMessageWithAttachments($channelId, '', [$imagePath]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function sendMessageWithAttachments(string $channelId, string $content, array $files): array
|
||||
{
|
||||
$channelId = $this->resolveUserToDmChannel($channelId);
|
||||
|
||||
$url = $this->baseUrl . "/channels/{$channelId}/messages";
|
||||
|
||||
// Preparar los datos multipart
|
||||
$postData = [
|
||||
'content' => $content,
|
||||
'payload_json' => json_encode(['content' => $content])
|
||||
];
|
||||
|
||||
// Agregar archivos
|
||||
$fileIndex = 0;
|
||||
foreach ($files as $filePath) {
|
||||
if (file_exists($filePath)) {
|
||||
$postData["file{$fileIndex}"] = new \CURLFile($filePath, mime_content_type($filePath), basename($filePath));
|
||||
$fileIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $this->token,
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
throw new \Exception("Discord API Error: " . ($result['message'] ?? 'Unknown error'));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function editMessage(string $channelId, string $messageId, string $content, ?array $embed = null): array
|
||||
{
|
||||
$data = ['content' => $content];
|
||||
|
||||
if ($embed) {
|
||||
$data['embeds'] = [$embed];
|
||||
}
|
||||
|
||||
return $this->request("PATCH", "/channels/{$channelId}/messages/{$messageId}", $data);
|
||||
}
|
||||
|
||||
public function deleteMessage(string $channelId, string $messageId): bool
|
||||
{
|
||||
$this->request("DELETE", "/channels/{$channelId}/messages/{$messageId}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getChannel(string $channelId): array
|
||||
{
|
||||
return $this->request("GET", "/channels/{$channelId}");
|
||||
}
|
||||
|
||||
public function getGuildChannels(): array
|
||||
{
|
||||
return $this->request("GET", "/guilds/{$this->guildId}/channels");
|
||||
}
|
||||
|
||||
private function buildActionRow(array $buttons): array
|
||||
{
|
||||
$components = [];
|
||||
|
||||
foreach ($buttons as $button) {
|
||||
$component = [
|
||||
'type' => 2,
|
||||
'style' => $button['style'] ?? 1,
|
||||
'label' => $button['label'],
|
||||
'custom_id' => $button['custom_id']
|
||||
];
|
||||
|
||||
if (isset($button['url'])) {
|
||||
$component['url'] = $button['url'];
|
||||
}
|
||||
|
||||
$components[] = $component;
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'type' => 1,
|
||||
'components' => $components
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function request(string $method, string $endpoint, ?array $data = null): array
|
||||
{
|
||||
$url = $this->baseUrl . $endpoint;
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $this->token,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
|
||||
if ($data) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
throw new \Exception("Discord API Error: " . ($result['message'] ?? 'Unknown error'));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function resolveUserToDmChannel(string $userId): string
|
||||
{
|
||||
try {
|
||||
$response = $this->request("POST", "/users/{$userId}/channels", [
|
||||
'recipient_id' => $userId
|
||||
]);
|
||||
|
||||
if (isset($response['id'])) {
|
||||
return $response['id'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Error creating DM channel: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $userId;
|
||||
}
|
||||
|
||||
public function splitMessage(string $content, int $maxLength = 2000): array
|
||||
{
|
||||
if (strlen($content) <= $maxLength) {
|
||||
return [$content];
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
$lines = explode("\n", $content);
|
||||
$currentPart = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (strlen($currentPart . "\n" . $line) > $maxLength) {
|
||||
if (!empty($currentPart)) {
|
||||
$parts[] = $currentPart;
|
||||
$currentPart = '';
|
||||
}
|
||||
|
||||
if (strlen($line) > $maxLength) {
|
||||
$chunks = str_split($line, $maxLength);
|
||||
$parts = array_merge($parts, array_slice($chunks, 0, -1));
|
||||
$currentPart = end($chunks);
|
||||
} else {
|
||||
$currentPart = $line;
|
||||
}
|
||||
} else {
|
||||
$currentPart .= (empty($currentPart) ? '' : "\n") . $line;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($currentPart)) {
|
||||
$parts[] = $currentPart;
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsear HTML y dividirlo en segmentos manteniendo el orden
|
||||
* Retorna array de ['type' => 'text|image', 'content' => '...', 'src' => '...']
|
||||
*/
|
||||
public function parseContent(string $html): array
|
||||
{
|
||||
$segments = [];
|
||||
$currentText = '';
|
||||
|
||||
// Usar regex para encontrar todas las etiquetas <img>
|
||||
$pattern = '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i';
|
||||
$parts = preg_split($pattern, $html, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
// El array parts alterna entre: [texto, src_imagen, texto, src_imagen, texto...]
|
||||
for ($i = 0; $i < count($parts); $i++) {
|
||||
if ($i % 2 === 0) {
|
||||
// Es texto
|
||||
$text = $this->htmlToPlainText($parts[$i]);
|
||||
if (!empty(trim($text))) {
|
||||
$segments[] = [
|
||||
'type' => 'text',
|
||||
'content' => $text
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// Es una imagen (el src capturado)
|
||||
$segments[] = [
|
||||
'type' => 'image',
|
||||
'src' => $parts[$i],
|
||||
'content' => ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertir HTML a texto plano manteniendo saltos de línea
|
||||
*/
|
||||
private function htmlToPlainText(string $html): string
|
||||
{
|
||||
// Reemplazar <br>, <p>, etc. con saltos de línea
|
||||
$text = preg_replace('/<br\s*\/?>/i', "\n", $html);
|
||||
$text = preg_replace('/<\/p>/i', "\n", $text);
|
||||
$text = preg_replace('/<p[^>]*>/i', '', $text);
|
||||
$text = preg_replace('/<div[^>]*>/i', '', $text);
|
||||
$text = preg_replace('/<\/div>/i', "\n", $text);
|
||||
|
||||
// Eliminar otras etiquetas HTML
|
||||
$text = strip_tags($text);
|
||||
|
||||
// Decodificar entidades HTML
|
||||
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
// Limpiar espacios múltiples y saltos de línea
|
||||
$text = preg_replace('/\n{3,}/', "\n\n", $text);
|
||||
$text = preg_replace('/[ \t]+/', ' ', $text);
|
||||
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
public function extractImages(string $html): array
|
||||
{
|
||||
preg_match_all('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $html, $matches);
|
||||
return $matches[1] ?? [];
|
||||
}
|
||||
|
||||
public function removeImages(string $html): string
|
||||
{
|
||||
return preg_replace('/<img[^>]+>/i', '', $html);
|
||||
}
|
||||
}
|
||||
101
discord/actions/DiscordActions.php
Executable file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Discord\Actions;
|
||||
|
||||
use Discord\DiscordSender;
|
||||
use Discord\Converters\HtmlToDiscordMarkdownConverter;
|
||||
|
||||
class DiscordActions
|
||||
{
|
||||
private DiscordSender $sender;
|
||||
private HtmlToDiscordMarkdownConverter $converter;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->sender = new DiscordSender();
|
||||
$this->converter = new HtmlToDiscordMarkdownConverter();
|
||||
}
|
||||
|
||||
public function sendTemplate(string $channelId, string $htmlContent, ?string $command = null): array
|
||||
{
|
||||
$content = $this->converter->convert($htmlContent);
|
||||
$images = $this->converter->extractImages($htmlContent);
|
||||
|
||||
$contentWithoutImages = $this->converter->removeImages($htmlContent);
|
||||
$content = $this->converter->convert($contentWithoutImages);
|
||||
|
||||
if (!empty($images)) {
|
||||
return $this->sender->sendMessageWithImages($channelId, $content, $images);
|
||||
}
|
||||
|
||||
return $this->sender->sendMessage($channelId, $content);
|
||||
}
|
||||
|
||||
public function sendScheduledMessage(string $channelId, string $htmlContent, ?array $buttons = null): array
|
||||
{
|
||||
$content = $this->converter->convert($htmlContent);
|
||||
$images = $this->converter->extractImages($htmlContent);
|
||||
|
||||
$contentWithoutImages = $this->converter->removeImages($htmlContent);
|
||||
$content = $this->converter->convert($contentWithoutImages);
|
||||
|
||||
if (!empty($images)) {
|
||||
return $this->sender->sendMessageWithImages($channelId, $content, $images);
|
||||
}
|
||||
|
||||
return $this->sender->sendMessage($channelId, $content, null, $buttons);
|
||||
}
|
||||
|
||||
public function sendWithTranslation(string $channelId, string $htmlContent, array $translations): array
|
||||
{
|
||||
$content = $this->converter->convert($htmlContent);
|
||||
|
||||
$embed = [
|
||||
'title' => '📝 Mensaje Original',
|
||||
'description' => $content,
|
||||
'color' => 3447003,
|
||||
'footer' => [
|
||||
'text' => 'Traducciones disponibles en los botones'
|
||||
]
|
||||
];
|
||||
|
||||
$buttons = [];
|
||||
foreach ($translations as $lang => $translatedText) {
|
||||
$buttons[] = [
|
||||
'label' => strtoupper($lang),
|
||||
'custom_id' => "translate_{$lang}",
|
||||
'style' => 1
|
||||
];
|
||||
}
|
||||
|
||||
return $this->sender->sendMessage($channelId, '', $embed, $buttons);
|
||||
}
|
||||
|
||||
public function translateMessage(string $channelId, string $messageId, string $originalText, string $targetLang, string $translatedText): array
|
||||
{
|
||||
$embed = [
|
||||
'title' => "🌐 Traducción ({strtoupper($targetLang)})",
|
||||
'description' => $translatedText,
|
||||
'color' => 3066993,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'Original',
|
||||
'value' => mb_substr($originalText, 0, 1024),
|
||||
'inline' => false
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
return $this->sender->sendMessage($channelId, '', $embed);
|
||||
}
|
||||
|
||||
public function handleButtonInteraction(string $channelId, string $messageId, string $customId): array
|
||||
{
|
||||
if (str_starts_with($customId, 'translate_')) {
|
||||
$lang = str_replace('translate_', '', $customId);
|
||||
return ['action' => 'translate', 'lang' => $lang];
|
||||
}
|
||||
|
||||
return ['action' => 'unknown'];
|
||||
}
|
||||
}
|
||||
127
discord/converters/HtmlToDiscordMarkdownConverter.php
Executable file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Discord\Converters;
|
||||
|
||||
class HtmlToDiscordMarkdownConverter
|
||||
{
|
||||
public function convert(string $html): string
|
||||
{
|
||||
$content = $html;
|
||||
|
||||
$content = $this->convertImages($content);
|
||||
$content = $this->convertBold($content);
|
||||
$content = $this->convertItalic($content);
|
||||
$content = $this->convertUnderline($content);
|
||||
$content = $this->convertStrikethrough($content);
|
||||
$content = $this->convertCode($content);
|
||||
$content = $this->convertLinks($content);
|
||||
$content = $this->convertLists($content);
|
||||
$content = $this->convertHeaders($content);
|
||||
$content = $this->convertLineBreaks($content);
|
||||
$content = $this->cleanUp($content);
|
||||
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
private function convertImages(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', '($1)', $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertBold(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<strong[^>]*>(.*?)<\/strong>/is', '**$1**', $content);
|
||||
$content = preg_replace('/<b[^>]*>(.*?)<\/b>/is', '**$1**', $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertItalic(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<em[^>]*>(.*?)<\/em>/is', '*$1*', $content);
|
||||
$content = preg_replace('/<i[^>]*>(.*?)<\/i>/is', '*$1*', $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertUnderline(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<u[^>]*>(.*?)<\/u>/is', '__$1__', $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertStrikethrough(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<s[^>]*>(.*?)<\/s>/is', '~~$1~~', $content);
|
||||
$content = preg_replace('/<strike[^>]*>(.*?)<\/strike>/is', '~~$1~~', $content);
|
||||
$content = preg_replace('/<del[^>]*>(.*?)<\/del>/is', '~~$1~~', $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertCode(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<code[^>]*>(.*?)<\/code>/is', '`$1`', $content);
|
||||
$content = preg_replace('/<pre[^>]*>(.*?)<\/pre>/is', "```\n$1\n```", $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertLinks(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is', '[$2]($1)', $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertLists(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<li[^>]*>(.*?)<\/li>/is', "\n• $1", $content);
|
||||
$content = preg_replace('/<ul[^>]*>/is', '', $content);
|
||||
$content = preg_replace('/<\/ul>/is', '', $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertHeaders(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<h1[^>]*>(.*?)<\/h1>/is', "\n\n## $1\n", $content);
|
||||
$content = preg_replace('/<h2[^>]*>(.*?)<\/h2>/is', "\n\n### $1\n", $content);
|
||||
$content = preg_replace('/<h3[^>]*>(.*?)<\/h3>/is', "\n\n#### $1\n", $content);
|
||||
$content = preg_replace('/<h4[^>]*>(.*?)<\/h4>/is', "\n\n##### $1\n", $content);
|
||||
$content = preg_replace('/<h5[^>]*>(.*?)<\/h5>/is', "\n\n###### $1\n", $content);
|
||||
$content = preg_replace('/<h6[^>]*>(.*?)<\/h6>/is', "\n\n###### $1\n", $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertLineBreaks(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<br\s*\/?>/i', "\n", $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function cleanUp(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<p[^>]*>(.*?)<\/p>/is', "\n$1\n", $content);
|
||||
$content = preg_replace('/<div[^>]*>(.*?)<\/div>/is', "\n$1\n", $content);
|
||||
$content = preg_replace('/<span[^>]*>(.*?)<\/span>/is', '$1', $content);
|
||||
|
||||
$content = strip_tags($content);
|
||||
|
||||
$content = preg_replace('/ /', ' ', $content);
|
||||
$content = preg_replace('/&/', '&', $content);
|
||||
$content = preg_replace('/</', '<', $content);
|
||||
$content = preg_replace('/>/', '>', $content);
|
||||
$content = preg_replace('/"/', '"', $content);
|
||||
|
||||
$content = preg_replace('/\n{3,}/', "\n\n", $content);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function extractImages(string $html): array
|
||||
{
|
||||
preg_match_all('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $html, $matches);
|
||||
return $matches[1] ?? [];
|
||||
}
|
||||
|
||||
public function removeImages(string $html): string
|
||||
{
|
||||
return preg_replace('/<img[^>]+>/i', '', $html);
|
||||
}
|
||||
}
|
||||
537
discord_bot.php
Executable file
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/env_loader.php';
|
||||
|
||||
use Discord\Discord;
|
||||
use Discord\Parts\Channel\Message;
|
||||
use Discord\Parts\Guild\Guild;
|
||||
use Discord\Parts\User\Member;
|
||||
use Discord\WebSockets\Intents;
|
||||
use Discord\WebSockets\Event;
|
||||
|
||||
$token = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||
|
||||
$discord = new Discord([
|
||||
'token' => $token,
|
||||
'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS | Intents::MESSAGE_CONTENT,
|
||||
]);
|
||||
|
||||
$discord->on('ready', function (Discord $discord) {
|
||||
echo "Bot de Discord conectado como: {$discord->user->username}" . PHP_EOL;
|
||||
|
||||
$guild = $discord->guilds->first();
|
||||
if ($guild) {
|
||||
echo "Servidor: {$guild->name}" . PHP_EOL;
|
||||
}
|
||||
});
|
||||
|
||||
$discord->on(Event::GUILD_MEMBER_ADD, function (Member $member, Discord $discord) {
|
||||
echo "Nuevo miembro: {$member->user->username}" . PHP_EOL;
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO recipients (platform_id, name, type, platform, language_code, chat_mode)
|
||||
VALUES (?, ?, 'user', 'discord', 'es', 'agent')
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name)
|
||||
");
|
||||
$stmt->execute([
|
||||
$member->user->id,
|
||||
$member->user->username
|
||||
]);
|
||||
|
||||
echo "Usuario registrado en la base de datos" . PHP_EOL;
|
||||
|
||||
if (!isExistingDiscordUser($pdo, $member->user->id)) {
|
||||
sendDiscordWelcomeMessage($pdo, $member, $discord);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "Error al registrar usuario: " . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
});
|
||||
|
||||
$discord->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) {
|
||||
if ($message->author->bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $message->content;
|
||||
$channelId = $message->channel_id;
|
||||
$userId = $message->author->id;
|
||||
$username = $message->author->username;
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
|
||||
$isNewUser = !isExistingDiscordUser($pdo, $userId);
|
||||
|
||||
registerDiscordUser($pdo, $message->author);
|
||||
|
||||
if ($isNewUser) {
|
||||
sendDiscordWelcomeMessageOnMessage($pdo, $message, $username);
|
||||
}
|
||||
|
||||
if (str_starts_with($content, '#')) {
|
||||
$command = ltrim($content, '#');
|
||||
handleTemplateCommand($pdo, $message, $command);
|
||||
|
||||
} elseif (str_starts_with($content, '/')) {
|
||||
handleSlashCommand($pdo, $message, $content);
|
||||
|
||||
} else {
|
||||
handleRegularMessage($pdo, $message, $content);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "Error: " . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
});
|
||||
|
||||
$discord->on(Event::INTERACTION_CREATE, function ($interaction, Discord $discord) {
|
||||
echo "Interacción recibida" . PHP_EOL;
|
||||
|
||||
try {
|
||||
handleButtonInteraction($interaction, $discord);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "Error en interacción: " . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
});
|
||||
|
||||
function handleTemplateCommand(PDO $pdo, Message $message, string $command): void
|
||||
{
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM recurrent_messages WHERE telegram_command = ?");
|
||||
$stmt->execute([$command]);
|
||||
$template = $stmt->fetch();
|
||||
|
||||
if ($template) {
|
||||
require_once __DIR__ . '/discord/converters/HtmlToDiscordMarkdownConverter.php';
|
||||
require_once __DIR__ . '/discord/DiscordSender.php';
|
||||
require_once __DIR__ . '/src/Translate.php';
|
||||
|
||||
$converter = new \Discord\Converters\HtmlToDiscordMarkdownConverter();
|
||||
|
||||
$images = $converter->extractImages($template['message_content']);
|
||||
$contentWithoutImages = $converter->removeImages($template['message_content']);
|
||||
$content = $converter->convert($contentWithoutImages);
|
||||
|
||||
require_once __DIR__ . '/discord/DiscordSender.php';
|
||||
require_once __DIR__ . '/src/Translate.php';
|
||||
|
||||
$sender = new \Discord\DiscordSender();
|
||||
|
||||
$plainText = $template['message_content'];
|
||||
$plainText = preg_replace('/<br\s*\/?>/i', "\n", $plainText);
|
||||
$plainText = preg_replace('/<\/p>/i', "\n", $plainText);
|
||||
$plainText = preg_replace('/<p[^>]*>/i', '', $plainText);
|
||||
$plainText = strip_tags($plainText);
|
||||
$plainText = html_entity_decode($plainText, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$plainText = trim($plainText);
|
||||
|
||||
$translationButtons = getDiscordTranslationButtons($pdo, $plainText);
|
||||
|
||||
if (!empty($images)) {
|
||||
$sender->sendMessageWithImages((string)$message->channel_id, $content, $images, $translationButtons);
|
||||
} else {
|
||||
$sender->sendMessage((string)$message->channel_id, $content, null, $translationButtons);
|
||||
}
|
||||
|
||||
} else {
|
||||
$message->channel->sendMessage("❌ Plantilla no encontrada: #{$command}");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$message->channel->sendMessage("❌ Error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
function getDiscordTranslationButtons(PDO $pdo, string $text): array
|
||||
{
|
||||
$stmt = $pdo->query("SELECT language_code, flag_emoji FROM supported_languages WHERE is_active = 1");
|
||||
$languages = $stmt->fetchAll();
|
||||
|
||||
if (count($languages) <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Guardar texto en BD con hash consistente
|
||||
$textHash = md5($text);
|
||||
$stmt = $pdo->prepare("INSERT INTO translation_cache (text_hash, original_text) VALUES (?, ?) ON DUPLICATE KEY UPDATE original_text = VALUES(original_text)");
|
||||
$stmt->execute([$textHash, $text]);
|
||||
|
||||
$buttons = [];
|
||||
foreach ($languages as $lang) {
|
||||
$buttons[] = [
|
||||
'label' => $lang['flag_emoji'] . ' ' . strtoupper($lang['language_code']),
|
||||
'custom_id' => 'translate_' . $lang['language_code'] . ':' . $textHash,
|
||||
'style' => 1
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'type' => 1,
|
||||
'components' => array_slice($buttons, 0, 5)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
function handleSlashCommand(PDO $pdo, Message $message, string $content): void
|
||||
{
|
||||
$parts = explode(' ', $content);
|
||||
$cmd = strtolower(str_replace('/', '', $parts[0]));
|
||||
$args = array_slice($parts, 1);
|
||||
|
||||
echo "Comando recibido: {$cmd}" . PHP_EOL;
|
||||
|
||||
switch ($cmd) {
|
||||
case 'comandos':
|
||||
$msg = "📋 **Comandos disponibles:**\n\n";
|
||||
$msg .= "`#comando` - Enviar plantilla\n";
|
||||
$msg .= "`/comandos` - Ver comandos\n";
|
||||
$msg .= "`/setlang [código]` - Establecer idioma\n";
|
||||
$msg .= "`/bienvenida` - Mensaje de bienvenida\n";
|
||||
$msg .= "`/agente` - Cambiar a modo IA";
|
||||
$message->channel->sendMessage($msg);
|
||||
break;
|
||||
|
||||
case 'setlang':
|
||||
$langCode = $args[0] ?? 'es';
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET language_code = ? WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$langCode, $message->author->id]);
|
||||
$message->channel->sendMessage("✅ Idioma actualizado a: " . strtoupper($langCode));
|
||||
break;
|
||||
|
||||
case 'bienvenida':
|
||||
$stmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
|
||||
$config = $stmt->fetch();
|
||||
|
||||
if ($config && $config['is_active']) {
|
||||
require_once __DIR__ . '/discord/DiscordSender.php';
|
||||
|
||||
$text = str_replace('{user_name}', $message->author->username, $config['message_text']);
|
||||
|
||||
// Convertir HTML a texto plano para botones de traducción
|
||||
$plainText = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$plainText = preg_replace('/\s+/', ' ', $plainText);
|
||||
$translationButtons = getDiscordTranslationButtons($pdo, $plainText);
|
||||
|
||||
$sender = new \Discord\DiscordSender();
|
||||
$sender->sendMessage((string)$message->channel_id, $text, null, $translationButtons);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'agente':
|
||||
$builder = \Discord\Builders\MessageBuilder::new();
|
||||
$builder->setContent("🤖 **Selecciona un modo de chat:**");
|
||||
|
||||
$row = new \Discord\Builders\Components\ActionRow();
|
||||
|
||||
$btnBot = \Discord\Builders\Components\Button::new(\Discord\Builders\Components\Button::STYLE_PRIMARY)
|
||||
->setLabel('Seguir con Bot')
|
||||
->setCustomId('chat_mode_bot:' . $message->author->id);
|
||||
$btnIa = \Discord\Builders\Components\Button::new(\Discord\Builders\Components\Button::STYLE_SUCCESS)
|
||||
->setLabel('Platicar con IA')
|
||||
->setCustomId('chat_mode_ia:' . $message->author->id);
|
||||
|
||||
$row->addComponent($btnBot);
|
||||
$row->addComponent($btnIa);
|
||||
$builder->addComponent($row);
|
||||
|
||||
$message->channel->sendMessage($builder);
|
||||
break;
|
||||
|
||||
default:
|
||||
$message->channel->sendMessage("Comando desconocido. Usa /comandos para ver los disponibles.");
|
||||
}
|
||||
}
|
||||
|
||||
function handleRegularMessage(PDO $pdo, Message $message, string $content): void
|
||||
{
|
||||
$stmt = $pdo->prepare("SELECT chat_mode FROM recipients WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$message->author->id]);
|
||||
$recipient = $stmt->fetch();
|
||||
|
||||
if ($recipient && $recipient['chat_mode'] === 'ia') {
|
||||
sendToN8NIA($message, $content);
|
||||
} else {
|
||||
handleAutoTranslationWithButtons($pdo, $message, $content);
|
||||
}
|
||||
}
|
||||
|
||||
function sendToN8NIA(Message $message, string $userMessage): void
|
||||
{
|
||||
require_once __DIR__ . '/includes/env_loader.php';
|
||||
|
||||
$webhookUrl = trim($_ENV['N8N_IA_WEBHOOK_URL_DISCORD'] ?? getenv('N8N_IA_WEBHOOK_URL_DISCORD') ?? '');
|
||||
$webhookUrl = trim($webhookUrl, '"');
|
||||
|
||||
if (!empty($webhookUrl)) {
|
||||
$data = [
|
||||
'user_id' => (string)$message->author->id,
|
||||
'username' => $message->author->username,
|
||||
'message' => $userMessage
|
||||
];
|
||||
|
||||
$jsonData = json_encode($data);
|
||||
|
||||
$ch = curl_init($webhookUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($jsonData)
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode >= 200 && $httpCode < 300) {
|
||||
$result = json_decode($response, true);
|
||||
$reply = $result['reply'] ?? $result['message'] ?? $response;
|
||||
$message->channel->sendMessage($reply);
|
||||
return;
|
||||
}
|
||||
|
||||
error_log("N8N Discord IA Error: HTTP $httpCode - Response: $response");
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/src/IA/Agent.php';
|
||||
$agent = new \IA\Agent();
|
||||
|
||||
try {
|
||||
$response = $agent->generateResponse($userMessage);
|
||||
$message->channel->sendMessage($response);
|
||||
} catch (\Exception $e) {
|
||||
$message->channel->sendMessage("❌ Error: " . $e->getMessage());
|
||||
error_log("IA Agent Discord Error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
function handleAutoTranslationWithButtons(PDO $pdo, Message $message, string $text): void
|
||||
{
|
||||
try {
|
||||
require_once __DIR__ . '/src/Translate.php';
|
||||
$translator = new src\Translate();
|
||||
|
||||
// Detectar idioma del mensaje
|
||||
$detectedLang = $translator->detectLanguage($text) ?? 'es';
|
||||
|
||||
// Obtener idiomas activos de la base de datos
|
||||
$stmt = $pdo->query("SELECT language_code, flag_emoji FROM supported_languages WHERE is_active = 1");
|
||||
$activeLanguages = $stmt->fetchAll();
|
||||
|
||||
if (count($activeLanguages) <= 1) {
|
||||
return; // No hay suficientes idiomas para traducir
|
||||
}
|
||||
|
||||
// Guardar texto en la base de datos con hash consistente
|
||||
$textHash = md5($text);
|
||||
$stmt = $pdo->prepare("INSERT INTO translation_cache (text_hash, original_text) VALUES (?, ?) ON DUPLICATE KEY UPDATE original_text = VALUES(original_text)");
|
||||
$stmt->execute([$textHash, $text]);
|
||||
|
||||
// Preparar botones
|
||||
$buttons = [];
|
||||
foreach ($activeLanguages as $lang) {
|
||||
if ($lang['language_code'] !== $detectedLang) {
|
||||
$buttons[] = [
|
||||
'label' => $lang['flag_emoji'] . ' ' . strtoupper($lang['language_code']),
|
||||
'custom_id' => 'translate_' . $lang['language_code'] . ':' . $textHash,
|
||||
'style' => 1
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Enviar mensaje con botones usando MessageBuilder correctamente
|
||||
if (!empty($buttons)) {
|
||||
$messageText = "🌐 **Traducciones disponibles:**\nHaz clic en una bandera para ver la traducción (solo tú la verás)";
|
||||
|
||||
// Crear MessageBuilder
|
||||
$builder = \Discord\Builders\MessageBuilder::new();
|
||||
$builder->setContent($messageText);
|
||||
|
||||
// Crear ActionRow con botones
|
||||
$row = new \Discord\Builders\Components\ActionRow();
|
||||
|
||||
foreach ($buttons as $btn) {
|
||||
$button = \Discord\Builders\Components\Button::new(\Discord\Builders\Components\Button::STYLE_PRIMARY)
|
||||
->setLabel($btn['label'])
|
||||
->setCustomId($btn['custom_id']);
|
||||
$row->addComponent($button);
|
||||
}
|
||||
|
||||
$builder->addComponent($row);
|
||||
$message->channel->sendMessage($builder);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Discord translation buttons error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
function handleButtonInteraction($interaction, Discord $discord): void
|
||||
{
|
||||
$data = $interaction->data;
|
||||
$customId = $data->custom_id ?? '';
|
||||
|
||||
if (str_starts_with($customId, 'translate_')) {
|
||||
try {
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/src/Translate.php';
|
||||
|
||||
$pdo = getDbConnection();
|
||||
$translator = new src\Translate();
|
||||
|
||||
// Parsear el custom_id: translate_LANG:hash
|
||||
$parts = explode(':', $customId, 2);
|
||||
$targetLang = str_replace('translate_', '', $parts[0]);
|
||||
$textHash = $parts[1] ?? '';
|
||||
|
||||
// Recuperar texto de la base de datos
|
||||
$stmt = $pdo->prepare("SELECT original_text FROM translation_cache WHERE text_hash = ?");
|
||||
$stmt->execute([$textHash]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row) {
|
||||
$builder = \Discord\Builders\MessageBuilder::new()
|
||||
->setContent('❌ Error: Texto no encontrado');
|
||||
$interaction->respondWithMessage($builder, true); // true = ephemeral
|
||||
return;
|
||||
}
|
||||
|
||||
$originalText = $row['original_text'];
|
||||
|
||||
if (empty($originalText)) {
|
||||
$builder = \Discord\Builders\MessageBuilder::new()
|
||||
->setContent('❌ Error: No se pudo recuperar el texto original');
|
||||
$interaction->respondWithMessage($builder, true); // true = ephemeral
|
||||
return;
|
||||
}
|
||||
|
||||
// Detectar idioma original
|
||||
$sourceLang = $translator->detectLanguage($originalText) ?? 'es';
|
||||
|
||||
// Traducir
|
||||
$translated = $translator->translate($originalText, $sourceLang, $targetLang);
|
||||
|
||||
if ($translated) {
|
||||
// Enviar traducción efímera (solo visible para quien presionó)
|
||||
$builder = \Discord\Builders\MessageBuilder::new()
|
||||
->setContent("🌐 **Traducción (" . strtoupper($targetLang) . "):**\n" . $translated);
|
||||
$interaction->respondWithMessage($builder, true); // true = ephemeral
|
||||
} else {
|
||||
$builder = \Discord\Builders\MessageBuilder::new()
|
||||
->setContent('❌ Error al traducir el mensaje');
|
||||
$interaction->respondWithMessage($builder, true); // true = ephemeral
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Discord button translation error: " . $e->getMessage());
|
||||
$builder = \Discord\Builders\MessageBuilder::new()
|
||||
->setContent('❌ Error en el proceso de traducción');
|
||||
$interaction->respondWithMessage($builder, true); // true = ephemeral
|
||||
}
|
||||
} elseif (str_starts_with($customId, 'chat_mode_')) {
|
||||
try {
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
|
||||
$pdo = getDbConnection();
|
||||
|
||||
$parts = explode(':', $customId, 2);
|
||||
$mode = str_replace('chat_mode_', '', $parts[0]);
|
||||
$userId = $parts[1] ?? '';
|
||||
|
||||
if (empty($userId)) {
|
||||
$builder = \Discord\Builders\MessageBuilder::new()
|
||||
->setContent('❌ Error: Usuario no identificado');
|
||||
$interaction->respondWithMessage($builder, true);
|
||||
return;
|
||||
}
|
||||
|
||||
$chatMode = ($mode === 'ia') ? 'ia' : 'bot';
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET chat_mode = ? WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$chatMode, $userId]);
|
||||
|
||||
if ($chatMode === 'ia') {
|
||||
$builder = \Discord\Builders\MessageBuilder::new()
|
||||
->setContent("✅ **Modo IA activado.** Ahora puedes platicar conmigo. Escribe cualquier cosa y la enviaré a la IA.");
|
||||
} else {
|
||||
$builder = \Discord\Builders\MessageBuilder::new()
|
||||
->setContent("✅ **Modo Bot activado.** Ahora puedes usar comandos como #comando y traducción.");
|
||||
}
|
||||
|
||||
$interaction->respondWithMessage($builder, true);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Discord chat mode error: " . $e->getMessage());
|
||||
$builder = \Discord\Builders\MessageBuilder::new()
|
||||
->setContent('❌ Error al cambiar el modo de chat');
|
||||
$interaction->respondWithMessage($builder, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isExistingDiscordUser(PDO $pdo, int $userId): bool
|
||||
{
|
||||
$stmt = $pdo->prepare("SELECT id FROM recipients WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$userId]);
|
||||
return $stmt->fetch() !== false;
|
||||
}
|
||||
|
||||
function sendDiscordWelcomeMessage(PDO $pdo, Member $member, Discord $discord): void
|
||||
{
|
||||
$stmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
|
||||
$config = $stmt->fetch();
|
||||
|
||||
$username = $member->user->username;
|
||||
|
||||
if ($config && $config['is_active']) {
|
||||
$text = str_replace('{user_name}', $username, $config['message_text']);
|
||||
|
||||
try {
|
||||
$member->sendMessage($text);
|
||||
} catch (Exception $e) {
|
||||
echo "No se pudo enviar mensaje privado: " . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$member->sendMessage("¡Hola {$username}! 👋\n\nUsa /comandos para ver los comandos disponibles.");
|
||||
} catch (Exception $e) {
|
||||
echo "No se pudo enviar mensaje privado: " . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerDiscordUser(PDO $pdo, $user): void
|
||||
{
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO recipients (platform_id, name, type, platform, language_code, chat_mode)
|
||||
VALUES (?, ?, 'user', 'discord', 'es', 'agent')
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name)
|
||||
");
|
||||
|
||||
$name = $user->username ?? 'Usuario';
|
||||
$stmt->execute([$user->id, $name]);
|
||||
}
|
||||
|
||||
function sendDiscordWelcomeMessageOnMessage(PDO $pdo, Message $message, string $username): void
|
||||
{
|
||||
$stmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
|
||||
$config = $stmt->fetch();
|
||||
|
||||
if ($config && $config['is_active']) {
|
||||
$text = str_replace('{user_name}', $username, $config['message_text']);
|
||||
$message->author->sendMessage($text);
|
||||
} else {
|
||||
$message->author->sendMessage("¡Hola {$username}! 👋\n\nUsa /comandos para ver los comandos disponibles.");
|
||||
}
|
||||
}
|
||||
|
||||
$discord->run();
|
||||
19
docker/Dockerfile
Executable file
@@ -0,0 +1,19 @@
|
||||
FROM php:8.2-cli
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libcurl4-openssl-dev \
|
||||
libzip-dev \
|
||||
unzip \
|
||||
supervisor \
|
||||
nano \
|
||||
&& pecl install curl \
|
||||
&& docker-php-ext-enable curl \
|
||||
&& docker-php-ext-install pdo_mysql zip \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/var/www/html/docker/supervisord.conf"]
|
||||
47
docker/docker-compose.yml
Executable file
@@ -0,0 +1,47 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
image: php:8.2-cli
|
||||
container_name: lastwar_bot
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ../lastwar:/var/www/html/lastwar
|
||||
working_dir: /var/www/html/lastwar
|
||||
command: /usr/local/bin/supervisord -c /var/www/html/lastwar/docker/supervisord.conf
|
||||
environment:
|
||||
- PHP_DISPLAY_ERRORS=On
|
||||
- PHP_ERROR_REPORTING=E_ALL
|
||||
networks:
|
||||
- bot_network
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: lastwar_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_PASS:-}
|
||||
MYSQL_DATABASE: ${DB_NAME:-bot}
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./db:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- bot_network
|
||||
|
||||
libretranslate:
|
||||
image: libretranslate/libretranslate
|
||||
container_name: lastwar_libretranslate
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5000:5000"
|
||||
networks:
|
||||
- bot_network
|
||||
|
||||
networks:
|
||||
bot_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
11
docker/supervisor_process_queue.conf
Executable file
@@ -0,0 +1,11 @@
|
||||
[program:bot_process_queue]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php /var/www/html/lastwar/process_queue.php
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=www-data
|
||||
numprocs=1
|
||||
stdout_logfile=/var/www/html/lastwar/logs/process_queue.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stderr_logfile=/var/www/html/lastwar/logs/process_queue_error.log
|
||||
redirect_stderr=true
|
||||
11
docker/supervisor_translation_queue.conf
Executable file
@@ -0,0 +1,11 @@
|
||||
[program:bot_translation_queue]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php /var/www/html/lastwar/process_translation_queue.php
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=www-data
|
||||
numprocs=1
|
||||
stdout_logfile=/var/www/html/lastwar/logs/translation_queue.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stderr_logfile=/var/www/html/lastwar/logs/translation_queue_error.log
|
||||
redirect_stderr=true
|
||||
46
docker/supervisord.conf
Executable file
@@ -0,0 +1,46 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/var/www/html/lastwar/logs/supervisor.log
|
||||
logfile_maxbytes=50MB
|
||||
pidfile=/var/run/supervisord.pid
|
||||
childlogdir=/var/www/html/lastwar/logs
|
||||
|
||||
[program:bot_discord]
|
||||
process_name=%(program_name)s
|
||||
command=php /var/www/html/lastwar/discord_bot.php
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=www-data
|
||||
numprocs=1
|
||||
stdout_logfile=/var/www/html/lastwar/logs/discord_bot.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stderr_logfile=/var/www/html/lastwar/logs/discord_bot_error.log
|
||||
redirect_stderr=true
|
||||
|
||||
[program:bot_process_queue]
|
||||
process_name=%(program_name)s
|
||||
command=php /var/www/html/lastwar/process_queue.php
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=www-data
|
||||
numprocs=1
|
||||
stdout_logfile=/var/www/html/lastwar/logs/process_queue.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stderr_logfile=/var/www/html/lastwar/logs/process_queue_error.log
|
||||
redirect_stderr=true
|
||||
|
||||
[program:bot_translation_queue]
|
||||
process_name=%(program_name)s
|
||||
command=php /var/www/html/lastwar/process_translation_queue.php
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=www-data
|
||||
numprocs=1
|
||||
stdout_logfile=/var/www/html/lastwar/logs/translation_queue.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stderr_logfile=/var/www/html/lastwar/logs/translation_queue_error.log
|
||||
redirect_stderr=true
|
||||
|
||||
[group:bot_workers]
|
||||
programs=bot_discord,bot_process_queue,bot_translation_queue
|
||||
priority=999
|
||||
227
edit_message.php
Executable file
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
checkSession();
|
||||
require_once __DIR__ . '/includes/message_handler.php';
|
||||
require_once __DIR__ . '/includes/schedule_helpers.php';
|
||||
|
||||
$pageTitle = 'Editar Mensaje';
|
||||
|
||||
$messageId = $_GET['id'] ?? null;
|
||||
$message = null;
|
||||
$schedule = null;
|
||||
$error = '';
|
||||
|
||||
if (!$messageId) {
|
||||
header('Location: scheduled_messages.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM messages WHERE id = ?");
|
||||
$stmt->execute([$messageId]);
|
||||
$message = $stmt->fetch();
|
||||
|
||||
if ($message) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM schedules WHERE message_id = ? ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute([$messageId]);
|
||||
$schedule = $stmt->fetch();
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM recipients ORDER BY platform, name");
|
||||
$recipients = $stmt->fetchAll();
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM recurrent_messages ORDER BY name");
|
||||
$templates = $stmt->fetchAll();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!$message) {
|
||||
echo "Mensaje no encontrado";
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$result = handleEditMessage($messageId, $_POST);
|
||||
|
||||
if ($result['success']) {
|
||||
header('Location: scheduled_messages.php?updated=1');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['error'];
|
||||
}
|
||||
}
|
||||
|
||||
$sendType = 'later';
|
||||
if ($schedule) {
|
||||
if ($schedule['is_recurring']) {
|
||||
$sendType = 'recurring';
|
||||
} elseif (strtotime($schedule['send_time']) <= time()) {
|
||||
$sendType = 'now';
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-pencil"></i> Editar Mensaje</h2>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" id="messageForm">
|
||||
<input type="hidden" name="action" value="update">
|
||||
<input type="hidden" name="schedule_id" value="<?= $schedule['id'] ?? '' ?>">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Contenido del Mensaje</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plantilla (opcional)</label>
|
||||
<select class="form-select" id="templateSelect">
|
||||
<option value="">-- Seleccionar plantilla --</option>
|
||||
<?php foreach ($templates as $template): ?>
|
||||
<option value="<?= htmlspecialchars($template['message_content']) ?>">
|
||||
<?= htmlspecialchars($template['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mensaje</label>
|
||||
<textarea name="content" id="messageContent" class="form-control" rows="10" required><?= htmlspecialchars($message['content'] ?? '') ?></textarea>
|
||||
<small class="text-muted">Usa HTML básico: <b>, <i>, <u>, <a href>, <img></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Destinatario</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plataforma</label>
|
||||
<select name="platform" id="platformSelect" class="form-select" required>
|
||||
<option value="">-- Seleccionar --</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Destinatario</label>
|
||||
<select name="recipient_id" id="recipientSelect" class="form-select" required>
|
||||
<option value="">Selecciona una plataforma primero</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Programación</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tipo de envío</label>
|
||||
<select name="send_type" id="sendType" class="form-select" required>
|
||||
<option value="now" <?= $sendType === 'now' ? 'selected' : '' ?>>Enviar ahora</option>
|
||||
<option value="later" <?= $sendType === 'later' ? 'selected' : '' ?>>Programar para después</option>
|
||||
<option value="recurring" <?= $sendType === 'recurring' ? 'selected' : '' ?>>Recurrente</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="datetimeField" style="display: <?= $sendType === 'later' ? 'block' : 'none' ?>;">
|
||||
<label class="form-label">Fecha y hora</label>
|
||||
<input type="datetime-local" name="send_datetime" class="form-control" value="<?= $schedule ? date('Y-m-d\TH:i', strtotime($schedule['send_time'])) : '' ?>">
|
||||
</div>
|
||||
|
||||
<div id="recurringFields" style="display: <?= $sendType === 'recurring' ? 'block' : 'none' ?>;">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Días</label>
|
||||
<select name="recurring_days" class="form-select">
|
||||
<option value="monday" <?= ($schedule['recurring_days'] ?? '') === 'monday' ? 'selected' : '' ?>>Lunes</option>
|
||||
<option value="tuesday" <?= ($schedule['recurring_days'] ?? '') === 'tuesday' ? 'selected' : '' ?>>Martes</option>
|
||||
<option value="wednesday" <?= ($schedule['recurring_days'] ?? '') === 'wednesday' ? 'selected' : '' ?>>Miércoles</option>
|
||||
<option value="thursday" <?= ($schedule['recurring_days'] ?? '') === 'thursday' ? 'selected' : '' ?>>Jueves</option>
|
||||
<option value="friday" <?= ($schedule['recurring_days'] ?? '') === 'friday' ? 'selected' : '' ?>>Viernes</option>
|
||||
<option value="saturday" <?= ($schedule['recurring_days'] ?? '') === 'saturday' ? 'selected' : '' ?>>Sábado</option>
|
||||
<option value="sunday" <?= ($schedule['recurring_days'] ?? '') === 'sunday' ? 'selected' : '' ?>>Domingo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hora</label>
|
||||
<input type="time" name="recurring_time" class="form-control" value="<?= $schedule['recurring_time'] ?? '09:00' ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-save"></i> Guardar Cambios
|
||||
</button>
|
||||
<a href="scheduled_messages.php" class="btn btn-outline-secondary">Cancelar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const recipients = <?= json_encode($recipients) ?>;
|
||||
const currentRecipientId = <?= $schedule['recipient_id'] ?? 'null' ?>;
|
||||
const currentPlatform = '<?= $recipients ? array_search($currentRecipientId, array_column($recipients, 'id')) !== false ? $recipients[array_search($currentRecipientId, array_column($recipients, 'id'))]['platform'] : '' : '' ?>';
|
||||
|
||||
document.getElementById('templateSelect').addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
document.getElementById('messageContent').value = this.value;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('platformSelect').addEventListener('change', function() {
|
||||
const platform = this.value;
|
||||
const recipientSelect = document.getElementById('recipientSelect');
|
||||
|
||||
recipientSelect.innerHTML = '<option value="">-- Seleccionar --</option>';
|
||||
|
||||
if (platform) {
|
||||
const filtered = recipients.filter(r => r.platform === platform);
|
||||
filtered.forEach(r => {
|
||||
const option = document.createElement('option');
|
||||
option.value = r.id;
|
||||
option.textContent = r.name + ' (' + r.type + ')';
|
||||
if (r.id === currentRecipientId) option.selected = true;
|
||||
recipientSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('sendType').addEventListener('change', function() {
|
||||
const datetimeField = document.getElementById('datetimeField');
|
||||
const recurringFields = document.getElementById('recurringFields');
|
||||
|
||||
datetimeField.style.display = this.value === 'later' ? 'block' : 'none';
|
||||
recurringFields.style.display = this.value === 'recurring' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
if (currentPlatform) {
|
||||
document.getElementById('platformSelect').value = currentPlatform;
|
||||
document.getElementById('platformSelect').dispatchEvent(new Event('change'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
89
edit_recurrent_message.php
Executable file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
checkSession();
|
||||
require_once __DIR__ . '/includes/activity_logger.php';
|
||||
|
||||
$pageTitle = 'Editar Plantilla';
|
||||
|
||||
$templateId = $_GET['id'] ?? null;
|
||||
$template = null;
|
||||
$error = '';
|
||||
|
||||
if (!$templateId) {
|
||||
header('Location: recurrentes.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
$stmt = $pdo->prepare("SELECT * FROM recurrent_messages WHERE id = ?");
|
||||
$stmt->execute([$templateId]);
|
||||
$template = $stmt->fetch();
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!$template) {
|
||||
echo "Plantilla no encontrada";
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$name = $_POST['name'];
|
||||
$messageContent = $_POST['message_content'];
|
||||
$telegramCommand = $_POST['telegram_command'] ?? null;
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE recurrent_messages
|
||||
SET name = ?, message_content = ?, telegram_command = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$name, $messageContent, $telegramCommand, $templateId]);
|
||||
|
||||
logActivity(getCurrentUserId(), 'update_template', "Plantilla actualizada: $name");
|
||||
header('Location: recurrentes.php?updated=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-pencil"></i> Editar Plantilla</h2>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre</label>
|
||||
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($template['name']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Comando de Telegram (sin #)</label>
|
||||
<input type="text" name="telegram_command" class="form-control" value="<?= htmlspecialchars($template['telegram_command'] ?? '') ?>" placeholder="ejemplo">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contenido</label>
|
||||
<textarea name="message_content" class="form-control" rows="10" required><?= htmlspecialchars($template['message_content']) ?></textarea>
|
||||
<small class="text-muted">Usa HTML básico: <b>, <i>, <u>, <a href>, <img></small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Guardar
|
||||
</button>
|
||||
<a href="recurrentes.php" class="btn btn-outline-secondary">Cancelar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
191
enviar_plantilla.php
Executable file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
checkSession();
|
||||
require_once __DIR__ . '/includes/message_handler.php';
|
||||
|
||||
$pageTitle = 'Enviar Plantilla';
|
||||
|
||||
$templates = [];
|
||||
$recipients = [];
|
||||
|
||||
try {
|
||||
$pdo = getDbConnection();
|
||||
$stmt = $pdo->query("SELECT * FROM recurrent_messages ORDER BY name");
|
||||
$templates = $stmt->fetchAll();
|
||||
|
||||
$stmt = $pdo->query("SELECT * FROM recipients ORDER BY platform, name");
|
||||
$recipients = $stmt->fetchAll();
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$sendError = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$templateId = $_POST['template_id'];
|
||||
$platform = $_POST['platform'];
|
||||
$recipientIds = $_POST['recipient_ids'] ?? [];
|
||||
$sendType = $_POST['send_type'] ?? 'now';
|
||||
|
||||
$template = null;
|
||||
foreach ($templates as $t) {
|
||||
if ($t['id'] == $templateId) {
|
||||
$template = $t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$template) {
|
||||
$sendError = 'Plantilla no encontrada';
|
||||
} elseif (empty($recipientIds)) {
|
||||
$sendError = 'Selecciona al menos un destinatario';
|
||||
} else {
|
||||
foreach ($recipientIds as $recipientId) {
|
||||
$recipient = null;
|
||||
foreach ($recipients as $r) {
|
||||
if ($r['id'] == $recipientId && $r['platform'] === $platform) {
|
||||
$recipient = $r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($recipient) {
|
||||
$sendTime = $sendType === 'now' ? date('Y-m-d H:i:s') : ($_POST['send_datetime'] ?? date('Y-m-d H:i:s'));
|
||||
|
||||
$messageId = createMessage([
|
||||
'user_id' => getCurrentUserId(),
|
||||
'content' => $template['message_content']
|
||||
]);
|
||||
|
||||
$scheduleId = createSchedule([
|
||||
'message_id' => $messageId,
|
||||
'recipient_id' => $recipientId,
|
||||
'send_time' => $sendTime,
|
||||
'status' => 'pending'
|
||||
]);
|
||||
|
||||
$success .= "Enviado a {$recipient['name']}<br>";
|
||||
}
|
||||
}
|
||||
|
||||
if ($sendType === 'now') {
|
||||
require_once __DIR__ . '/process_queue.php';
|
||||
processScheduledMessages();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-send"></i> Enviar Plantilla</h2>
|
||||
</div>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= $success ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($sendError)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($sendError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Seleccionar Plantilla</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (empty($templates)): ?>
|
||||
<p class="text-muted">No hay plantillas disponibles</p>
|
||||
<?php else: ?>
|
||||
<div class="list-group">
|
||||
<?php foreach ($templates as $template): ?>
|
||||
<label class="list-group-item">
|
||||
<input type="radio" name="template_id" value="<?= $template['id'] ?>" <?= $_POST['template_id'] == $template['id'] ? 'checked' : '' ?> required>
|
||||
<strong><?= htmlspecialchars($template['name']) ?></strong>
|
||||
<?php if ($template['telegram_command']): ?>
|
||||
<span class="badge bg-secondary">#<?= htmlspecialchars($template['telegram_command']) ?></span>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white border-0">
|
||||
<h5 class="mb-0">Destinatarios</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plataforma</label>
|
||||
<select name="platform" id="platformSelect" class="form-select" required>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Seleccionar destinatarios</label>
|
||||
<div id="recipientsList" class="border rounded p-2" style="max-height: 200px; overflow-y: auto;">
|
||||
<p class="text-muted small">Selecciona una plataforma primero</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tipo de envío</label>
|
||||
<select name="send_type" class="form-select">
|
||||
<option value="now">Enviar ahora</option>
|
||||
<option value="later">Programar</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="datetimeField" style="display: none;">
|
||||
<label class="form-label">Fecha y hora</label>
|
||||
<input type="datetime-local" name="send_datetime" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100">
|
||||
<i class="bi bi-send"></i> Enviar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const recipients = <?= json_encode($recipients) ?>;
|
||||
|
||||
document.getElementById('platformSelect').addEventListener('change', function() {
|
||||
const platform = this.value;
|
||||
const list = document.getElementById('recipientsList');
|
||||
|
||||
const filtered = recipients.filter(r => r.platform === platform);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = '<p class="text-muted small">No hay destinatarios para esta plataforma</p>';
|
||||
} else {
|
||||
list.innerHTML = filtered.map(r => `
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="recipient_ids[]" value="${r.id}" id="recipient_${r.id}">
|
||||
<label class="form-check-label" for="recipient_${r.id}">${r.name} (${r.type})</label>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('select[name="send_type"]').addEventListener('change', function() {
|
||||
document.getElementById('datetimeField').style.display = this.value === 'later' ? 'block' : 'none';
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
380
flujos/DIscord.json
Executable file
@@ -0,0 +1,380 @@
|
||||
{
|
||||
"name": "Discord",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "42e803ae-8aee-4b1c-858a-6c6d3fbb6230",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2.1,
|
||||
"position": [
|
||||
-464,
|
||||
112
|
||||
],
|
||||
"id": "52d47c40-63c3-4d17-ab86-d0e19a9ca911",
|
||||
"name": "Webhook",
|
||||
"webhookId": "42e803ae-8aee-4b1c-858a-6c6d3fbb6230"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $('Webhook').item.json.body.message }}",
|
||||
"options": {
|
||||
"systemMessage": "────────────────────────────────────────────────────────────────\nBASE DE DATOS - IMPORTANTE\n────────────────────────────────────────────────────────────────\n\nUsa la herramienta 'MySQL Tool' para buscar información.\n\nTabla: knowledge_base\nBase de datos: lastwar_mysql\n\nEjemplos de consultas:\n- SELECT * FROM knowledge_base WHERE topic='Heroes' LIMIT 10\n- SELECT * FROM knowledge_base WHERE entity_name LIKE '%Mason%' LIMIT 5\n- SELECT * FROM knowledge_base WHERE topic='General' LIMIT 10\n\n────────────────────────────────────────────────────────────────\nREGLAS\n────────────────────────────────────────────────────────────────\n\n1. NUNCA inventes información - usa solo datos de MySQL\n2. Si no hay resultados, dice \"No encontré información\"\n3. Los héroes reales incluyen: Mason, Murphy, Kimberly, Blade, Shadow Marshall, Striker Carlie, Thunder Tesla, etc.\n\n────────────────────────────────────────────────────────────────\nIDIOMA\n────────────────────────────────────────────────────────────────\n\n- ESCRIBE en el mismo idioma que el usuario\n- Si el usuario escribe en português, responde en português\n- Si el usuario escribe en inglés, responde en inglés"
|
||||
}
|
||||
},
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
176,
|
||||
0
|
||||
],
|
||||
"id": "52957df8-1036-40f9-84bf-c1411780aa05",
|
||||
"name": "AI Agent"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"sessionIdType": "customKey",
|
||||
"sessionKey": "={{ $('Webhook').item.json.body.user_id }}",
|
||||
"contextWindowLength": 10
|
||||
},
|
||||
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
||||
"typeVersion": 1.3,
|
||||
"position": [
|
||||
176,
|
||||
208
|
||||
],
|
||||
"id": "eb69403c-6ccd-4ea0-8840-b58dbb06c156",
|
||||
"name": "Simple Memory"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const maxLen = 4000;\nlet inputText = $input.first().json.output || $input.first().json.text || '';\n\nif (!inputText || inputText.length <= maxLen) {\n return [{ json: { text_chunk: inputText } }];\n}\n\nconst chunks = [];\nlet currentPos = 0;\n\nwhile (currentPos < inputText.length) {\n let chunk = inputText.substring(currentPos, currentPos + maxLen);\n let lastNewline = chunk.lastIndexOf('\\n');\n\n if (lastNewline !== -1 && currentPos + maxLen < inputText.length) {\n chunk = inputText.substring(currentPos, currentPos + lastNewline + 1);\n currentPos += lastNewline + 1;\n } else {\n currentPos += maxLen;\n }\n chunks.push({ json: { text_chunk: chunk } });\n}\n\nreturn chunks;"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
496,
|
||||
0
|
||||
],
|
||||
"id": "54cb88eb-9980-4966-965c-98f6f32dbc7d",
|
||||
"name": "Dividir"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"description": "Eres un asistente interno especializado en Last War Survival Game."
|
||||
},
|
||||
"type": "@n8n/n8n-nodes-langchain.toolThink",
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
288,
|
||||
208
|
||||
],
|
||||
"id": "7632a9ee-8f28-4089-9dfc-aad2c36e738b",
|
||||
"name": "Think"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "chooseBranch",
|
||||
"useDataOfInput": 2
|
||||
},
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"id": "0f2fa41b-a2b1-435c-a11b-7dd085914225",
|
||||
"name": "Merge"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": []
|
||||
},
|
||||
"includeOtherFields": true,
|
||||
"options": {
|
||||
"ignoreConversionErrors": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
-208,
|
||||
16
|
||||
],
|
||||
"id": "817d5af2-1a3f-469a-94c3-d387f073c90d",
|
||||
"name": "Edit Fields"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"guildId": {
|
||||
"__rl": true,
|
||||
"value": "1385792757980987523",
|
||||
"mode": "list",
|
||||
"cachedResultName": "El servidor de Pruebas web",
|
||||
"cachedResultUrl": "https://discord.com/channels/1385792757980987523"
|
||||
},
|
||||
"channelId": {
|
||||
"__rl": true,
|
||||
"value": "1471360653896847402",
|
||||
"mode": "list",
|
||||
"cachedResultName": "general",
|
||||
"cachedResultUrl": "https://discord.com/channels/1385792757980987523/1471360653896847402"
|
||||
},
|
||||
"content": "={{ $json.body.message }}",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.discord",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
-240,
|
||||
224
|
||||
],
|
||||
"id": "98ef93b5-bc6b-4a7a-99a2-801e932f1047",
|
||||
"name": "Send a message",
|
||||
"webhookId": "0bbbede1-c843-4188-8e3e-00effe69ff6e",
|
||||
"credentials": {
|
||||
"discordBotApi": {
|
||||
"id": "5CsYocisvR0L2b4A",
|
||||
"name": "Discord Bot account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"guildId": {
|
||||
"__rl": true,
|
||||
"value": "1385792757980987523",
|
||||
"mode": "list",
|
||||
"cachedResultName": "El servidor de Pruebas web",
|
||||
"cachedResultUrl": "https://discord.com/channels/1385792757980987523"
|
||||
},
|
||||
"channelId": {
|
||||
"__rl": true,
|
||||
"value": "1471360653896847402",
|
||||
"mode": "list",
|
||||
"cachedResultName": "general",
|
||||
"cachedResultUrl": "https://discord.com/channels/1385792757980987523/1471360653896847402"
|
||||
},
|
||||
"content": "={{ $json.text_chunk }}",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.discord",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
672,
|
||||
0
|
||||
],
|
||||
"id": "ca5b0946-6083-4bae-ac8e-7c4509049363",
|
||||
"name": "Send a message1",
|
||||
"webhookId": "b517e733-400b-40ce-ad0d-c7fbc36f3b5c",
|
||||
"credentials": {
|
||||
"discordBotApi": {
|
||||
"id": "5CsYocisvR0L2b4A",
|
||||
"name": "Discord Bot account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "select",
|
||||
"table": {
|
||||
"__rl": true,
|
||||
"value": "knowledge_base",
|
||||
"mode": "name"
|
||||
},
|
||||
"options": {
|
||||
"limit": 10
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.mySqlTool",
|
||||
"typeVersion": 2.5,
|
||||
"position": [
|
||||
384,
|
||||
256
|
||||
],
|
||||
"id": "8ce4bcc7-f4e3-43b7-9b15-fc849eb99b5c",
|
||||
"name": "MySQL Tool",
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
"id": "GxTgZPkOMennctva",
|
||||
"name": "MySQL account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"model": "llama-3.3-70b-versatile",
|
||||
"options": {
|
||||
"temperature": 0.1
|
||||
}
|
||||
},
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatGroq",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
16,
|
||||
224
|
||||
],
|
||||
"id": "0aca1a17-b9a8-40bc-a087-77731df514dc",
|
||||
"name": "Groq Chat Model",
|
||||
"credentials": {
|
||||
"groqApi": {
|
||||
"id": "MMphZipiXwg84ObQ",
|
||||
"name": "Groq account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
144,
|
||||
384
|
||||
],
|
||||
"id": "f1f54ecd-6938-4218-a912-d51451c2f2d5",
|
||||
"name": "Google Gemini Chat Model",
|
||||
"credentials": {
|
||||
"googlePalmApi": {
|
||||
"id": "qsNvxBK1JwvqckgL",
|
||||
"name": "Google Gemini(PaLM) Api account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Send a message",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"AI Agent": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Dividir",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Simple Memory": {
|
||||
"ai_memory": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "ai_memory",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Dividir": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send a message1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Think": {
|
||||
"ai_tool": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "ai_tool",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Merge": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Merge",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Merge",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"MySQL Tool": {
|
||||
"ai_tool": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "ai_tool",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Groq Chat Model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Google Gemini Chat Model": {
|
||||
"ai_languageModel": [
|
||||
[]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": ""
|
||||
},
|
||||
"id": "",
|
||||
"tags": []
|
||||
}
|
||||
10401
flujos/knowledge_base.sql
Executable file
BIN
galeria/Asedio.jpg
Executable file
|
After Width: | Height: | Size: 50 KiB |
BIN
galeria/CAOS.png
Executable file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
galeria/Carbon.png
Executable file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
galeria/Dia1.jpg
Executable file
|
After Width: | Height: | Size: 148 KiB |
BIN
galeria/Dia2.jpg
Executable file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
galeria/Dia3.jpg
Executable file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
galeria/Dia4.jpg
Executable file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
galeria/Dia5.jpg
Executable file
|
After Width: | Height: | Size: 131 KiB |
BIN
galeria/Dia6.jpg
Executable file
|
After Width: | Height: | Size: 139 KiB |
BIN
galeria/Escudo.jpg
Executable file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
galeria/Escudos.png
Executable file
|
After Width: | Height: | Size: 107 KiB |
BIN
galeria/Radar.png
Executable file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
galeria/S1-Skill-Points.png
Executable file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
galeria/Schuyler-exclusive-weapon-shard.png
Executable file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
galeria/Screenshot-from-2024-02-26-16-34-21.png
Executable file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-11-06.png
Executable file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-15-35.png
Executable file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-18-40.png
Executable file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-30-48.png
Executable file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-32-10.png
Executable file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
galeria/Screenshot-from-2024-03-10-23-36-14.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
galeria/Screenshot-from-2024-03-19-18-30-28.png
Executable file
|
After Width: | Height: | Size: 21 KiB |
BIN
galeria/Screenshot-from-2024-03-19-18-32-53.png
Executable file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
galeria/Screenshot-from-2024-03-19-18-41-51.png
Executable file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
galeria/Screenshot-from-2024-03-25-23-18-07.png
Executable file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
galeria/Screenshot-from-2024-04-05-04-04-58.png
Executable file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
galeria/Screenshot-from-2024-05-06-04-02-00.png
Executable file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
galeria/Screenshot-from-2024-06-10-10-54-29.png
Executable file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
galeria/Screenshot-from-2024-08-19-13-37-42.png
Executable file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
galeria/Screenshot-from-2024-08-19-13-38-33.png
Executable file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
galeria/Screenshot-from-2024-08-19-13-38-44.png
Executable file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
galeria/Screenshot-from-2024-08-19-13-38-52.png
Executable file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
galeria/Screenshot-from-2024-10-08-11-48-47.png
Executable file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
galeria/Screenshot-from-2025-01-06-06-13-18.png
Executable file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
galeria/Screenshot_2025-06-27-19-29-45-460_com.fun.lastwar.gp.jpg
Executable file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
galeria/Season-2-icon.png
Executable file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
galeria/Titanium-alloy-1.png
Executable file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
galeria/Titanium-alloy.png
Executable file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
galeria/Tormenta.jpeg
Executable file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
galeria/Tormenta.jpg
Executable file
|
After Width: | Height: | Size: 403 KiB |
BIN
galeria/Tormenta_arena.jpeg
Executable file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
galeria/Universal-hero-exclusive-weapon-share.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
galeria/adam-exclusive-weapon-shard.png
Executable file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
galeria/b.png
Executable file
|
After Width: | Height: | Size: 66 KiB |
BIN
galeria/black-market-cash.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
galeria/buff-basic-resource-output.png
Executable file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
galeria/buff-unit-healing-speed.png
Executable file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
galeria/buff-units-morale.png
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
galeria/campaign-medals.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
galeria/caos.png
Executable file
|
After Width: | Height: | Size: 190 KiB |
BIN
galeria/carlie-exclusive-weapon-shard.png
Executable file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
galeria/copper.png
Executable file
|
After Width: | Height: | Size: 19 KiB |
BIN
galeria/crimson-memory-nameplate.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
galeria/cropped-favicon-192x192.jpeg
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
galeria/cure.png
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
galeria/descarga (1).jpeg
Executable file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
galeria/diamond.png
Executable file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
galeria/drone-battle-data.png
Executable file
|
After Width: | Height: | Size: 11 KiB |
BIN
galeria/drone-chip-chest-common.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
galeria/drone-chip-chest-epic.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
galeria/drone-chip-chest-legendary.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
galeria/drone-chip-chest-rare.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
galeria/drone-component-level-1.png
Executable file
|
After Width: | Height: | Size: 11 KiB |
BIN
galeria/drone-component-level-2.png
Executable file
|
After Width: | Height: | Size: 11 KiB |
BIN
galeria/drone-component-level-3.png
Executable file
|
After Width: | Height: | Size: 11 KiB |