Initial commit - Last War messaging system

This commit is contained in:
2026-02-19 01:33:28 -06:00
commit 38a8447a64
2162 changed files with 216183 additions and 0 deletions

50
.env Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'; ?>

View 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);
}
}

View 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
View 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

File diff suppressed because it is too large Load Diff

90
configure_webhook.php Executable file
View 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
View 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
View 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
View 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);
}
}

View 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'];
}
}

View 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('/&nbsp;/', ' ', $content);
$content = preg_replace('/&amp;/', '&', $content);
$content = preg_replace('/&lt;/', '<', $content);
$content = preg_replace('/&gt;/', '>', $content);
$content = preg_replace('/&quot;/', '"', $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
View 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
View 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
View 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:

View 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

View 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
View 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
View 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: &lt;b&gt;, &lt;i&gt;, &lt;u&gt;, &lt;a href&gt;, &lt;img&gt;</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
View 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: &lt;b&gt;, &lt;i&gt;, &lt;u&gt;, &lt;a href&gt;, &lt;img&gt;</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
View 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
View 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

File diff suppressed because it is too large Load Diff

BIN
galeria/Asedio.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
galeria/CAOS.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
galeria/Carbon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
galeria/Dia1.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
galeria/Dia2.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
galeria/Dia3.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
galeria/Dia4.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
galeria/Dia5.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
galeria/Dia6.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
galeria/Escudo.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
galeria/Escudos.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
galeria/Radar.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
galeria/S1-Skill-Points.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
galeria/Season-2-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
galeria/Titanium-alloy-1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
galeria/Titanium-alloy.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
galeria/Tormenta.jpeg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
galeria/Tormenta.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

BIN
galeria/Tormenta_arena.jpeg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
galeria/b.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
galeria/black-market-cash.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
galeria/buff-units-morale.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
galeria/campaign-medals.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
galeria/caos.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
galeria/copper.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
galeria/cure.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
galeria/descarga (1).jpeg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
galeria/diamond.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
galeria/drone-battle-data.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
galeria/drone-chip-chest-epic.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
galeria/drone-chip-chest-rare.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show More