Commit inicial con archivos existentes
7
.env
Executable file
@@ -0,0 +1,7 @@
|
||||
# Auto-generado desde variables de entorno
|
||||
# NO editar manualmente - los cambios se perderán al reiniciar
|
||||
|
||||
APP_ENV=pruebas
|
||||
APP_DEBUG=false
|
||||
|
||||
APP_ENVIRONMENT=pruebas
|
||||
23
.env.example
Executable file
@@ -0,0 +1,23 @@
|
||||
# Configuración de la aplicación
|
||||
APP_ENV=development
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
# Configuración de la base de datos
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=nombre_base_datos
|
||||
DB_USER=usuario
|
||||
DB_PASS=contraseña
|
||||
DB_DIALECT=mysql
|
||||
|
||||
# Configuración de JWT
|
||||
JWT_SECRET=clave_secreta_segura
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRATION=3600
|
||||
|
||||
# Configuración de Discord
|
||||
DISCORD_GUILD_ID=tu_guild_id
|
||||
DISCORD_CLIENT_ID=tu_client_id
|
||||
DISCORD_CLIENT_SECRET=tu_client_secret
|
||||
DISCORD_BOT_TOKEN=tu_bot_token
|
||||
49
.env.pruebas
Executable file
@@ -0,0 +1,49 @@
|
||||
# Configuración de la aplicación
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://ponsprueba.ddns.net
|
||||
|
||||
# Configuración de la base de datos
|
||||
DB_HOST=10.10.4.17
|
||||
DB_PORT=3391
|
||||
DB_NAME=bot2
|
||||
DB_USER=nickpons666
|
||||
DB_PASS=MiPo6425@@
|
||||
DB_DIALECT=mysql
|
||||
|
||||
# Configuración de JWT
|
||||
JWT_SECRET=19c5020fa8207d2c3b9e82f430784667e001f1eb733848922f7bcb9be98f93c2
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRATION=3600
|
||||
|
||||
# Configuración de Discord
|
||||
DISCORD_GUILD_ID=1338327171013541999
|
||||
DISCORD_CLIENT_ID=1385790344594985061
|
||||
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
|
||||
TEST_ENV_LOAD=caos_cargado
|
||||
|
||||
LIBRETRANSLATE_URL=http://10.10.4.17:5000
|
||||
|
||||
N8N_URL=https://n8n-dragon.ddns.net
|
||||
N8N_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4MWY4YjU3YS0wMTg2LTQ1NTctOWZlMC1jYWUxNjZlYzZlMTkiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzU1OTMwODM5fQ.2tLbddyhMTKplp9n-qVNiAgQCUj2YEvVASwLnNjgCt0
|
||||
|
||||
# -----------------------------------------
|
||||
# --- Configuración para la migración a n8n ---
|
||||
# -----------------------------------------
|
||||
# URL base de esta aplicación, para que n8n pueda llamarla.
|
||||
APP_BASE_URL=https://ponsprueba.ddns.net
|
||||
|
||||
# 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-dragon.ddns.net/webhooktest/ia"
|
||||
N8N_IA_WEBHOOK_URL="https://n8n-dragon.ddns.net/webhook/ia"
|
||||
N8N_IA_WEBHOOK_URL_DISCORD="https://n8n-dragon.ddns.net/webhook/42e803ae-8aee-4b1c-858a-6c6d3fbb6230"
|
||||
49
.env.reod
Executable file
@@ -0,0 +1,49 @@
|
||||
# Configuración de la aplicación
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://reod-dragon.ddns.net
|
||||
|
||||
# Configuración de la base de datos
|
||||
DB_HOST=10.10.4.17
|
||||
DB_PORT=3390
|
||||
DB_NAME=bot
|
||||
DB_USER=nickpons666
|
||||
DB_PASS=MiPo6425@@
|
||||
DB_DIALECT=mysql
|
||||
|
||||
# Configuración de JWT
|
||||
JWT_SECRET=19c5020fa8207d2c3b9e82f430784667e001f1eb733848922f7bcb9be98f93c2
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRATION=3600
|
||||
|
||||
# Configuración de Discord
|
||||
DISCORD_GUILD_ID=1338327171013541999
|
||||
DISCORD_CLIENT_ID=1385790344594985061
|
||||
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
|
||||
TEST_ENV_LOAD=caos_cargado
|
||||
|
||||
LIBRETRANSLATE_URL=http://10.10.4.17:5000
|
||||
|
||||
N8N_URL=https://n8n-dragon.ddns.net
|
||||
N8N_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4MWY4YjU3YS0wMTg2LTQ1NTctOWZlMC1jYWUxNjZlYzZlMTkiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzU1OTMwODM5fQ.2tLbddyhMTKplp9n-qVNiAgQCUj2YEvVASwLnNjgCt0
|
||||
|
||||
# -----------------------------------------
|
||||
# --- Configuración para la migración a n8n ---
|
||||
# -----------------------------------------
|
||||
# URL base de esta aplicación, para que n8n pueda llamarla.
|
||||
APP_BASE_URL=https://reod-dragon.ddns.net
|
||||
|
||||
# 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-dragon.ddns.net/webhook/telegram-unified"
|
||||
N8N_IA_WEBHOOK_URL="https://n8n-dragon.ddns.net/webhook/ia"
|
||||
N8N_IA_WEBHOOK_URL_DISCORD="https://n8n-dragon.ddns.net/webhook/42e803ae-8aee-4b1c-858a-6c6d3fbb6230"
|
||||
114
.htaccess
Executable file
@@ -0,0 +1,114 @@
|
||||
# Configuración básica
|
||||
Options -Indexes +FollowSymLinks -MultiViews
|
||||
|
||||
# Habilitar reescritura de URLs
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Asegurar que el servidor siga los enlaces simbólicos
|
||||
Options +FollowSymLinks
|
||||
|
||||
# Regla para el webhook de Telegram - debe ser lo primero
|
||||
RewriteCond %{REQUEST_URI} ^/telegram_bot_webhook\.php [NC]
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Reglas para otros archivos de webhook
|
||||
RewriteRule ^(telegram_webhook|test_webhook|set_webhook)\.php$ - [L,NC]
|
||||
|
||||
# Excluir archivos específicos de la verificación de sesión
|
||||
RewriteCond %{REQUEST_URI} !^/login\.php [NC]
|
||||
RewriteCond %{REQUEST_URI} !^/assets/ [NC]
|
||||
RewriteCond %{REQUEST_URI} !^/translate_proxy\.php [NC]
|
||||
RewriteCond %{REQUEST_URI} !\.(css|js|jpe?g|png|gif|ico|svg|woff2?|ttf|eot|json|txt|map)$ [NC]
|
||||
|
||||
# Para el resto de las rutas, redirigir a login.php si no hay sesión
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} !^/translate_proxy\.php [NC]
|
||||
RewriteRule ^(.*)$ /login.php [L,QSA]
|
||||
</IfModule>
|
||||
|
||||
# Configuración de seguridad
|
||||
<IfModule mod_headers.c>
|
||||
# Protección básica de cabeceras
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
|
||||
# Habilita la política de seguridad de contenido (CSP) - Ajusta según sea necesario
|
||||
# Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;"
|
||||
|
||||
# Habilita HSTS (solo para HTTPS)
|
||||
# Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
|
||||
# Configuración de referrer policy
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# Eliminar cabeceras que revelan información
|
||||
Header unset X-Powered-By
|
||||
Header unset X-Pingback
|
||||
Header unset Server
|
||||
Header unset X-AspNet-Version
|
||||
Header unset X-AspNetMvc-Version
|
||||
</IfModule>
|
||||
|
||||
# Proteger archivos sensibles
|
||||
<FilesMatch "^\.env$|composer\.(json|lock)|package(-lock)?\.json|.*\.(sql|log|bak|swp|swo|gitignore|gitattributes|htaccess|htpasswd|DS_Store)$">
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# Deshabilitar la visualización de directorios
|
||||
Options -Indexes
|
||||
|
||||
# Prevenir acceso a archivos ocultos
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Configuración de caché para mejorar el rendimiento
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType image/jpg "access plus 1 year"
|
||||
ExpiresByType image/jpeg "access plus 1 year"
|
||||
ExpiresByType image/gif "access plus 1 year"
|
||||
ExpiresByType image/png "access plus 1 year"
|
||||
ExpiresByType text/css "access plus 1 month"
|
||||
ExpiresByType application/pdf "access plus 1 month"
|
||||
ExpiresByType text/x-javascript "access plus 1 month"
|
||||
ExpiresByType application/x-shockwave-flash "access plus 1 month"
|
||||
ExpiresByType image/x-icon "access plus 1 year"
|
||||
ExpiresDefault "access plus 2 days"
|
||||
</IfModule>
|
||||
|
||||
# Comprimir archivos para mejorar el rendimiento
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/plain
|
||||
AddOutputFilterByType DEFLATE text/html
|
||||
AddOutputFilterByType DEFLATE text/xml
|
||||
AddOutputFilterByType DEFLATE text/css
|
||||
AddOutputFilterByType DEFLATE application/xml
|
||||
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||
AddOutputFilterByType DEFLATE application/rss+xml
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
AddOutputFilterByType DEFLATE application/x-javascript
|
||||
AddOutputFilterByType DEFLATE image/svg+xml
|
||||
</IfModule>
|
||||
|
||||
# Configuración de PHP
|
||||
<IfModule mod_php7.c>
|
||||
php_flag display_errors off
|
||||
php_value max_execution_time 30
|
||||
php_value max_input_time 60
|
||||
php_value max_input_vars 1000
|
||||
php_value memory_limit 128M
|
||||
php_value post_max_size 32M
|
||||
php_value upload_max_filesize 32M
|
||||
php_flag log_errors on
|
||||
php_value error_log /var/log/php_errors.log
|
||||
</IfModule>
|
||||
84
admin/activity.php
Executable file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
|
||||
// Only admins can access this page
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header("Location: ../index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
$limit = 25;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// Get total number of records
|
||||
$total_stmt = $pdo->query("SELECT COUNT(*) FROM activity_log");
|
||||
$total_records = $total_stmt->fetchColumn();
|
||||
$total_pages = ceil($total_records / $limit);
|
||||
|
||||
// Fetch activity logs
|
||||
$stmt = $pdo->prepare("SELECT * FROM activity_log ORDER BY timestamp DESC LIMIT :limit OFFSET :offset");
|
||||
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4">Registro de Actividad</h1>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>Usuario</th>
|
||||
<th>Acción</th>
|
||||
<th>Detalles</th>
|
||||
<th>Fecha y Hora</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($logs)): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">No hay registros de actividad.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($logs as $log): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($log['username'] ?? 'N/A'); ?></td>
|
||||
<td><?php echo htmlspecialchars($log['action']); ?></td>
|
||||
<td><?php echo htmlspecialchars($log['details'] ?? 'N/A'); ?></td>
|
||||
<td><?php echo htmlspecialchars(date('d/m/Y H:i:s', strtotime($log['timestamp']))); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
<?php if ($page > 1): ?>
|
||||
<li class="page-item"><a class="page-link" href="?page=<?php echo $page - 1; ?>">Anterior</a></li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||
<li class="page-item <?php echo ($i == $page) ? 'active' : ''; ?>">
|
||||
<a class="page-link" href="?page=<?php echo $i; ?>"><?php echo $i; ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $total_pages): ?>
|
||||
<li class="page-item"><a class="page-link" href="?page=<?php echo $page + 1; ?>">Siguiente</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
54
admin/comandos.php
Executable file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
|
||||
// Fetch recurrent messages with commands
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT name, telegram_command FROM recurrent_messages WHERE telegram_command IS NOT NULL AND telegram_command != '' ORDER BY name ASC");
|
||||
$commands = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
die("Error: No se pudieron cargar los comandos.");
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4" data-translate="true">Lista de Comandos de Telegram</h1>
|
||||
<p class="text-muted" data-translate="true">Esta es una lista de todos los comandos de plantilla que has configurado. Los usuarios pueden usar estos comandos en Telegram para recibir el mensaje correspondiente.</p>
|
||||
|
||||
<div class="card card-body p-4">
|
||||
<?php if (empty($commands)): ?>
|
||||
<div class="alert alert-info text-center" role="alert">
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
<span data-translate="true">No has configurado ningún comando de Telegram todavía.</span>
|
||||
<br>
|
||||
<span data-translate="true">Puedes asignar comandos a tus plantillas desde la página de</span>
|
||||
<a href="recurrentes.php" class="alert-link" data-translate="true">Mensajes Recurrentes</a>.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th scope="col" data-translate="true">Comando de Telegram</th>
|
||||
<th scope="col" data-translate="true">Nombre de la Plantilla</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($commands as $command): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<code class="fs-5">#<?php echo htmlspecialchars($command['telegram_command']); ?></code>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($command['name']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
45
admin/get_user_groups.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Admin-only access
|
||||
if ($_SESSION['role'] !== 'admin') {
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$recipientId = $_GET['recipient_id'] ?? null;
|
||||
|
||||
if (!$recipientId) {
|
||||
echo json_encode(['error' => 'Recipient ID missing']);
|
||||
exit();
|
||||
}
|
||||
|
||||
try {
|
||||
// Obtener el nombre del usuario
|
||||
$stmt = $pdo->prepare("SELECT name FROM recipients WHERE id = ?");
|
||||
$stmt->execute([$recipientId]);
|
||||
$userName = $stmt->fetchColumn();
|
||||
|
||||
// Obtener los grupos a los que pertenece el usuario
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT tgm.chat_id, r.name as group_name
|
||||
FROM telegram_group_members tgm
|
||||
JOIN recipients r ON tgm.chat_id = r.platform_id AND r.platform = 'telegram' AND r.type = 'channel'
|
||||
WHERE tgm.recipient_id = ?
|
||||
");
|
||||
$stmt->execute([$recipientId]);
|
||||
$groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'userName' => $userName, 'groups' => $groups]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error fetching user groups: " . $e->getMessage());
|
||||
echo json_encode(['error' => 'Database error']);
|
||||
} catch (Exception $e) {
|
||||
error_log("Error: " . $e->getMessage());
|
||||
echo json_encode(['error' => 'Server error']);
|
||||
}
|
||||
?>
|
||||
214
admin/languages.php
Executable file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
|
||||
// Solo para administradores
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: /login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pageTitle = 'Gestionar Idiomas de Traducción';
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT * FROM supported_languages ORDER BY language_name ASC");
|
||||
$languages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
$languages = [];
|
||||
$errorMessage = "Error al cargar los idiomas: " . $e->getMessage();
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4">Gestionar Idiomas de Traducción</h1>
|
||||
<p class="text-muted">Activa o desactiva los idiomas a los que el bot traducirá automáticamente los mensajes.</p>
|
||||
|
||||
<?php if (isset($errorMessage)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($errorMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Idiomas Soportados</h5>
|
||||
<button id="sync-languages-btn" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-repeat me-1"></i> Sincronizar con LibreTranslate
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="language-list" class="list-group">
|
||||
<?php if (empty($languages)): ?>
|
||||
<div class="list-group-item">No se encontraron idiomas.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($languages as $lang): ?>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flag-container me-2" style="min-width: 40px;">
|
||||
<span class="fs-4 flag-emoji"><?= htmlspecialchars($lang['flag_emoji'] ?? '') ?></span>
|
||||
<div class="edit-flag-form d-none">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control flag-input" value="<?= htmlspecialchars($lang['flag_emoji'] ?? '') ?>" placeholder=" पेस्ट">
|
||||
<button class="btn btn-success btn-sm save-flag-btn" data-lang-id="<?= $lang['id'] ?>">✓</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="bi bi-pencil-square edit-flag-btn me-2" style="cursor: pointer;" data-lang-id="<?= $lang['id'] ?>"></i>
|
||||
<div>
|
||||
<span class="fw-bold"><?= htmlspecialchars($lang['language_name']) ?></span>
|
||||
<small class="text-muted">(<?= htmlspecialchars($lang['language_code']) ?>)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input language-toggle" type="checkbox" role="switch"
|
||||
id="lang-toggle-<?= $lang['id'] ?>"
|
||||
data-lang-id="<?= $lang['id'] ?>"
|
||||
<?= $lang['is_active'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="lang-toggle-<?= $lang['id'] ?>"></label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="alert-container" class="position-fixed bottom-0 end-0 p-3" style="z-index: 11"></div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const languageToggles = document.querySelectorAll('.language-toggle');
|
||||
const syncBtn = document.getElementById('sync-languages-btn');
|
||||
|
||||
if (syncBtn) {
|
||||
syncBtn.addEventListener('click', function () {
|
||||
const originalHtml = this.innerHTML;
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Sincronizando...';
|
||||
|
||||
fetch('sync_languages.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert(`Sincronización completada. Se añadieron ${data.new_languages} nuevos idiomas.`, 'success');
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
showAlert('Error en la sincronización: ' + data.error, 'danger');
|
||||
this.disabled = false;
|
||||
this.innerHTML = originalHtml;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('Error de red durante la sincronización.', 'danger');
|
||||
this.disabled = false;
|
||||
this.innerHTML = originalHtml;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
languageToggles.forEach(toggle => {
|
||||
toggle.addEventListener('change', function () {
|
||||
const langId = this.dataset.langId;
|
||||
const isActive = this.checked;
|
||||
|
||||
fetch('update_language_status.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({ id: langId, is_active: isActive })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('Estado del idioma actualizado con éxito.', 'success');
|
||||
} else {
|
||||
showAlert('Error: ' + data.error, 'danger');
|
||||
// Revertir el cambio visual si falla la actualización
|
||||
this.checked = !isActive;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('Error de red al actualizar el idioma.', 'danger');
|
||||
this.checked = !isActive;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Lógica para editar la bandera
|
||||
const languageList = document.getElementById('language-list');
|
||||
if (languageList) {
|
||||
languageList.addEventListener('click', function(e) {
|
||||
// Botón de editar
|
||||
if (e.target.classList.contains('edit-flag-btn')) {
|
||||
const listItem = e.target.closest('.list-group-item');
|
||||
listItem.querySelector('.flag-emoji').classList.add('d-none');
|
||||
e.target.classList.add('d-none');
|
||||
listItem.querySelector('.edit-flag-form').classList.remove('d-none');
|
||||
listItem.querySelector('.flag-input').focus();
|
||||
}
|
||||
|
||||
// Botón de guardar
|
||||
if (e.target.classList.contains('save-flag-btn')) {
|
||||
const langId = e.target.dataset.langId;
|
||||
const listItem = e.target.closest('.list-group-item');
|
||||
const input = listItem.querySelector('.flag-input');
|
||||
const newEmoji = input.value;
|
||||
|
||||
fetch('update_language_flag.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({ id: langId, flag_emoji: newEmoji })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const flagEmojiSpan = listItem.querySelector('.flag-emoji');
|
||||
flagEmojiSpan.textContent = newEmoji;
|
||||
flagEmojiSpan.classList.remove('d-none');
|
||||
listItem.querySelector('.edit-flag-form').classList.add('d-none');
|
||||
listItem.querySelector('.edit-flag-btn').classList.remove('d-none');
|
||||
showAlert('Bandera actualizada.', 'success');
|
||||
} else {
|
||||
showAlert('Error: ' + data.error, 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('Error de red al actualizar la bandera.', 'danger');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showAlert(message, type = 'success') {
|
||||
const alertContainer = document.getElementById('alert-container');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
alert.role = 'alert';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
`;
|
||||
alertContainer.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
538
admin/recipients.php
Executable file
@@ -0,0 +1,538 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/activity_logger.php';
|
||||
|
||||
// Admin-only access
|
||||
if ($_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../index.php?error=unauthorized');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Initialize variables for edit mode
|
||||
$edit_mode = false;
|
||||
$edit_recipient = [
|
||||
'id' => null,
|
||||
'name' => '',
|
||||
'platform_id' => '',
|
||||
'type' => 'channel',
|
||||
'platform' => 'discord',
|
||||
'language_code' => 'es'
|
||||
];
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$current_user_id = $_SESSION['user_id'];
|
||||
$current_username = $_SESSION['username'];
|
||||
$action = $_POST['action'] ?? null;
|
||||
|
||||
// Action: Add or Update Recipient
|
||||
if (isset($_POST['add_recipient']) || isset($_POST['update_recipient'])) {
|
||||
$platform = $_POST['platform'] ?? 'discord';
|
||||
$type = $_POST['type'] ?? 'channel';
|
||||
$name = $_POST['name'] ?? '';
|
||||
$platform_id = $_POST['platform_id'] ?? '';
|
||||
$language_code = $_POST['language_code'] ?? 'es';
|
||||
$id = $_POST['id'] ?? null;
|
||||
|
||||
if (empty($name) || empty($platform_id) || empty($type) || empty($platform)) {
|
||||
$error = "Todos los campos son obligatorios.";
|
||||
} elseif (!is_numeric($platform_id)) {
|
||||
$error = "El ID de Plataforma debe ser un número.";
|
||||
} else {
|
||||
try {
|
||||
if (isset($_POST['update_recipient'])) { // UPDATE
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET name = ?, platform_id = ?, type = ?, platform = ?, language_code = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $platform_id, $type, $platform, $language_code, $id]);
|
||||
$details = 'Admin ' . $current_username . ' updated recipient: ' . $name . ' (' . $platform . ':' . $platform_id . ')';
|
||||
log_activity($current_user_id, 'Recipient Updated', $details);
|
||||
header('Location: recipients.php?success=updated');
|
||||
exit();
|
||||
} else { // ADD
|
||||
$stmt = $pdo->prepare("INSERT INTO recipients (name, platform_id, type, platform, language_code) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$name, $platform_id, $type, $platform, $language_code]);
|
||||
$new_recipient_id = $pdo->lastInsertId();
|
||||
$details = 'Admin ' . $current_username . ' added new recipient: ' . $name . ' (' . $platform . ':' . $platform_id . ')';
|
||||
log_activity($current_user_id, 'Recipient Added', $details);
|
||||
$success = "Destinatario añadido con éxito.";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
if ($e->errorInfo[1] == 1062) {
|
||||
$error = "El ID de Plataforma ('$platform_id') ya existe.";
|
||||
} else {
|
||||
$error = "Error en la base de datos: " . $e->getMessage();
|
||||
}
|
||||
// Keep form data on error
|
||||
$edit_mode = isset($_POST['update_recipient']);
|
||||
$edit_recipient = ['id' => $id, 'name' => $name, 'platform_id' => $platform_id, 'type' => $type, 'platform' => $platform, 'language_code' => $language_code];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Action: Delete Single Recipient
|
||||
elseif (isset($_POST['delete_recipient'])) {
|
||||
$id_to_delete = $_POST['id_to_delete'];
|
||||
try {
|
||||
$stmt_recipient = $pdo->prepare("SELECT name, platform, platform_id FROM recipients WHERE id = ?");
|
||||
$stmt_recipient->execute([$id_to_delete]);
|
||||
$recipient_info = $stmt_recipient->fetch(PDO::FETCH_ASSOC);
|
||||
$details = 'Admin ' . $current_username . ' deleted recipient: ' . ($recipient_info['name'] ?? 'Unknown') . ' (' . ($recipient_info['platform'] ?? 'N/A') . ':' . ($recipient_info['platform_id'] ?? 'N/A') . ')';
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM recipients WHERE id = ?");
|
||||
$stmt->execute([$id_to_delete]);
|
||||
log_activity($current_user_id, 'Recipient Deleted', $details);
|
||||
$success = "Destinatario eliminado con éxito.";
|
||||
} catch (PDOException $e) {
|
||||
$error = "Error al eliminar. Es posible que el destinatario esté en uso.";
|
||||
}
|
||||
}
|
||||
// Action: Kick Telegram User
|
||||
elseif ($action === 'kick_telegram_user') {
|
||||
$recipient_id_to_kick = $_POST['recipient_id_to_kick'] ?? null;
|
||||
$chat_id_to_kick_from = $_POST['chat_id_to_kick_from'] ?? null;
|
||||
|
||||
if ($recipient_id_to_kick && $chat_id_to_kick_from) {
|
||||
try {
|
||||
// Get recipient's platform_id (Telegram user ID)
|
||||
$stmt = $pdo->prepare("SELECT platform_id, name FROM recipients WHERE id = ? AND platform = 'telegram' AND type = 'user'");
|
||||
$stmt->execute([$recipient_id_to_kick]);
|
||||
$recipient_info = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$telegram_user_id = $recipient_info['platform_id'] ?? null;
|
||||
$recipient_name = $recipient_info['name'] ?? 'Unknown';
|
||||
|
||||
if ($telegram_user_id) {
|
||||
// Get bot token
|
||||
$botToken = $_ENV['TELEGRAM_BOT_TOKEN'] ?? '';
|
||||
if (empty($botToken)) {
|
||||
throw new Exception("Token de bot de Telegram no configurado.");
|
||||
}
|
||||
|
||||
// Telegram API URL for banning a chat member
|
||||
$telegramApiUrl = "https://api.telegram.org/bot{$botToken}/banChatMember";
|
||||
$params = [
|
||||
'chat_id' => $chat_id_to_kick_from,
|
||||
'user_id' => $telegram_user_id,
|
||||
'until_date' => time() + 30 // Ban for 30 seconds to ensure they are removed, then they can rejoin
|
||||
];
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'method' => 'POST',
|
||||
'content' => http_build_query($params),
|
||||
],
|
||||
];
|
||||
$context = stream_context_create($options);
|
||||
$result = file_get_contents($telegramApiUrl, false, $context);
|
||||
$response = json_decode($result, true);
|
||||
|
||||
if ($response && $response['ok']) {
|
||||
// If successful, unban immediately to allow re-entry
|
||||
$unbanTelegramApiUrl = "https://api.telegram.org/bot{$botToken}/unbanChatMember";
|
||||
$unbanParams = [
|
||||
'chat_id' => $chat_id_to_kick_from,
|
||||
'user_id' => $telegram_user_id,
|
||||
'only_if_banned' => true // Only try to unban if they are actually banned
|
||||
];
|
||||
$unbanOptions = [
|
||||
'http' => [
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'method' => 'POST',
|
||||
'content' => http_build_query($unbanParams),
|
||||
],
|
||||
];
|
||||
$unbanContext = stream_context_create($unbanOptions);
|
||||
$unbanResult = file_get_contents($unbanTelegramApiUrl, false, $unbanContext);
|
||||
$unbanResponse = json_decode($unbanResult, true);
|
||||
|
||||
if ($unbanResponse && $unbanResponse['ok']) {
|
||||
$success = "Usuario de Telegram expulsado del grupo (permite reingreso) y eliminado de la base de datos.";
|
||||
} else {
|
||||
// Log unban error but still proceed with local deletion as kick was successful
|
||||
error_log("Error al desbanear usuario de Telegram después de la expulsión: " . ($unbanResponse['description'] ?? 'Error desconocido'));
|
||||
$success = "Usuario de Telegram expulsado del grupo (error al permitir reingreso) y eliminado de la base de datos.";
|
||||
}
|
||||
|
||||
// Delete from local DB regardless of unban success, as the kick itself was successful
|
||||
$stmt = $pdo->prepare("DELETE FROM recipients WHERE id = ?");
|
||||
$stmt->execute([$recipient_id_to_kick]);
|
||||
$details = 'Admin ' . $current_username . ' kicked Telegram user: ' . $recipient_name . ' (ID: ' . $telegram_user_id . ') from group ID: ' . $chat_id_to_kick_from;
|
||||
log_activity($current_user_id, 'Telegram User Kicked', $details);
|
||||
|
||||
} else {
|
||||
$error = "Error al expulsar usuario de Telegram: " . ($response['description'] ?? 'Error desconocido');
|
||||
}
|
||||
} else {
|
||||
$error = "Usuario de Telegram no encontrado o no es un usuario válido para expulsar.";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = "Error al procesar la expulsión: " . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = "Faltan parámetros para expulsar al usuario de Telegram.";
|
||||
}
|
||||
}
|
||||
// Action: Delete Multiple Recipients
|
||||
elseif ($action === 'delete_selected' && !empty($_POST['selected_recipients'])) {
|
||||
$deleted_count = 0;
|
||||
$error_count = 0;
|
||||
|
||||
foreach ($_POST['selected_recipients'] as $recipient_id) {
|
||||
try {
|
||||
$stmt_recipient = $pdo->prepare("SELECT name, platform, platform_id FROM recipients WHERE id = ?");
|
||||
$stmt_recipient->execute([$recipient_id]);
|
||||
$recipient_info = $stmt_recipient->fetch(PDO::FETCH_ASSOC);
|
||||
$details = 'Admin ' . $current_username . ' deleted recipient: ' . ($recipient_info['name'] ?? 'Unknown') . ' (' . ($recipient_info['platform'] ?? 'N/A') . ':' . ($recipient_info['platform_id'] ?? 'N/A') . ')';
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM recipients WHERE id = ?");
|
||||
$stmt->execute([$recipient_id]);
|
||||
log_activity($current_user_id, 'Recipient Deleted', $details);
|
||||
$deleted_count++;
|
||||
} catch (PDOException $e) {
|
||||
$error_count++;
|
||||
error_log("Error al eliminar destinatario ID $recipient_id: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleted_count > 0) {
|
||||
$success = "Se eliminaron $deleted_count destinatarios correctamente.";
|
||||
if ($error_count > 0) {
|
||||
$error = "Hubo errores al eliminar $error_count destinatarios.";
|
||||
}
|
||||
header('Location: recipients.php?success=deleted_multiple&deleted=' . $deleted_count . '&errors=' . $error_count);
|
||||
exit();
|
||||
} else if ($error_count > 0) {
|
||||
$error = "No se pudo eliminar ningún destinatario. Por favor, inténtalo de nuevo.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle entering edit mode via GET request
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'edit' && isset($_GET['id'])) {
|
||||
$edit_mode = true;
|
||||
$stmt = $pdo->prepare("SELECT * FROM recipients WHERE id = ?");
|
||||
$stmt->execute([$_GET['id']]);
|
||||
$recipient_to_edit = $stmt->fetch();
|
||||
if ($recipient_to_edit) {
|
||||
$edit_recipient = $recipient_to_edit;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all recipients to display
|
||||
$recipients = $pdo->query("SELECT * FROM recipients ORDER BY platform, type, name ASC")->fetchAll();
|
||||
|
||||
$pageTitle = 'Gestionar Destinatarios';
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
|
||||
// Display feedback messages
|
||||
if (isset($error)) echo "<div class='alert alert-danger'>$error</div>";
|
||||
if (isset($success)) echo "<div class='alert alert-success'>$success</div>";
|
||||
|
||||
if (isset($_GET['success'])) {
|
||||
if ($_GET['success'] === 'deleted_multiple') {
|
||||
$deleted = isset($_GET['deleted']) ? (int)$_GET['deleted'] : 0;
|
||||
$errors = isset($_GET['errors']) ? (int)$_GET['errors'] : 0;
|
||||
|
||||
if ($deleted > 0) {
|
||||
echo '<div class="alert alert-success">Se eliminaron ' . $deleted . ' destinatarios correctamente.</div>';
|
||||
}
|
||||
if ($errors > 0) {
|
||||
echo '<div class="alert alert-danger">Hubo errores al eliminar ' . $errors . ' destinatarios.</div>';
|
||||
}
|
||||
} else {
|
||||
echo "<div class='alert alert-success'>Operación completada con éxito.</div>";
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4" data-translate="true">Gestionar Destinatarios</h1>
|
||||
|
||||
<!-- Add/Edit Recipient Form -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0" data-translate="true"><?= $edit_mode ? 'Editar Destinatario' : 'Añadir Nuevo Destinatario' ?></h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="recipients.php" method="POST">
|
||||
<?php if ($edit_mode): ?><input type="hidden" name="id" value="<?= $edit_recipient['id'] ?>"><?php endif; ?>
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-2 mb-3">
|
||||
<label for="platform" class="form-label" data-translate="true">Plataforma</label>
|
||||
<select class="form-select" id="platform" name="platform" required>
|
||||
<option value="discord" <?= ($edit_recipient['platform'] === 'discord') ? 'selected' : '' ?> data-translate="true">Discord</option>
|
||||
<option value="telegram" <?= ($edit_recipient['platform'] === 'telegram') ? 'selected' : '' ?> data-translate="true">Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="name" class="form-label" data-translate="true">Nombre (identificador)</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($edit_recipient['name']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<label for="platform_id" class="form-label" data-translate="true">ID de Plataforma</label>
|
||||
<input type="text" class="form-control" id="platform_id" name="platform_id" value="<?= htmlspecialchars($edit_recipient['platform_id']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<label for="type" class="form-label" data-translate="true">Tipo</label>
|
||||
<select class="form-select" id="type" name="type" required>
|
||||
<option value="channel" <?= ($edit_recipient['type'] === 'channel') ? 'selected' : '' ?> data-translate="true">Canal/Grupo</option>
|
||||
<option value="user" <?= ($edit_recipient['type'] === 'user') ? 'selected' : '' ?> data-translate="true">Usuario</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1 mb-3">
|
||||
<label for="language_code" class="form-label" data-translate="true">Idioma</label>
|
||||
<select class="form-select" id="language_code" name="language_code" required>
|
||||
<option value="es" <?= ($edit_recipient['language_code'] === 'es') ? 'selected' : '' ?>>ES</option>
|
||||
<option value="en" <?= ($edit_recipient['language_code'] === 'en') ? 'selected' : '' ?>>EN</option>
|
||||
<option value="fr" <?= ($edit_recipient['language_code'] === 'fr') ? 'selected' : '' ?>>FR</option>
|
||||
<option value="de" <?= ($edit_recipient['language_code'] === 'de') ? 'selected' : '' ?>>DE</option>
|
||||
<option value="it" <?= ($edit_recipient['language_code'] === 'it') ? 'selected' : '' ?>>IT</option>
|
||||
<option value="pt" <?= ($edit_recipient['language_code'] === 'pt') ? 'selected' : '' ?>>PT</option>
|
||||
<option value="ru" <?= ($edit_recipient['language_code'] === 'ru') ? 'selected' : '' ?>>RU</option>
|
||||
<option value="ja" <?= ($edit_recipient['language_code'] === 'ja') ? 'selected' : '' ?>>JA</option>
|
||||
<option value="zh" <?= ($edit_recipient['language_code'] === 'zh') ? 'selected' : '' ?>>ZH</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<?php if ($edit_mode): ?>
|
||||
<button type="submit" name="update_recipient" class="btn btn-success w-100">
|
||||
<i class="bi bi-save"></i> <span data-translate="true">Guardar</span>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button type="submit" name="add_recipient" class="btn btn-primary w-100">
|
||||
<i class="bi bi-plus-circle"></i> <span data-translate="true">Añadir</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($edit_mode): ?>
|
||||
<div class="col-md-12"><a href="recipients.php" class="btn btn-sm btn-outline-secondary" data-translate="true">Cancelar Edición</a></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recipients List -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0" data-translate="true">Lista de Destinatarios</h5>
|
||||
<div>
|
||||
<button type="button" id="selectAllBtn" class="btn btn-sm btn-outline-secondary me-2">
|
||||
<i class="bi bi-check2-square"></i> <span data-translate="true">Seleccionar todo</span>
|
||||
</button>
|
||||
<button type="button" id="deleteSelectedBtn" class="btn btn-sm btn-danger" disabled>
|
||||
<i class="bi bi-trash"></i> <span data-translate="true">Eliminar seleccionados</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="recipientsForm" action="" method="POST" onsubmit="return confirm('¿Estás seguro de que quieres eliminar los destinatarios seleccionados?');">
|
||||
<input type="hidden" name="action" value="delete_selected">
|
||||
<input type="hidden" name="confirm_message" value="¿Estás seguro de que quieres eliminar los destinatarios seleccionados?" data-translate-confirm="¿Estás seguro de que quieres eliminar los destinatarios seleccionados?">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th width="40"><input type="checkbox" id="selectAll"></th>
|
||||
<th data-translate="true">Plataforma</th>
|
||||
<th data-translate="true">Nombre</th>
|
||||
<th data-translate="true">ID de Plataforma</th>
|
||||
<th data-translate="true">Tipo</th>
|
||||
<th data-translate="true">Idioma</th>
|
||||
<th data-translate="true">Añadido en</th>
|
||||
<th class="text-center" data-translate="true">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recipients as $recipient): ?>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="selected_recipients[]" value="<?= $recipient['id'] ?>" class="recipient-checkbox"></td>
|
||||
<td>
|
||||
<span class="badge <?= $recipient['platform'] === 'discord' ? 'bg-primary' : 'bg-info' ?>">
|
||||
<?= htmlspecialchars(ucfirst($recipient['platform'])) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($recipient['name']) ?></td>
|
||||
<td><?= htmlspecialchars($recipient['platform_id']) ?></td>
|
||||
<td><?= htmlspecialchars(ucfirst($recipient['type'])) ?></td>
|
||||
<td><span class="badge bg-secondary"><?= strtoupper(htmlspecialchars($recipient['language_code'] ?? 'es')) ?></span></td>
|
||||
<td><?= date('d/m/Y H:i', strtotime($recipient['created_at'])) ?></td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="?action=edit&id=<?= $recipient['id'] ?>" class="btn btn-sm btn-primary" title="Editar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="recipients.php" method="POST" onsubmit="return confirm(this.querySelector('[data-translate-confirm]').getAttribute('data-translate-confirm'));" class="d-inline ms-1">
|
||||
<input type="hidden" name="id_to_delete" value="<?= $recipient['id'] ?>">
|
||||
<input type="hidden" name="confirm_message" value="¿Estás seguro de que quieres eliminar este destinatario?" data-translate-confirm="¿Estás seguro de que quieres eliminar este destinatario?">
|
||||
<button type="submit" name="delete_recipient" class="btn btn-sm btn-danger" title="Eliminar" data-translate-title="true">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php if ($recipient['platform'] === 'telegram' && $recipient['type'] === 'user'): ?>
|
||||
<button type="button" class="btn btn-sm btn-warning ms-1 kick-telegram-user-btn"
|
||||
data-bs-toggle="modal" data-bs-target="#kickUserModal"
|
||||
data-recipient-id="<?= $recipient['id'] ?>"
|
||||
title="Expulsar de Telegram y Eliminar" data-translate-title="true">
|
||||
<i class="bi bi-person-x"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
|
||||
<!-- Modal para Expulsar Usuario de Telegram -->
|
||||
<div class="modal fade" id="kickUserModal" tabindex="-1" aria-labelledby="kickUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="kickUserModalLabel">Expulsar Usuario de Telegram</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="kickUserForm" action="recipients.php" method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" value="kick_telegram_user">
|
||||
<input type="hidden" name="recipient_id_to_kick" id="modalRecipientId">
|
||||
<p>Selecciona el grupo del que deseas expulsar a <strong id="modalUserName"></strong>:</p>
|
||||
<div id="groupListContainer">
|
||||
<!-- Los grupos se cargarán aquí dinámicamente -->
|
||||
<p class="text-muted">Cargando grupos...</p>
|
||||
</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-danger" id="confirmKickBtn" disabled>Expulsar y Eliminar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Selección múltiple
|
||||
const selectAllCheckbox = document.getElementById('selectAll');
|
||||
const recipientCheckboxes = document.querySelectorAll('.recipient-checkbox');
|
||||
const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
|
||||
const selectAllBtn = document.getElementById('selectAllBtn');
|
||||
|
||||
// Seleccionar/deseleccionar todos los checkboxes
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
recipientCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
});
|
||||
updateDeleteButton();
|
||||
});
|
||||
}
|
||||
|
||||
// Botón "Seleccionar todo"
|
||||
if (selectAllBtn) {
|
||||
selectAllBtn.addEventListener('click', function() {
|
||||
const allChecked = Array.from(recipientCheckboxes).every(checkbox => checkbox.checked);
|
||||
recipientCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = !allChecked;
|
||||
});
|
||||
selectAllCheckbox.checked = !allChecked;
|
||||
updateDeleteButton();
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar el estado del botón de eliminar seleccionados
|
||||
function updateDeleteButton() {
|
||||
const checkedBoxes = document.querySelectorAll('.recipient-checkbox:checked');
|
||||
deleteSelectedBtn.disabled = checkedBoxes.length === 0;
|
||||
}
|
||||
|
||||
// Actualizar el checkbox "Seleccionar todo" cuando se marcan/desmarcan checkboxes individuales
|
||||
recipientCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
updateDeleteButton();
|
||||
selectAllCheckbox.checked = Array.from(recipientCheckboxes).every(checkbox => checkbox.checked);
|
||||
});
|
||||
});
|
||||
|
||||
// Manejar el clic en el botón de eliminar seleccionados
|
||||
if (deleteSelectedBtn) {
|
||||
deleteSelectedBtn.addEventListener('click', function() {
|
||||
if (confirm('¿Estás seguro de que quieres eliminar los destinatarios seleccionados?')) {
|
||||
document.getElementById('recipientsForm').submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Lógica para el modal de expulsar usuario de Telegram
|
||||
const kickUserModal = document.getElementById('kickUserModal');
|
||||
const modalRecipientId = document.getElementById('modalRecipientId');
|
||||
const modalUserName = document.getElementById('modalUserName');
|
||||
const groupListContainer = document.getElementById('groupListContainer');
|
||||
const confirmKickBtn = document.getElementById('confirmKickBtn');
|
||||
const kickUserForm = document.getElementById('kickUserForm');
|
||||
|
||||
kickUserModal.addEventListener('show.bs.modal', function (event) {
|
||||
const button = event.relatedTarget; // Botón que activó el modal
|
||||
const recipientId = button.getAttribute('data-recipient-id');
|
||||
|
||||
modalRecipientId.value = recipientId;
|
||||
modalUserName.textContent = 'Cargando...';
|
||||
groupListContainer.innerHTML = '<p class="text-muted">Cargando grupos...</p>';
|
||||
confirmKickBtn.disabled = true;
|
||||
|
||||
// Cargar los grupos del usuario vía AJAX
|
||||
fetch(`get_user_groups.php?recipient_id=${recipientId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
modalUserName.textContent = data.userName;
|
||||
if (data.groups.length > 0) {
|
||||
let groupsHtml = '';
|
||||
data.groups.forEach(group => {
|
||||
groupsHtml += `
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="chat_id_to_kick_from" id="group_${group.chat_id}" value="${group.chat_id}">
|
||||
<label class="form-check-label" for="group_${group.chat_id}">
|
||||
${group.group_name} (ID: ${group.chat_id})
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
groupListContainer.innerHTML = groupsHtml;
|
||||
|
||||
// Habilitar el botón de confirmar cuando se selecciona un radio
|
||||
groupListContainer.querySelectorAll('input[type="radio"]').forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
confirmKickBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
groupListContainer.innerHTML = '<p class="text-info">Este usuario no está registrado en ningún grupo de Telegram.</p>';
|
||||
}
|
||||
} else {
|
||||
groupListContainer.innerHTML = `<p class="text-danger">Error al cargar grupos: ${data.error}</p>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching groups:', error);
|
||||
groupListContainer.innerHTML = '<p class="text-danger">Error de red al cargar grupos.</p>';
|
||||
});
|
||||
});
|
||||
|
||||
// Confirmación final antes de enviar el formulario de expulsión
|
||||
kickUserForm.addEventListener('submit', function(event) {
|
||||
if (!confirm('¿Estás seguro de que quieres expulsar a este usuario del grupo seleccionado y eliminarlo de la base de datos?')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
65
admin/set_test_webhook.php
Executable file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
// Cargar configuración del entorno de pruebas
|
||||
$envFile = __DIR__ . '/.env.pruebas';
|
||||
if (!file_exists($envFile)) {
|
||||
die("Error: No se encontró el archivo de configuración de pruebas (.env.pruebas)\n");
|
||||
}
|
||||
|
||||
// Cargar variables de entorno
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos(trim($line), '#') === 0) continue;
|
||||
list($name, $value) = explode('=', $line, 2);
|
||||
$name = trim($name);
|
||||
$value = trim($value, "'\" \t\n\r\0\x0B");
|
||||
if (!empty($name)) $_ENV[$name] = $value;
|
||||
}
|
||||
|
||||
// Configuración
|
||||
$botToken = $_ENV['TELEGRAM_BOT_TOKEN'] ?? '';
|
||||
$webhookToken = $_ENV['TELEGRAM_WEBHOOK_TOKEN'] ?? 'webhook_secure_token_12345';
|
||||
$webhookUrl = 'https://pruebaspons.duckdns.org/telegram_bot_webhook.php?auth_token=' . urlencode($webhookToken);
|
||||
|
||||
if (empty($botToken)) die("Error: No se configuró TELEGRAM_BOT_TOKEN\n");
|
||||
|
||||
echo "Configurando webhook para pruebas...\nURL: $webhookUrl\n";
|
||||
|
||||
// Configurar webhook
|
||||
$apiUrl = "https://api.telegram.org/bot{$botToken}/setWebhook";
|
||||
$postData = ['url' => $webhookUrl, 'allowed_updates' => json_encode(['message', 'callback_query'])];
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
// Mostrar resultados
|
||||
echo "\nRespuesta de la API de Telegram (HTTP $httpCode):\n";
|
||||
if ($error) {
|
||||
echo "Error: $error\n";
|
||||
} else {
|
||||
$result = json_decode($response, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
if ($result['ok'] ?? false) {
|
||||
echo "✅ Webhook configurado correctamente.\n";
|
||||
echo "URL: " . ($result['result']['url'] ?? 'N/A') . "\n";
|
||||
echo "Tiene certificado: " . ($result['result']['has_custom_certificate'] ? 'Sí' : 'No') . "\n";
|
||||
echo "Updates pendientes: " . ($result['result']['pending_update_count'] ?? '0') . "\n";
|
||||
} else {
|
||||
echo "❌ Error al configurar el webhook: " . ($result['description'] ?? 'Error desconocido') . "\n";
|
||||
}
|
||||
} else {
|
||||
echo "Respuesta no válida: " . substr($response, 0, 200) . "...\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nPara verificar la configuración actual del webhook, ejecuta:\n";
|
||||
echo "curl -s 'https://api.telegram.org/bot{$botToken}/getWebhookInfo' | jq\n\n";
|
||||
66
admin/sync_languages.php
Executable file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
// admin/sync_languages.php
|
||||
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/logger.php';
|
||||
require_once __DIR__ . '/../src/Translate.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Envolver todo en un manejador de errores para capturar hasta los errores fatales
|
||||
register_shutdown_function(function () {
|
||||
$error = error_get_last();
|
||||
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||
// Limpiar cualquier salida anterior
|
||||
if (ob_get_length()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
echo json_encode(['success' => false, 'error' => 'Error fatal en el servidor: ' . $error['message'] . ' en ' . $error['file'] . ' línea ' . $error['line']]);
|
||||
}
|
||||
});
|
||||
|
||||
// Solo para administradores
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
echo json_encode(['success' => false, 'error' => 'Acceso denegado.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Verificar que la URL de LibreTranslate esté configurada
|
||||
if (empty($_ENV['LIBRETRANSLATE_URL'])) {
|
||||
throw new Exception("La variable de entorno LIBRETRANSLATE_URL no está configurada en tu archivo .env");
|
||||
}
|
||||
|
||||
$translator = new Translate();
|
||||
$libreLanguages = $translator->getSupportedLanguages();
|
||||
|
||||
if ($libreLanguages === null) {
|
||||
throw new Exception("No se pudo obtener la lista de idiomas de LibreTranslate. Revisa que la URL ('" . htmlspecialchars($_ENV['LIBRETRANSLATE_URL']) . "') sea correcta y que el servicio esté funcionando.");
|
||||
}
|
||||
|
||||
if (empty($libreLanguages)) {
|
||||
throw new Exception("LibreTranslate devolvió una lista de idiomas vacía.");
|
||||
}
|
||||
|
||||
$newLanguagesCount = 0;
|
||||
$sql = "INSERT IGNORE INTO supported_languages (language_code, language_name, is_active) VALUES (?, ?, 0)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
foreach ($libreLanguages as $lang) {
|
||||
if (isset($lang['code']) && isset($lang['name'])) {
|
||||
$stmt->execute([$lang['code'], $lang['name']]);
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$newLanguagesCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'new_languages' => $newLanguagesCount]);
|
||||
|
||||
} catch (Throwable $e) { // Captura Throwable para errores y excepciones
|
||||
error_log("Error en sync_languages.php: " . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
408
admin/test_discord_connection.php
Executable file
@@ -0,0 +1,408 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../src/DiscordSender.php';
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
// Verificar que el usuario sea administrador
|
||||
if ($_SESSION['role'] !== 'admin') {
|
||||
header('HTTP/1.0 403 Forbidden');
|
||||
die('Acceso denegado. Solo los administradores pueden acceder a esta página.');
|
||||
}
|
||||
|
||||
$message = '';
|
||||
$error = '';
|
||||
$success = '';
|
||||
$discordResponse = null;
|
||||
$permissionsInfo = [];
|
||||
$telegramWebhookInfo = null;
|
||||
|
||||
// Manejar el envío del formulario de prueba
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Verificar si es una solicitud de verificación de permisos de Discord
|
||||
if (isset($_POST['check_permissions']) && !empty($_POST['guild_id'])) {
|
||||
$guildId = trim($_POST['guild_id']);
|
||||
try {
|
||||
$ch = curl_init("https://discord.com/api/v10/guilds/{$guildId}");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => [
|
||||
"Authorization: Bot " . DISCORD_BOT_TOKEN,
|
||||
"Content-Type: application/json"
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if ($httpCode === 200) {
|
||||
$guild = json_decode($response, true);
|
||||
|
||||
$ch = curl_init("https://discord.com/api/v10/users/@me");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => ["Authorization: Bot " . DISCORD_BOT_TOKEN],
|
||||
CURLOPT_RETURNTRANSFER => true
|
||||
]);
|
||||
|
||||
$botInfo = json_decode(curl_exec($ch), true);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if ($httpCode !== 200 || !isset($botInfo['id'])) {
|
||||
throw new Exception("No se pudo obtener la información del bot de Discord. Verifica que el token sea correcto. Código HTTP: $httpCode");
|
||||
}
|
||||
|
||||
$botId = $botInfo['id'];
|
||||
|
||||
$ch = curl_init("https://discord.com/api/v10/guilds/{$guildId}/roles");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => ["Authorization: Bot " . DISCORD_BOT_TOKEN],
|
||||
CURLOPT_RETURNTRANSFER => true
|
||||
]);
|
||||
|
||||
$roles = json_decode(curl_exec($ch), true);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if ($httpCode === 200 && is_array($roles)) {
|
||||
$ch = curl_init("https://discord.com/api/v10/guilds/{$guildId}/members/{$botId}");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => ["Authorization: Bot " . DISCORD_BOT_TOKEN],
|
||||
CURLOPT_RETURNTRANSFER => true
|
||||
]);
|
||||
|
||||
$member = json_decode(curl_exec($ch), true);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if ($httpCode === 404) {
|
||||
throw new Exception("El bot no está en el servidor de Discord especificado.");
|
||||
} elseif ($httpCode !== 200) {
|
||||
throw new Exception("Error al obtener información del bot en el servidor. Código HTTP: $httpCode");
|
||||
}
|
||||
|
||||
if (isset($member['roles']) && is_array($member['roles'])) {
|
||||
$botRoles = array_filter($roles, fn($role) => in_array($role['id'], $member['roles']));
|
||||
$permissions = 0;
|
||||
foreach ($botRoles as $role) {
|
||||
$permissions |= intval($role['permissions']);
|
||||
}
|
||||
|
||||
$permissionsInfo = [
|
||||
'guild_name' => $guild['name'] ?? 'Desconocido',
|
||||
'bot_has_admin' => ($permissions & 0x8) === 0x8,
|
||||
'can_kick' => ($permissions & 0x2) === 0x2,
|
||||
'can_ban' => ($permissions & 0x4) === 0x4,
|
||||
'permissions_int' => $permissions,
|
||||
'permissions_bin' => decbin($permissions),
|
||||
'roles' => array_column($botRoles, 'name')
|
||||
];
|
||||
|
||||
$success = "Permisos de Discord verificados para el servidor: " . htmlspecialchars($permissionsInfo['guild_name']);
|
||||
} else {
|
||||
$error = "No se pudo obtener la información de roles del bot en el servidor de Discord.";
|
||||
}
|
||||
} else {
|
||||
$error = "No se pudieron obtener los roles del servidor de Discord. Código HTTP: $httpCode";
|
||||
}
|
||||
} else {
|
||||
$error = "Error al obtener información del servidor de Discord. Código HTTP: $httpCode";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = "Error al verificar permisos de Discord: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
// Verificar si es una solicitud de verificación de webhook de Telegram
|
||||
elseif (isset($_POST['check_telegram_webhook'])) {
|
||||
try {
|
||||
if (!defined('TELEGRAM_BOT_TOKEN') || empty(TELEGRAM_BOT_TOKEN)) {
|
||||
throw new Exception("La constante TELEGRAM_BOT_TOKEN no está definida o está vacía.");
|
||||
}
|
||||
|
||||
$botToken = TELEGRAM_BOT_TOKEN;
|
||||
$apiUrl = "https://api.telegram.org/bot{$botToken}/getWebhookInfo";
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 10, // Segundos para esperar la conexión
|
||||
CURLOPT_TIMEOUT => 20, // Segundos para la ejecución total de cURL
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if ($response === false) {
|
||||
throw new Exception('Error en cURL al contactar la API de Telegram: ' . curl_error($ch));
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("La API de Telegram devolvió un código HTTP {$httpCode}. Respuesta: " . $response);
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if (!$result['ok']) {
|
||||
throw new Exception("Error de la API de Telegram: " . ($result['description'] ?? 'Error desconocido'));
|
||||
}
|
||||
|
||||
$telegramWebhookInfo = $result['result'];
|
||||
$success = "Información del webhook de Telegram obtenida correctamente.";
|
||||
|
||||
} catch (Exception $e) {
|
||||
$error = "Error al verificar el webhook de Telegram: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
// Manejar la eliminación del webhook de Telegram
|
||||
elseif (isset($_POST['delete_telegram_webhook'])) {
|
||||
try {
|
||||
if (!defined('TELEGRAM_BOT_TOKEN') || empty(TELEGRAM_BOT_TOKEN)) {
|
||||
throw new Exception("La constante TELEGRAM_BOT_TOKEN no está definida o está vacía.");
|
||||
}
|
||||
$botToken = TELEGRAM_BOT_TOKEN;
|
||||
$apiUrl = "https://api.telegram.org/bot{$botToken}/deleteWebhook";
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if ($httpCode === 200 && $result['ok']) {
|
||||
$success = "Webhook de Telegram eliminado correctamente.";
|
||||
} else {
|
||||
throw new Exception("Error al eliminar el webhook: " . ($result['description'] ?? 'Respuesta inválida de la API.'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = "Error al eliminar el webhook de Telegram: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
// Manejar la configuración del webhook de Telegram
|
||||
elseif (isset($_POST['set_telegram_webhook'])) {
|
||||
try {
|
||||
// Las constantes son definidas en config.php
|
||||
if (!defined('TELEGRAM_BOT_TOKEN') || !defined('BOT_BASE_URL') || !defined('TELEGRAM_WEBHOOK_TOKEN')) {
|
||||
throw new Exception("Una o más constantes requeridas (TELEGRAM_BOT_TOKEN, BOT_BASE_URL, TELEGRAM_WEBHOOK_TOKEN) no están definidas. Revisa tu archivo .env y la configuración.");
|
||||
}
|
||||
|
||||
$botToken = TELEGRAM_BOT_TOKEN;
|
||||
$webhookUrl = rtrim(BOT_BASE_URL, '/') . '/telegram/webhook/telegram_bot_webhook.php?auth_token=' . TELEGRAM_WEBHOOK_TOKEN;
|
||||
$apiUrl = "https://api.telegram.org/bot{$botToken}/setWebhook?url=" . urlencode($webhookUrl);
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if ($httpCode === 200 && $result['ok']) {
|
||||
$success = "Webhook de Telegram configurado correctamente en: " . htmlspecialchars($webhookUrl);
|
||||
} else {
|
||||
throw new Exception("Error al configurar el webhook: " . ($result['description'] ?? 'Respuesta inválida de la API.'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = "Error al configurar el webhook de Telegram: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
// Si es un envío de mensaje de prueba de Discord
|
||||
elseif (isset($_POST['discord_id']) && !empty($_POST['test_content'])) {
|
||||
$recipientId = trim($_POST['discord_id']);
|
||||
$testContent = trim($_POST['test_content']);
|
||||
$testRecipientType = $_POST['test_recipient_type'] ?? 'channel';
|
||||
|
||||
try {
|
||||
$logDir = __DIR__ . '/../logs';
|
||||
if (!is_dir($logDir)) mkdir($logDir, 0755, true);
|
||||
error_log("Test Discord: ID={$recipientId}, Type={$testRecipientType}, Token=" . substr(DISCORD_BOT_TOKEN, 0, 8) . "...", 3, $logDir . '/discord_api.log');
|
||||
|
||||
$discordSender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$discordResponse = $discordSender->sendMessage($recipientId, $testContent, $testRecipientType);
|
||||
$success = "Mensaje de prueba de Discord enviado con éxito.";
|
||||
} catch (Exception $e) {
|
||||
$error = "Error al enviar mensaje de prueba de Discord: " . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = "Por favor, completa todos los campos requeridos.";
|
||||
}
|
||||
}
|
||||
|
||||
// Incluir el encabezado que contiene el menú lateral
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
?>
|
||||
|
||||
<!-- Contenido principal -->
|
||||
<div id="page-content-wrapper">
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4">Probar Conexiones de Bots</h1>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?php echo $error; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?php echo $success; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Enviar Mensaje de Prueba a Discord</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="discord_id" class="form-label">ID de Canal o Usuario de Discord</label>
|
||||
<input type="text" class="form-control" id="discord_id" name="discord_id" required
|
||||
value="<?php echo htmlspecialchars($_POST['discord_id'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tipo de Destinatario</label>
|
||||
<div>
|
||||
<input type="radio" id="testRecipientTypeChannel" name="test_recipient_type" value="channel"
|
||||
<?php echo (($_POST['test_recipient_type'] ?? 'channel') === 'channel') ? 'checked' : ''; ?>>
|
||||
<label for="testRecipientTypeChannel">Canal</label>
|
||||
|
||||
<input type="radio" id="testRecipientTypeUser" name="test_recipient_type" value="user" class="ms-3"
|
||||
<?php echo (($_POST['test_recipient_type'] ?? 'channel') === 'user') ? 'checked' : ''; ?>>
|
||||
<label for="testRecipientTypeUser">Usuario</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="test_content" class="form-label">Contenido del Mensaje</label>
|
||||
<textarea class="form-control" id="test_content" name="test_content" rows="3" required><?php
|
||||
echo htmlspecialchars($_POST['test_content'] ?? 'Mensaje de prueba desde el bot.');
|
||||
?></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Enviar Prueba a Discord</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (isset($discordResponse) && $discordResponse !== null): ?>
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header"><h5 class="mb-0">Respuesta de la API de Discord</h5></div>
|
||||
<div class="card-body"><pre><code><?php print_r($discordResponse); ?></code></pre></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header"><h5 class="mb-0">Verificar Permisos del Bot de Discord</h5></div>
|
||||
<div class="card-body">
|
||||
<form action="" method="POST" class="mb-4">
|
||||
<input type="hidden" name="check_permissions" value="1">
|
||||
<div class="mb-3">
|
||||
<label for="guild_id" class="form-label">ID del Servidor de Discord</label>
|
||||
<input type="text" class="form-control" id="guild_id" name="guild_id" required
|
||||
value="<?php echo htmlspecialchars($_POST['guild_id'] ?? ''); ?>"
|
||||
placeholder="Ej: 123456789012345678">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-info">Verificar Permisos de Discord</button>
|
||||
</form>
|
||||
|
||||
<?php if (!empty($permissionsInfo)): ?>
|
||||
<div class="alert alert-info">
|
||||
<h6>Resultado de la verificación de Discord:</h6>
|
||||
<ul class="mb-0">
|
||||
<li>Servidor: <strong><?php echo htmlspecialchars($permissionsInfo['guild_name']); ?></strong></li>
|
||||
<li>Es Administrador:
|
||||
<span class="badge bg-<?php echo $permissionsInfo['bot_has_admin'] ? 'success' : 'danger'; ?>">
|
||||
<?php echo $permissionsInfo['bot_has_admin'] ? 'Sí' : 'No'; ?>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header"><h5 class="mb-0">Verificar Estado del Webhook de Telegram</h5></div>
|
||||
<div class="card-body">
|
||||
<p>Obtén el estado actual de tu webhook directamente desde la API de Telegram para diagnosticar problemas de conexión.</p>
|
||||
<form action="" method="POST">
|
||||
<input type="hidden" name="check_telegram_webhook" value="1">
|
||||
<button type="submit" class="btn btn-info">Verificar Webhook de Telegram</button>
|
||||
</form>
|
||||
|
||||
<?php if (isset($telegramWebhookInfo) && is_array($telegramWebhookInfo)): ?>
|
||||
<div class="mt-4">
|
||||
<h6>Resultado de la verificación de Telegram:</h6>
|
||||
<?php if (empty($telegramWebhookInfo['url'])): ?>
|
||||
<div class="alert alert-danger">
|
||||
<strong>No hay webhook configurado.</strong> El bot está funcionando en modo 'getUpdates'.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item"><strong>URL:</strong> <code><?php echo htmlspecialchars($telegramWebhookInfo['url']); ?></code></li>
|
||||
<li class="list-group-item"><strong>Actualizaciones pendientes:</strong>
|
||||
<span class="badge bg-<?php echo $telegramWebhookInfo['pending_update_count'] > 0 ? 'warning' : 'success'; ?>">
|
||||
<?php echo $telegramWebhookInfo['pending_update_count']; ?>
|
||||
</span>
|
||||
</li>
|
||||
<?php if (!empty($telegramWebhookInfo['last_error_date'])): ?>
|
||||
<li class="list-group-item list-group-item-danger">
|
||||
<strong>Último error:</strong> <?php echo date('Y-m-d H:i:s', $telegramWebhookInfo['last_error_date']); ?>
|
||||
<br>
|
||||
<strong>Mensaje:</strong> <?php echo htmlspecialchars($telegramWebhookInfo['last_error_message']); ?>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li class="list-group-item list-group-item-success"><strong>Último error:</strong> Ninguno reportado.</li>
|
||||
<?php endif; ?>
|
||||
<li class="list-group-item"><strong>Máx. Conexiones:</strong> <?php echo $telegramWebhookInfo['max_connections'] ?? 'No definido'; ?></li>
|
||||
<li class="list-group-item"><strong>Actualizaciones permitidas:</strong>
|
||||
<?php if (!empty($telegramWebhookInfo['allowed_updates']) && count($telegramWebhookInfo['allowed_updates']) > 0): ?>
|
||||
<code><?php echo implode(', ', $telegramWebhookInfo['allowed_updates']); ?></code>
|
||||
<?php else: ?>
|
||||
<span>Todos los tipos (por defecto).</span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header"><h5 class="mb-0">Gestionar Webhook de Telegram</h5></div>
|
||||
<div class="card-body">
|
||||
<p>Usa estos botones para eliminar o re-configurar el webhook de Telegram. Esto es útil para forzar a Telegram a actualizar la dirección IP de tu servidor si ha cambiado (por ejemplo, con DuckDNS).</p>
|
||||
<form action="" method="POST" class="d-inline me-2">
|
||||
<input type="hidden" name="delete_telegram_webhook" value="1">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('¿Estás seguro de que quieres eliminar el webhook? El bot dejará de recibir actualizaciones.');">Eliminar Webhook</button>
|
||||
</form>
|
||||
<form action="" method="POST" class="d-inline">
|
||||
<input type="hidden" name="set_telegram_webhook" value="1">
|
||||
<button type="submit" class="btn btn-success">Configurar Webhook</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts de Bootstrap -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
// Toggle del menú lateral
|
||||
function toggleSidebar() {
|
||||
document.getElementById('wrapper').classList.toggle('toggled');
|
||||
}
|
||||
|
||||
// Función para copiar al portapapeles
|
||||
function copyToClipboard(elementId) {
|
||||
var copyText = document.getElementById(elementId);
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999);
|
||||
document.execCommand("copy");
|
||||
|
||||
var button = event.target;
|
||||
var originalText = button.innerHTML;
|
||||
button.innerHTML = '¡Copiado!';
|
||||
button.classList.remove('btn-outline-secondary');
|
||||
button.classList.add('btn-success');
|
||||
|
||||
setTimeout(function() {
|
||||
button.innerHTML = originalText;
|
||||
button.classList.remove('btn-success');
|
||||
button.classList.add('btn-outline-secondary');
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
42
admin/update_language_flag.php
Executable file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// admin/update_language_flag.php
|
||||
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Solo para administradores
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
echo json_encode(['success' => false, 'error' => 'Acceso denegado.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verificar que la solicitud sea AJAX y POST
|
||||
if (strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'xmlhttprequest' || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'Solicitud no válida.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$langId = $data['id'] ?? null;
|
||||
$flagEmoji = $data['flag_emoji'] ?? '';
|
||||
|
||||
if ($langId === null) {
|
||||
echo json_encode(['success' => false, 'error' => 'ID de idioma no proporcionado.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE supported_languages SET flag_emoji = ? WHERE id = ?");
|
||||
$stmt->execute([$flagEmoji, $langId]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error en update_language_flag.php: " . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'error' => 'Error en la base de datos.']);
|
||||
}
|
||||
?>
|
||||
49
admin/update_language_status.php
Executable file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// admin/update_language_status.php
|
||||
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Verificar que la solicitud sea AJAX y POST
|
||||
if (strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'xmlhttprequest' || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'Solicitud no válida.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Solo para administradores
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
echo json_encode(['success' => false, 'error' => 'Acceso denegado.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Obtener datos del cuerpo de la solicitud
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$langId = $data['id'] ?? null;
|
||||
$isActive = isset($data['is_active']) ? (int)$data['is_active'] : null;
|
||||
|
||||
if ($langId === null || $isActive === null) {
|
||||
echo json_encode(['success' => false, 'error' => 'Datos incompletos.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE supported_languages SET is_active = ? WHERE id = ?");
|
||||
$stmt->execute([$isActive, $langId]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
// No se actualizó ninguna fila, podría ser que el ID no exista
|
||||
echo json_encode(['success' => false, 'error' => 'El idioma no fue encontrado o el estado ya era el mismo.']);
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Loguear el error real en el servidor
|
||||
error_log("Error en update_language_status.php: " . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'error' => 'Error en la base de datos.']);
|
||||
}
|
||||
?>
|
||||
201
admin/users.php
Executable file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/session_check.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/activity_logger.php';
|
||||
|
||||
// Admin-only access
|
||||
if ($_SESSION['role'] !== 'admin') {
|
||||
header('Location: ../index.php?error=unauthorized');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Inicializar variables para el modo edición
|
||||
$edit_mode = false;
|
||||
$edit_id = null;
|
||||
$edit_username = '';
|
||||
$edit_role = 'user';
|
||||
$edit_telegram_chat_id = '';
|
||||
|
||||
// Handle GET actions for editing
|
||||
if (isset($_GET['edit'])) {
|
||||
$edit_id = $_GET['edit'];
|
||||
$stmt = $pdo->prepare("SELECT id, username, role, telegram_chat_id FROM users WHERE id = ?");
|
||||
$stmt->execute([$edit_id]);
|
||||
$user_to_edit = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($user_to_edit) {
|
||||
$edit_mode = true;
|
||||
$edit_username = $user_to_edit['username'];
|
||||
$edit_role = $user_to_edit['role'];
|
||||
$edit_telegram_chat_id = $user_to_edit['telegram_chat_id'];
|
||||
} else {
|
||||
$error = "Usuario no encontrado.";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle POST actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Action: Create or Update User
|
||||
if (isset($_POST['save_user'])) {
|
||||
$username = $_POST['username'];
|
||||
$role = $_POST['role'];
|
||||
$telegram_chat_id = trim($_POST['telegram_chat_id']);
|
||||
$is_edit = isset($_POST['edit_id']);
|
||||
|
||||
if (empty($username) || empty($role)) {
|
||||
$error = "El nombre de usuario y el rol son obligatorios.";
|
||||
} elseif (!empty($telegram_chat_id) && !is_numeric($telegram_chat_id)) {
|
||||
$error = "El ID de Chat de Telegram debe ser un número.";
|
||||
} else {
|
||||
$chat_id_to_save = empty($telegram_chat_id) ? null : $telegram_chat_id;
|
||||
try {
|
||||
if ($is_edit) {
|
||||
$edit_id = $_POST['edit_id'];
|
||||
$details = 'Admin ' . $_SESSION['username'] . ' updated user: ' . $username . ' (ID: ' . $edit_id . ').';
|
||||
|
||||
if (!empty($_POST['password'])) {
|
||||
$hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("UPDATE users SET username = ?, password = ?, role = ?, telegram_chat_id = ? WHERE id = ?");
|
||||
$stmt->execute([$username, $hashedPassword, $role, $chat_id_to_save, $edit_id]);
|
||||
$details .= ' Password was changed.';
|
||||
} else {
|
||||
$stmt = $pdo->prepare("UPDATE users SET username = ?, role = ?, telegram_chat_id = ? WHERE id = ?");
|
||||
$stmt->execute([$username, $role, $chat_id_to_save, $edit_id]);
|
||||
}
|
||||
log_activity($_SESSION['user_id'], 'User Updated', $details);
|
||||
header('Location: users.php?success=updated');
|
||||
exit();
|
||||
} else {
|
||||
if (empty($_POST['password'])) {
|
||||
$error = "La contraseña es obligatoria para nuevos usuarios.";
|
||||
} else {
|
||||
$hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, password, role, telegram_chat_id) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$username, $hashedPassword, $role, $chat_id_to_save]);
|
||||
$new_user_id = $pdo->lastInsertId();
|
||||
log_activity($_SESSION['user_id'], 'User Created', 'Admin ' . $_SESSION['username'] . ' created new user: ' . $username . ' (ID: ' . $new_user_id . ')');
|
||||
header('Location: users.php?success=created');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$error = ($e->errorInfo[1] == 1062) ? "El nombre de usuario ya existe." : "Error al guardar el usuario: " . $e->getMessage();
|
||||
if ($is_edit) {
|
||||
$edit_mode = true;
|
||||
$edit_id = $_POST['edit_id'];
|
||||
$edit_username = $username;
|
||||
$edit_role = $role;
|
||||
$edit_telegram_chat_id = $telegram_chat_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ... (Otras acciones POST como eliminar, etc. se mantienen aquí)
|
||||
}
|
||||
|
||||
// Fetch all users to display
|
||||
$users = $pdo->query("SELECT id, username, role, created_at, telegram_chat_id FROM users ORDER BY username ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
require_once __DIR__ . '/../templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4" data-translate="true">Administrar Usuarios</h1>
|
||||
|
||||
<?php if (isset($error)): ?><div class="alert alert-danger"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||||
<?php if (isset($_GET['success'])): /* ... Lógica de mensajes de éxito ... */ endif; ?>
|
||||
|
||||
<!-- Create/Edit User Form -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0" data-translate="true"><?= $edit_mode ? 'Editar Usuario' : 'Crear Nuevo Usuario' ?></h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="users.php" method="POST">
|
||||
<?php if ($edit_mode): ?>
|
||||
<input type="hidden" name="edit_id" value="<?= $edit_id ?>">
|
||||
<?php endif; ?>
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="username" class="form-label" data-translate="true">Usuario</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
value="<?= htmlspecialchars($edit_username) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<span data-translate="true">Contraseña</span> <?php if ($edit_mode): ?><small class="text-muted" data-translate="true">(no cambiar)</small><?php endif; ?>
|
||||
</label>
|
||||
<input type="password" class="form-control" id="password" name="password" <?= !$edit_mode ? 'required' : '' ?>>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<label for="role" class="form-label" data-translate="true">Rol</label>
|
||||
<select class="form-select" id="role" name="role" required>
|
||||
<option value="user" <?= ($edit_role === 'user') ? 'selected' : '' ?> data-translate="true">Usuario</option>
|
||||
<option value="admin" <?= ($edit_role === 'admin') ? 'selected' : '' ?> data-translate="true">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<label for="telegram_chat_id" class="form-label" data-translate="true">ID Chat Telegram</label>
|
||||
<input type="text" class="form-control" id="telegram_chat_id" name="telegram_chat_id"
|
||||
value="<?= htmlspecialchars($edit_telegram_chat_id) ?>" placeholder="(Opcional)">
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<button type="submit" name="save_user" class="btn btn-<?= $edit_mode ? 'success' : 'primary' ?> w-100" data-translate="true">
|
||||
<?= $edit_mode ? 'Actualizar' : 'Crear' ?>
|
||||
</button>
|
||||
<?php if ($edit_mode): ?>
|
||||
<a href="users.php" class="btn btn-outline-secondary w-100 mt-2" data-translate="true">Cancelar</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users List -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header"><h5 data-translate="true">Lista de Usuarios</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th data-translate="true">ID</th>
|
||||
<th data-translate="true">Usuario</th>
|
||||
<th data-translate="true">Rol</th>
|
||||
<th data-translate="true">ID Chat Telegram</th>
|
||||
<th data-translate="true">Creado en</th>
|
||||
<th class="text-center" data-translate="true">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr>
|
||||
<td><?= $user['id'] ?></td>
|
||||
<td><?= htmlspecialchars($user['username']) ?></td>
|
||||
<td><?= ucfirst($user['role']) ?></td>
|
||||
<td>
|
||||
<?php if (!empty($user['telegram_chat_id'])): ?>
|
||||
<span class="badge bg-info text-dark"><?= htmlspecialchars($user['telegram_chat_id']) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary" data-translate="true">No vinculado</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= date('d/m/Y H:i', strtotime($user['created_at'])) ?></td>
|
||||
<td class="text-center">
|
||||
<a href="?edit=<?= $user['id'] ?>#username" class="btn btn-sm btn-primary" title="Editar" data-translate-title="true">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<!-- Otros botones de acción (modal, eliminar) -->
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modales y Scripts -->
|
||||
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
|
||||
52
admin_send_message.php
Executable file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/config/config.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/src/TelegramSender.php';
|
||||
|
||||
// Solo para administradores
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Acceso denegado.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$chatId = $_POST['chat_id'] ?? null;
|
||||
$message = $_POST['message'] ?? null;
|
||||
|
||||
if (!$chatId || !$message) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Faltan parámetros: chat_id y message son requeridos.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$telegram = new TelegramSender($_ENV['TELEGRAM_BOT_TOKEN'], $pdo);
|
||||
|
||||
// Se envía el mensaje. La clase TelegramSender se encargará de:
|
||||
// 1. Formatear el texto si es necesario.
|
||||
// 2. Añadir el botón de traducción (true).
|
||||
// 3. Registrar la interacción en la base de datos.
|
||||
$result = $telegram->sendMessage(
|
||||
$chatId,
|
||||
htmlspecialchars($message), // Usamos htmlspecialchars para seguridad básica
|
||||
['parse_mode' => 'HTML'],
|
||||
true, // Añadir botón de traducción
|
||||
'es' // El mensaje del admin se asume que es en español
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
// El error específico ya se loguea dentro de TelegramSender
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'El servicio de Telegram no pudo enviar el mensaje.']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
custom_log("Error fatal en admin_send_message.php: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Error interno del servidor al procesar la solicitud.']);
|
||||
}
|
||||
BIN
assets/css/font/summernote.ttf
Executable file
BIN
assets/css/font/summernote.woff
Executable file
BIN
assets/css/font/summernote.woff2
Executable file
1
assets/css/summernote-bs5.min.css
vendored
Executable file
BIN
assets/images/logo.png
Executable file
|
After Width: | Height: | Size: 1.8 MiB |
2
assets/js/jquery-3.6.0.min.js
vendored
Executable file
2
assets/js/summernote-bs5.min.js
vendored
Executable file
114
assets/js/translate_frontend.js
Executable file
@@ -0,0 +1,114 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const languageSelector = document.getElementById('language-selector');
|
||||
const currentLang = localStorage.getItem('appLang') || 'es'; // Default to Spanish
|
||||
|
||||
const translatableElements = document.querySelectorAll('[data-translate="true"]');
|
||||
|
||||
// Function to translate text
|
||||
async function translateText(text, sourceLang, targetLang) {
|
||||
try {
|
||||
const response = await fetch('/translate_proxy.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'translate',
|
||||
text: text,
|
||||
source: sourceLang,
|
||||
target: targetLang
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return data.translatedText; // LibreTranslate returns { translatedText: "..." }
|
||||
} else {
|
||||
console.error('Error translating:', data.error);
|
||||
return text; // Return original text on error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error during translation:', error);
|
||||
return text; // Return original text on network error
|
||||
}
|
||||
}
|
||||
|
||||
async function applyTranslation(targetLang) {
|
||||
for (const element of translatableElements) {
|
||||
const originalText = element.dataset.originalText || element.textContent;
|
||||
if (!element.dataset.originalText) {
|
||||
element.dataset.originalText = originalText;
|
||||
}
|
||||
|
||||
const icon = element.querySelector('i.bi');
|
||||
const textToTranslate = originalText.trim();
|
||||
|
||||
let newText;
|
||||
if (targetLang === 'es') {
|
||||
newText = textToTranslate;
|
||||
} else {
|
||||
newText = await translateText(textToTranslate, 'es', targetLang);
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
element.textContent = ' ' + newText;
|
||||
element.insertAdjacentHTML('afterbegin', icon.outerHTML);
|
||||
} else {
|
||||
element.textContent = newText;
|
||||
}
|
||||
}
|
||||
localStorage.setItem('appLang', targetLang);
|
||||
if (languageSelector) {
|
||||
languageSelector.value = targetLang;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to fetch and populate languages
|
||||
async function fetchAndPopulateLanguages() {
|
||||
try {
|
||||
const response = await fetch('/translate_proxy.php?action=languages'); // Assuming translate_proxy.php can handle a 'languages' action
|
||||
const languages = await response.json();
|
||||
if (response.ok && Array.isArray(languages)) {
|
||||
languageSelector.innerHTML = ''; // Clear existing options
|
||||
languages.forEach(lang => {
|
||||
const option = document.createElement('option');
|
||||
option.value = lang.code;
|
||||
option.textContent = `${lang.flag_emoji} ${lang.name}`;
|
||||
languageSelector.appendChild(option);
|
||||
});
|
||||
// Set the selector to the current language
|
||||
languageSelector.value = currentLang;
|
||||
} else {
|
||||
console.error('Error fetching languages:', languages.error || 'Unknown error');
|
||||
// Fallback to default options if fetching fails
|
||||
languageSelector.innerHTML = `
|
||||
<option value="es">🇪🇸 Español</option>
|
||||
<option value="en">🇬🇧 English</option>
|
||||
<option value="pt">🇧🇷 Português</option>
|
||||
`;
|
||||
languageSelector.value = currentLang;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error fetching languages:', error);
|
||||
// Fallback to default options if network error
|
||||
languageSelector.innerHTML = `
|
||||
<option value="es">🇪🇸 Español</option>
|
||||
<option value="en">🇬🇧 English</option>
|
||||
<option value="pt">🇧🇷 Português</option>
|
||||
`;
|
||||
languageSelector.value = currentLang;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial application of language based on stored preference
|
||||
fetchAndPopulateLanguages().then(() => {
|
||||
applyTranslation(currentLang);
|
||||
});
|
||||
|
||||
// Event listener for the language selector
|
||||
if (languageSelector) {
|
||||
languageSelector.addEventListener('change', (event) => {
|
||||
const newLang = event.target.value;
|
||||
applyTranslation(newLang);
|
||||
});
|
||||
}
|
||||
});
|
||||
86
change_language.php
Executable file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* change_language.php
|
||||
*
|
||||
* Este script maneja el cambio de idioma en la aplicación.
|
||||
* Actualiza la sesión del usuario con el idioma seleccionado.
|
||||
*/
|
||||
|
||||
// Iniciar la sesión si no está iniciada
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Incluir configuración y base de datos
|
||||
require_once __DIR__ . '/config/config.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/translation_helper.php';
|
||||
|
||||
// Configurar cabeceras para respuesta JSON
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Verificar que la solicitud sea POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405); // Método no permitido
|
||||
echo json_encode(['success' => false, 'message' => 'Método no permitido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Obtener el idioma de la solicitud
|
||||
$language = $_POST['language'] ?? null;
|
||||
|
||||
// Validar que se proporcionó un idioma
|
||||
if (empty($language)) {
|
||||
http_response_code(400); // Solicitud incorrecta
|
||||
echo json_encode(['success' => false, 'message' => 'No se proporcionó un idioma']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verificar que el idioma esté disponible
|
||||
$availableLanguages = get_available_languages();
|
||||
$validLanguage = false;
|
||||
|
||||
foreach ($availableLanguages as $lang) {
|
||||
if ($lang['code'] === $language) {
|
||||
$validLanguage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$validLanguage) {
|
||||
http_response_code(400); // Solicitud incorrecta
|
||||
echo json_encode(['success' => false, 'message' => 'Idioma no válido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Actualizar el idioma en la sesión
|
||||
$_SESSION['language'] = $language;
|
||||
|
||||
// Si el usuario está autenticado, actualizar su preferencia de idioma en la base de datos
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE users SET language = ? WHERE id = ?");
|
||||
$stmt->execute([$language, $_SESSION['user_id']]);
|
||||
} catch (PDOException $e) {
|
||||
// Registrar el error pero no fallar la operación
|
||||
error_log("Error al actualizar el idioma del usuario: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Limpiar la caché de traducciones si existe
|
||||
if (isset($GLOBALS['_translations_cache'])) {
|
||||
$GLOBALS['_translations_cache'] = [];
|
||||
}
|
||||
|
||||
// Devolver respuesta exitosa
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Idioma actualizado correctamente',
|
||||
'language' => $language
|
||||
]);
|
||||
|
||||
// Registrar el cambio de idioma
|
||||
custom_log("Idioma cambiado a: " . $language, 'language');
|
||||
|
||||
// Finalizar el script
|
||||
exit;
|
||||
621
chat_telegram.php
Executable file
@@ -0,0 +1,621 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
|
||||
// Solo para administradores
|
||||
if ($_SESSION['role'] !== 'admin') {
|
||||
header('HTTP/1.0 403 Forbidden');
|
||||
die('Acceso denegado.');
|
||||
}
|
||||
|
||||
// Obtener todos los usuarios de Telegram que han interactuado o son miembros de grupos
|
||||
$stmt = $pdo->query(
|
||||
"SELECT DISTINCT r.platform_id, r.name
|
||||
FROM recipients r
|
||||
WHERE r.platform = 'telegram' AND r.type = 'user'
|
||||
ORDER BY r.name ASC"
|
||||
);
|
||||
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 200px); /* Ajustar altura */
|
||||
}
|
||||
|
||||
/* Estilos para el selector de emojis */
|
||||
.emoji-picker {
|
||||
position: fixed;
|
||||
width: 250px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.emoji-option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.emoji-option:hover {
|
||||
background-color: #f8f9fa;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.emoji-option:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.input-group-prepend .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group-append .btn {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
#message-text {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.user-list {
|
||||
border-right: 1px solid #dee2e6;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.chat-history {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.message-form {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
.message {
|
||||
max-width: 70%;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.message.in {
|
||||
background-color: #6495ED; /* Azul claro (CornflowerBlue) */
|
||||
color: white;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.message.out {
|
||||
background-color: #6495ED; /* Azul claro (CornflowerBlue) */
|
||||
color: white;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.user-list .list-group-item.active {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4">Chat de Soporte de Telegram</h1>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="chat-container">
|
||||
<!-- Columna de Usuarios -->
|
||||
<div class="col-md-4 col-lg-3 user-list">
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach ($users as $user): ?>
|
||||
<a href="#" class="list-group-item list-group-item-action" data-chat-id="<?= $user['platform_id'] ?>">
|
||||
<?= htmlspecialchars($user['name']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna de Chat -->
|
||||
<div class="col-md-8 col-lg-9 d-flex flex-column">
|
||||
<div id="chat-history" class="chat-history">
|
||||
<div class="text-center text-muted my-auto">
|
||||
<i class="bi bi-arrow-left-circle-fill fs-1"></i>
|
||||
<p>Selecciona un usuario para ver la conversación.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="message-form-container" class="message-form" style="display: none;">
|
||||
<form id="message-form">
|
||||
<input type="hidden" id="chat-id-input" name="chat_id">
|
||||
<div class="input-group" style="position: relative;">
|
||||
<div class="input-group-prepend">
|
||||
<button type="button" id="emoji-trigger" class="btn btn-outline-secondary" onclick="toggleEmojiPicker()">
|
||||
<i class="bi bi-emoji-smile"></i>
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" id="message-text" name="message" class="form-control" placeholder="Escribe tu respuesta..." required>
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-primary">Enviar</button>
|
||||
</div>
|
||||
|
||||
<!-- Selector de emojis simplificado -->
|
||||
<div id="emoji-picker" style="
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
width: 250px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 5px;">
|
||||
<span class="emoji-option" onclick="insertEmoji('😊')" style="cursor: pointer; font-size: 24px;">😊</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('😂')" style="cursor: pointer; font-size: 24px;">😂</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('😍')" style="cursor: pointer; font-size: 24px;">😍</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('😎')" style="cursor: pointer; font-size: 24px;">😎</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('😢')" style="cursor: pointer; font-size: 24px;">😢</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('😡')" style="cursor: pointer; font-size: 24px;">😡</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('😴')" style="cursor: pointer; font-size: 24px;">😴</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('😷')" style="cursor: pointer; font-size: 24px;">😷</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('🤔')" style="cursor: pointer; font-size: 24px;">🤔</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('🤗')" style="cursor: pointer; font-size: 24px;">🤗</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('👍')" style="cursor: pointer; font-size: 24px;">👍</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('👎')" style="cursor: pointer; font-size: 24px;">👎</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('👏')" style="cursor: pointer; font-size: 24px;">👏</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('🙌')" style="cursor: pointer; font-size: 24px;">🙌</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('🤝')" style="cursor: pointer; font-size: 24px;">🤝</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('📱')" style="cursor: pointer; font-size: 24px;">📱</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('💻')" style="cursor: pointer; font-size: 24px;">💻</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('📷')" style="cursor: pointer; font-size: 24px;">📷</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('🎮')" style="cursor: pointer; font-size: 24px;">🎮</span>
|
||||
<span class="emoji-option" onclick="insertEmoji('📚')" style="cursor: pointer; font-size: 24px;">📚</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Función para mostrar/ocultar el selector de emojis
|
||||
function toggleEmojiPicker() {
|
||||
const picker = document.getElementById('emoji-picker');
|
||||
if (picker.style.display === 'none' || !picker.style.display) {
|
||||
picker.style.display = 'block';
|
||||
} else {
|
||||
picker.style.display = 'none';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Función para insertar un emoji en el campo de texto
|
||||
function insertEmoji(emoji) {
|
||||
const input = document.getElementById('message-text');
|
||||
const start = input.selectionStart;
|
||||
const end = input.selectionEnd;
|
||||
const text = input.value;
|
||||
input.value = text.substring(0, start) + emoji + text.substring(end);
|
||||
input.focus();
|
||||
input.setSelectionRange(start + emoji.length, start + emoji.length);
|
||||
document.getElementById('emoji-picker').style.display = 'none';
|
||||
}
|
||||
|
||||
// Cerrar el selector al hacer clic fuera de él
|
||||
document.addEventListener('click', function(e) {
|
||||
const picker = document.getElementById('emoji-picker');
|
||||
const trigger = document.getElementById('emoji-trigger');
|
||||
if (picker && trigger &&
|
||||
e.target !== picker && !picker.contains(e.target) &&
|
||||
e.target !== trigger && !trigger.contains(e.target)) {
|
||||
picker.style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
|
||||
<script>
|
||||
// Función para insertar emojis en el campo de texto
|
||||
function insertEmoji(emoji) {
|
||||
const input = document.getElementById('message-text');
|
||||
const start = input.selectionStart;
|
||||
const end = input.selectionEnd;
|
||||
const text = input.value;
|
||||
|
||||
// Insertar el emoji en la posición del cursor
|
||||
input.value = text.substring(0, start) + emoji + text.substring(end);
|
||||
|
||||
// Mover el cursor después del emoji insertado
|
||||
const newPos = start + emoji.length;
|
||||
input.selectionStart = input.selectionEnd = newPos;
|
||||
|
||||
// Enfocar el campo de texto
|
||||
input.focus();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const userLinks = document.querySelectorAll('.user-list .list-group-item');
|
||||
const chatHistory = document.getElementById('chat-history');
|
||||
const formContainer = document.getElementById('message-form-container');
|
||||
const messageForm = document.getElementById('message-form');
|
||||
const chatIdInput = document.getElementById('chat-id-input');
|
||||
const messageTextInput = document.getElementById('message-text');
|
||||
|
||||
let activeChatId = null;
|
||||
let lastMessageId = 0;
|
||||
let refreshInterval = null;
|
||||
|
||||
userLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Marcar usuario activo
|
||||
userLinks.forEach(l => l.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Detener el intervalo anterior si existe
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
|
||||
// Establecer el nuevo chat activo
|
||||
activeChatId = this.getAttribute('data-chat-id');
|
||||
chatIdInput.value = activeChatId;
|
||||
lastMessageId = 0; // Reiniciar el ID del último mensaje
|
||||
|
||||
// Cargar el historial y comenzar a actualizar
|
||||
loadChatHistory(activeChatId);
|
||||
|
||||
// Iniciar la actualización automática con un intervalo dinámico
|
||||
let refreshDelay = 500; // Comenzar con 0.5 segundos
|
||||
|
||||
function startRefreshTimer() {
|
||||
if (refreshInterval) clearInterval(refreshInterval);
|
||||
|
||||
refreshInterval = setInterval(() => {
|
||||
if (activeChatId && !document.hidden) {
|
||||
checkForNewMessages(activeChatId);
|
||||
}
|
||||
}, refreshDelay);
|
||||
}
|
||||
|
||||
// Iniciar el temporizador
|
||||
startRefreshTimer();
|
||||
});
|
||||
});
|
||||
|
||||
function loadChatHistory(chatId, onlyNew = false) {
|
||||
// Usar un timestamp para evitar caché del navegador
|
||||
const timestamp = new Date().getTime();
|
||||
const url = `get_chat_history.php?chat_id=${chatId}${onlyNew ? '&last_id=' + lastMessageId : ''}&_=${timestamp}`;
|
||||
|
||||
if (!onlyNew) {
|
||||
chatHistory.innerHTML = '<div class="text-center text-muted my-auto"><div class="spinner-border" role="status"><span class="visually-hidden">Cargando...</span></div></div>';
|
||||
formContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Datos recibidos:', data); // Depuración
|
||||
|
||||
if (onlyNew && (!data.history || data.history.length === 0)) {
|
||||
return; // No hay mensajes nuevos
|
||||
}
|
||||
|
||||
if (!onlyNew) {
|
||||
chatHistory.innerHTML = '';
|
||||
}
|
||||
|
||||
let hasNewMessages = false;
|
||||
|
||||
if (data.success && data.history && data.history.length > 0) {
|
||||
// Ordenar los mensajes por ID para asegurar el orden correcto
|
||||
data.history.sort((a, b) => a.id - b.id);
|
||||
|
||||
// Ordenar los mensajes por ID para asegurar el orden correcto
|
||||
data.history.sort((a, b) => a.id - b);
|
||||
|
||||
data.history.forEach(msg => {
|
||||
// Solo agregar mensajes más recientes que el último mostrado
|
||||
if (msg.id > lastMessageId || !onlyNew) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.classList.add('message', msg.direction);
|
||||
// Usar innerHTML para permitir que el enlace de traducción se añada correctamente
|
||||
messageDiv.innerHTML = `<div class="message-text">${msg.message_text.replace(/\n/g, '<br>')}</div>`;
|
||||
|
||||
// Add translate link if needed
|
||||
// For testing, always show for incoming messages
|
||||
if (msg.direction === 'in') { // msg.language_code !== 'es'
|
||||
const translateLink = document.createElement('a');
|
||||
translateLink.href = '#';
|
||||
translateLink.textContent = ' (Traducir)';
|
||||
translateLink.classList.add('translate-link');
|
||||
translateLink.setAttribute('data-message-id', msg.id);
|
||||
messageDiv.appendChild(translateLink);
|
||||
}
|
||||
|
||||
// Agregar el mensaje al historial
|
||||
chatHistory.appendChild(messageDiv);
|
||||
|
||||
// Actualizar el ID del último mensaje
|
||||
if (msg.id > lastMessageId) {
|
||||
lastMessageId = msg.id;
|
||||
}
|
||||
|
||||
hasNewMessages = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Desplazarse al final solo si hay mensajes nuevos
|
||||
if (hasNewMessages) {
|
||||
chatHistory.scrollTop = chatHistory.scrollHeight;
|
||||
}
|
||||
} else if (!onlyNew) {
|
||||
chatHistory.innerHTML = '<div class="text-center text-muted my-auto">No hay mensajes en esta conversación.</div>';
|
||||
}
|
||||
|
||||
// Si no es una actualización, desplazarse al final
|
||||
if (!onlyNew) {
|
||||
chatHistory.scrollTop = chatHistory.scrollHeight;
|
||||
}
|
||||
|
||||
// Actualizar lastMessageId si viene en la respuesta
|
||||
if (data.last_id && data.last_id > lastMessageId) {
|
||||
lastMessageId = data.last_id;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (!onlyNew) {
|
||||
console.error('Error al cargar el historial:', error);
|
||||
chatHistory.innerHTML = '<div class="text-center text-danger my-auto">Error al cargar el historial. Intenta recargar la página.</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Función para verificar mensajes nuevos
|
||||
function checkForNewMessages(chatId) {
|
||||
if (!document.hidden && chatId) {
|
||||
// Usar fetch con headers para evitar caché
|
||||
fetch(`get_chat_history.php?chat_id=${chatId}&last_id=${lastMessageId}&_=${new Date().getTime()}`, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Datos recibidos en checkForNewMessages:', data); // Depuración
|
||||
|
||||
if (data.success && data.history && data.history.length > 0) {
|
||||
let hasNewMessages = false;
|
||||
|
||||
// Ordenar los mensajes por ID para asegurar el orden correcto
|
||||
data.history.sort((a, b) => a.id - b);
|
||||
|
||||
data.history.forEach(msg => {
|
||||
if (msg.id > lastMessageId) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.classList.add('message', msg.direction);
|
||||
messageDiv.textContent = msg.message_text;
|
||||
|
||||
chatHistory.appendChild(messageDiv);
|
||||
|
||||
if (msg.id > lastMessageId) {
|
||||
lastMessageId = msg.id;
|
||||
}
|
||||
|
||||
hasNewMessages = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasNewMessages) {
|
||||
chatHistory.scrollTop = chatHistory.scrollHeight;
|
||||
// Si hay mensajes nuevos, reiniciar el temporizador con el intervalo más corto
|
||||
refreshDelay = 2000;
|
||||
startRefreshTimer();
|
||||
}
|
||||
}
|
||||
|
||||
if (data.last_id && data.last_id > lastMessageId) {
|
||||
lastMessageId = data.last_id;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error al verificar mensajes nuevos:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar el chat cuando la pestaña vuelve a estar activa
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (!document.hidden && activeChatId) {
|
||||
loadChatHistory(activeChatId);
|
||||
}
|
||||
|
||||
// Inicializar el selector de emojis
|
||||
console.log('Inicializando selector de emojis...');
|
||||
let emojiPickerVisible = false;
|
||||
const emojiTrigger = document.getElementById('emoji-trigger');
|
||||
const emojiPicker = document.getElementById('emoji-picker');
|
||||
const messageTextInput = document.getElementById('message-text');
|
||||
|
||||
console.log('Elementos del selector de emojis:', {
|
||||
emojiTrigger: emojiTrigger ? 'Encontrado' : 'No encontrado',
|
||||
emojiPicker: emojiPicker ? 'Encontrado' : 'No encontrado',
|
||||
messageTextInput: messageTextInput ? 'Encontrado' : 'No encontrado'
|
||||
});
|
||||
|
||||
// Asegurarse de que el selector esté oculto inicialmente
|
||||
if (emojiPicker) {
|
||||
emojiPicker.style.display = 'none';
|
||||
emojiPicker.style.backgroundColor = '#fff';
|
||||
emojiPicker.style.border = '2px solid red';
|
||||
emojiPicker.innerHTML = '<div style="padding: 10px;">Selector de emojis</div>' + emojiPicker.innerHTML;
|
||||
}
|
||||
|
||||
// Mostrar/ocultar selector de emojis
|
||||
if (emojiTrigger) {
|
||||
emojiTrigger.addEventListener('click', function(e) {
|
||||
console.log('Botón de emoji clickeado');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!emojiPicker) {
|
||||
console.error('El selector de emojis no se encontró en el DOM');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (emojiPickerVisible) {
|
||||
console.log('Ocultando selector de emojis');
|
||||
emojiPicker.style.display = 'none';
|
||||
} else {
|
||||
console.log('Mostrando selector de emojis');
|
||||
emojiPicker.style.display = 'block';
|
||||
// Posicionar el selector debajo del botón
|
||||
const rect = emojiTrigger.getBoundingClientRect();
|
||||
emojiPicker.style.position = 'fixed';
|
||||
emojiPicker.style.top = (rect.bottom + window.scrollY) + 'px';
|
||||
emojiPicker.style.left = (rect.left + window.scrollX) + 'px';
|
||||
emojiPicker.style.zIndex = '9999';
|
||||
console.log('Posición del selector:', emojiPicker.style.top, emojiPicker.style.left);
|
||||
}
|
||||
|
||||
emojiPickerVisible = !emojiPickerVisible;
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
console.error('No se pudo encontrar el botón de emoji');
|
||||
}
|
||||
|
||||
// Cerrar el selector al hacer clic fuera
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!emojiPicker || !emojiTrigger) return;
|
||||
|
||||
const isClickInside = emojiPicker.contains(e.target) || emojiTrigger.contains(e.target);
|
||||
|
||||
if (emojiPickerVisible && !isClickInside) {
|
||||
console.log('Clic fuera del selector, ocultando...');
|
||||
emojiPicker.style.display = 'none';
|
||||
emojiPickerVisible = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Manejar clics en emojis
|
||||
if (emojiPicker) {
|
||||
emojiPicker.addEventListener('click', function(e) {
|
||||
console.log('Clic en el selector de emojis');
|
||||
const emojiOption = e.target.closest('.emoji-option');
|
||||
if (emojiOption) {
|
||||
const emojiChar = emojiOption.getAttribute('data-emoji');
|
||||
console.log('Emoji seleccionado:', emojiChar);
|
||||
if (emojiChar) {
|
||||
insertEmoji(emojiChar);
|
||||
emojiPicker.style.display = 'none';
|
||||
emojiPickerVisible = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Evento para enviar mensaje
|
||||
messageForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const messageText = messageTextInput.value.trim();
|
||||
if (!messageText) return;
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
// Optimistic UI update
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message out`;
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message-text">${messageText.replace(/\n/g, '<br>')}</div>
|
||||
<div class="message-time">${new Date().toLocaleTimeString()}</div>
|
||||
`;
|
||||
chatHistory.appendChild(messageDiv);
|
||||
chatHistory.scrollTop = chatHistory.scrollHeight;
|
||||
|
||||
messageTextInput.value = '';
|
||||
|
||||
fetch('admin_send_message.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
alert('Error al enviar el mensaje: ' + data.error);
|
||||
// Opcional: eliminar el mensaje de la UI si falla
|
||||
chatHistory.removeChild(messageDiv);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error de red al enviar el mensaje.');
|
||||
chatHistory.removeChild(messageDiv);
|
||||
});
|
||||
});
|
||||
|
||||
// Evento para traducir mensaje
|
||||
chatHistory.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('translate-link')) {
|
||||
e.preventDefault();
|
||||
const messageId = e.target.getAttribute('data-message-id');
|
||||
const messageDiv = e.target.parentElement;
|
||||
|
||||
fetch(`translate_message.php?message_id=${messageId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const translatedText = document.createElement('div');
|
||||
translatedText.classList.add('translated-text');
|
||||
translatedText.textContent = data.translated_text;
|
||||
messageDiv.appendChild(translatedText);
|
||||
e.target.remove(); // Remove the translate link
|
||||
} else {
|
||||
alert('Error al traducir el mensaje: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error de red al traducir el mensaje.');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
81
check_webhook.php
Executable file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config/config.php';
|
||||
require_once __DIR__ . '/includes/db.php'; // Necesario para la instancia de PDO
|
||||
require_once __DIR__ . '/src/TelegramSender.php';
|
||||
|
||||
$botToken = $_ENV['TELEGRAM_BOT_TOKEN'];
|
||||
$webhookUrl = 'https://' . $_SERVER['HTTP_HOST'] . '/telegram_bot_webhook.php?auth_token=' . urlencode($_ENV['TELEGRAM_WEBHOOK_TOKEN']);
|
||||
|
||||
// Instanciar TelegramSender con la conexión PDO
|
||||
$telegramSender = new TelegramSender($botToken, $pdo); // $pdo debe estar disponible desde includes/db.php
|
||||
|
||||
// Verificar el webhook actual
|
||||
$apiUrl = "https://api.telegram.org/bot{$botToken}/getWebhookInfo";
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
header('Content-Type: text/plain');
|
||||
echo "=== Estado actual del webhook ===\n";
|
||||
if ($httpCode === 200) {
|
||||
$result = json_decode($response, true);
|
||||
echo "URL actual: " . ($result['result']['url'] ?? 'No configurado') . "\n";
|
||||
echo "Tiene certificado: " . ($result['result']['has_custom_certificate'] ? 'Sí' : 'No') . "\n";
|
||||
echo "Último error: " . ($result['result']['last_error_message'] ?? 'Ninguno') . "\n";
|
||||
echo "\n";
|
||||
} else {
|
||||
echo "Error al obtener información del webhook: " . $error . "\n";
|
||||
}
|
||||
|
||||
// Configurar el webhook
|
||||
echo "\n=== Configurando webhook ===\n";
|
||||
$apiUrl = "https://api.telegram.org/bot{$botToken}/setWebhook";
|
||||
$postData = [
|
||||
'url' => $webhookUrl,
|
||||
'max_connections' => 40,
|
||||
'allowed_updates' => ['message', 'callback_query']
|
||||
];
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode === 200) {
|
||||
$result = json_decode($response, true);
|
||||
if ($result['ok'] === true) {
|
||||
echo "✅ Webhook configurado correctamente en: " . $webhookUrl . "\n";
|
||||
} else {
|
||||
echo "❌ Error al configurar el webhook: " . ($result['description'] ?? 'Error desconocido') . "\n";
|
||||
}
|
||||
} else {
|
||||
echo "❌ Error HTTP al configurar el webhook: " . $httpCode . " - " . $error . "\n";
|
||||
if ($response) {
|
||||
echo "Respuesta: " . $response . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== Prueba de envío de mensaje ===\n";
|
||||
$chatId = 'YOUR_CHAT_ID'; // Reemplaza con tu ID de chat de Telegram
|
||||
$message = "✅ Webhook configurado correctamente en: " . $webhookUrl;
|
||||
|
||||
// Enviar el mensaje usando TelegramSender
|
||||
$response = $telegramSender->sendMessage($chatId, $message);
|
||||
|
||||
if ($response) {
|
||||
echo "✅ Mensaje de prueba enviado correctamente a tu chat de Telegram.\n";
|
||||
} else {
|
||||
echo "❌ No se pudo enviar el mensaje de prueba. Asegúrate de que el bot te haya enviado un mensaje primero.\n";
|
||||
if ($error) {
|
||||
echo "Error: " . $error . "\n";
|
||||
}
|
||||
if ($response) {
|
||||
echo "Respuesta: " . json_encode($response) . "\n";
|
||||
}
|
||||
}
|
||||
57
common/helpers/converter_factory.php
Executable file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* Factory para crear Converters según la plataforma
|
||||
*
|
||||
* Este factory simplifica la creación de converters HTML para diferentes plataformas
|
||||
* usando la nueva estructura de directorios.
|
||||
*
|
||||
* Ubicación: /common/helpers/converter_factory.php
|
||||
* Fecha de creación: 2025-11-25
|
||||
*/
|
||||
|
||||
class ConverterFactory {
|
||||
|
||||
/**
|
||||
* Crea un converter para la plataforma especificada
|
||||
*
|
||||
* @param string $platform 'telegram' o 'discord'
|
||||
* @return HtmlToTelegramHtmlConverter|HtmlToDiscordMarkdownConverter
|
||||
* @throws Exception Si la plataforma no es soportada
|
||||
*/
|
||||
public static function create($platform) {
|
||||
switch(strtolower($platform)) {
|
||||
case 'telegram':
|
||||
require_once __DIR__ . '/../../telegram/converters/HtmlToTelegramHtmlConverter.php';
|
||||
return new HtmlToTelegramHtmlConverter();
|
||||
|
||||
case 'discord':
|
||||
require_once __DIR__ . '/../../discord/converters/HtmlToDiscordMarkdownConverter.php';
|
||||
return new HtmlToDiscordMarkdownConverter();
|
||||
|
||||
default:
|
||||
throw new Exception("Plataforma no soportada: $platform. Use 'telegram' o 'discord'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte HTML a formato específico de plataforma
|
||||
*
|
||||
* @param string $html Contenido HTML a convertir
|
||||
* @param string $platform Plataforma destino
|
||||
* @return string Contenido convertido
|
||||
*/
|
||||
public static function convert($html, $platform) {
|
||||
$converter = self::create($platform);
|
||||
return $converter->convert($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si una plataforma es soportada
|
||||
*
|
||||
* @param string $platform
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported($platform) {
|
||||
return in_array(strtolower($platform), ['telegram', 'discord']);
|
||||
}
|
||||
}
|
||||
547
common/helpers/emojis.php
Executable file
@@ -0,0 +1,547 @@
|
||||
<?php
|
||||
/**
|
||||
* Funcionalidad para el manejo de emojis en el editor Summernote
|
||||
*/
|
||||
|
||||
// Limpiar cualquier salida previa
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Establecer el tipo de contenido como JavaScript
|
||||
header('Content-Type: application/javascript; charset=utf-8');
|
||||
|
||||
// Evitar caché
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
|
||||
// Iniciar sesión si no está iniciada
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start(['read_and_close' => true]);
|
||||
}
|
||||
|
||||
// Inicializar array de emojis personalizados si no existe
|
||||
if (!isset($_SESSION['custom_emojis'])) {
|
||||
$_SESSION['custom_emojis'] = [];
|
||||
}
|
||||
|
||||
// Si es una petición POST para agregar un emoji personalizado
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['emoji'])) {
|
||||
$emoji = trim($_POST['emoji']);
|
||||
if (!empty($emoji) && !in_array($emoji, $_SESSION['custom_emojis'] ?? [])) {
|
||||
array_unshift($_SESSION['custom_emojis'], $emoji);
|
||||
$_SESSION['custom_emojis'] = array_slice($_SESSION['custom_emojis'], 0, 50);
|
||||
echo json_encode(['success' => true, 'message' => 'Emoji guardado correctamente']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'No se pudo guardar el emoji']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// Si llegamos aquí, es una petición GET para cargar los emojis
|
||||
// Generamos el código JavaScript directamente
|
||||
|
||||
echo "// Asegurarse de que la función no se defina múltiples veces\n";
|
||||
echo "if (typeof window.setupEmojiHandlers === 'undefined') {\n";
|
||||
echo " // Función para manejar los eventos de los emojis\n";
|
||||
echo " window.setupEmojiHandlers = function() {\n";
|
||||
echo " console.log('Inicializando manejadores de emojis...');\n\n";
|
||||
echo " // Función para manejar clics en emojis\n";
|
||||
echo " function handleEmojiClick(e) {\n";
|
||||
echo " const emoji = e.target.closest('.emoji-item');\n";
|
||||
echo " if (!emoji) return;\n\n";
|
||||
echo " e.preventDefault();\n";
|
||||
echo " e.stopPropagation();\n\n";
|
||||
echo " const emojiChar = emoji.getAttribute('data-emoji');\n";
|
||||
echo " if (!emojiChar) return;\n\n";
|
||||
echo " console.log('Emoji seleccionado:', emojiChar);\n\n";
|
||||
echo " // Intentar encontrar el editor en diferentes contextos\n";
|
||||
echo " const contexts = [window, window.parent, window.opener, window.top];\n";
|
||||
echo " let emojiInserted = false;\n\n";
|
||||
echo " for (const ctx of contexts) {\n";
|
||||
echo " try {\n";
|
||||
echo " if (ctx && ctx.$ && ctx.$('#messageContent').length > 0) {\n";
|
||||
echo " ctx.$('#messageContent').summernote('editor.saveRange');\n";
|
||||
echo " ctx.$('#messageContent').summernote('editor.restoreRange');\n";
|
||||
echo " ctx.$('#messageContent').summernote('editor.focus');\n";
|
||||
echo " ctx.$('#messageContent').summernote('editor.insertText', emojiChar);\n";
|
||||
echo " emojiInserted = true;\n";
|
||||
echo " break;\n";
|
||||
echo " }\n";
|
||||
echo " } catch (e) {\n";
|
||||
echo " console.error('Error al insertar emoji en contexto:', e);\n";
|
||||
echo " }\n";
|
||||
echo " }\n\n";
|
||||
echo " if (!emojiInserted) {\n";
|
||||
echo " console.warn('No se pudo insertar el emoji: No se encontró el editor Summernote');\n";
|
||||
echo " alert('No se pudo insertar el emoji. Asegúrate de que el editor esté disponible.');\n";
|
||||
echo " }\n\n";
|
||||
echo " // Cerrar el modal después de seleccionar un emoji\n";
|
||||
echo " $('#emojiModal').modal('hide');\n";
|
||||
echo " }\n\n";
|
||||
echo " // Función para manejar clics en pestañas\n";
|
||||
echo " function handleTabClick(e) {\n";
|
||||
echo " const tab = e.target.closest('.emoji-tab');\n";
|
||||
echo " if (!tab) return;\n\n";
|
||||
echo " e.preventDefault();\n";
|
||||
echo " e.stopPropagation();\n\n";
|
||||
echo " const category = tab.getAttribute('data-category');\n";
|
||||
echo " if (!category) return;\n\n";
|
||||
echo " console.log('Cambiando a categoría:', category);\n\n";
|
||||
echo " // Actualizar pestaña activa\n";
|
||||
echo " document.querySelectorAll('.emoji-tab').forEach(function(t) {\n";
|
||||
echo " t.classList.remove('btn-primary');\n";
|
||||
echo " t.classList.add('btn-outline-secondary');\n";
|
||||
echo " });\n\n";
|
||||
echo " tab.classList.remove('btn-outline-secondary');\n";
|
||||
echo " tab.classList.add('btn-primary');\n\n";
|
||||
echo " // Mostrar la categoría seleccionada\n";
|
||||
echo " document.querySelectorAll('.emoji-category').forEach(function(cat) {\n";
|
||||
echo " cat.classList.add('d-none');\n";
|
||||
echo " });\n\n";
|
||||
echo " const targetCategory = document.getElementById('emoji-' + category);\n";
|
||||
echo " if (targetCategory) {\n";
|
||||
echo " targetCategory.classList.remove('d-none');\n";
|
||||
echo " }\n";
|
||||
echo " }\n\n";
|
||||
echo " // Eliminar manejadores anteriores para evitar duplicados\n";
|
||||
echo " document.removeEventListener('click', handleEmojiClick);\n";
|
||||
echo " document.removeEventListener('click', handleTabClick);\n\n";
|
||||
echo " // Agregar nuevos manejadores\n";
|
||||
echo " document.addEventListener('click', handleEmojiClick);\n";
|
||||
echo " document.addEventListener('click', handleTabClick);\n\n";
|
||||
echo " // Activar la primera pestaña por defecto si no hay ninguna activa\n";
|
||||
echo " const activeTab = document.querySelector('.emoji-tab.btn-primary');\n";
|
||||
echo " if (!activeTab) {\n";
|
||||
echo " const firstTab = document.querySelector('.emoji-tab');\n";
|
||||
echo " if (firstTab) {\n";
|
||||
echo " firstTab.click();\n";
|
||||
echo " }\n";
|
||||
echo " }\n";
|
||||
echo " }; // Cierre de la función setupEmojiHandlers\n";
|
||||
echo "} // Cierre del if que verifica si la función ya está definida\n\n";
|
||||
|
||||
// Lista de emojis organizados por categorías
|
||||
$emojis = [
|
||||
'caritas' => ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩'],
|
||||
'manos' => ['👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌', '🤲', '🤝', '🙏'],
|
||||
'simbolos' => ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️', '✝️', '☪️', '🕉️', '☸️', '✡️', '🔯', '🕎', '☯️', '☦️', '🛐'],
|
||||
'objetos' => ['📱', '📲', '💻', '⌨️', '🖥️', '🖨️', '🖱️', '🖲️', '🕹️', '⌚', '📷', '📸', '📹', '🎥', '📽️', '🎞️', '📞', '☎️', '📟', '📠', '📺', '📻', '🎙️', '🎚️', '🎛️', '🧭', '⏱️', '⏲️', '⏰', '🕰️'],
|
||||
'naturaleza' => ['🐵', '🐒', '🦍', '🦧', '🐶', '🐕', '🦮', '🐕🦺', '🐩', '🐺', '🦊', '🦝', '🐱', '🐈', '🦁', '🐯', '🐅', '🐆', '🐴', '🐎', '🦄', '🦓', '🦌', '🐮', '🐂', '🐃', '🐄', '🐷', '🐖', '🐗']
|
||||
];
|
||||
|
||||
// Agregar emojis personalizados si existen
|
||||
if (!empty($_SESSION['custom_emojis'])) {
|
||||
$emojis['personalizados'] = $_SESSION['custom_emojis'];
|
||||
} else {
|
||||
$emojis['personalizados'] = [];
|
||||
}
|
||||
|
||||
// Nombres de categorías más amigables
|
||||
$categoryNames = [
|
||||
'caritas' => 'Caritas',
|
||||
'manos' => 'Manos',
|
||||
'simbolos' => 'Símbolos',
|
||||
'objetos' => 'Objetos',
|
||||
'naturaleza' => 'Naturaleza',
|
||||
'personalizados' => 'Mis Emojis'
|
||||
];
|
||||
|
||||
// Manejar la adición de un nuevo emoji personalizado
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['emoji'])) {
|
||||
// Limpiar el buffer de salida
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Establecer el tipo de contenido como JSON
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Validar y limpiar el emoji
|
||||
$emoji = trim($_POST['emoji']);
|
||||
|
||||
// Validar que el emoji no esté vacío
|
||||
if (empty($emoji)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'El emoji no puede estar vacío']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Iniciar la sesión si no está iniciada
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Inicializar el array de emojis personalizados si no existe
|
||||
if (!isset($_SESSION['custom_emojis'])) {
|
||||
$_SESSION['custom_emojis'] = [];
|
||||
}
|
||||
|
||||
// Verificar si el emoji ya existe
|
||||
if (!in_array($emoji, $_SESSION['custom_emojis'])) {
|
||||
// Agregar el nuevo emoji al inicio del array
|
||||
array_unshift($_SESSION['custom_emojis'], $emoji);
|
||||
|
||||
// Mantener solo los últimos 50 emojis
|
||||
$_SESSION['custom_emojis'] = array_slice($_SESSION['custom_emojis'], 0, 50);
|
||||
|
||||
// Devolver éxito
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Emoji guardado correctamente',
|
||||
'emoji' => $emoji
|
||||
]);
|
||||
} else {
|
||||
// El emoji ya existe
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Este emoji ya existe en tu colección'
|
||||
]);
|
||||
}
|
||||
|
||||
// Terminar la ejecución
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container-fluid p-0">
|
||||
<!-- Campo para emoji personalizado -->
|
||||
<div class="p-3 border-bottom">
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" id="customEmojiInput" class="form-control" placeholder="Pega o escribe un emoji aquí">
|
||||
<button class="btn btn-primary" id="addCustomEmoji" type="button">
|
||||
<i class="bi bi-plus-lg"></i> Agregar
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">Puedes pegar cualquier emoji o carácter especial</small>
|
||||
</div>
|
||||
|
||||
<!-- Pestañas de categorías -->
|
||||
<div class="emoji-tabs d-flex flex-wrap border-bottom px-3 pt-2">
|
||||
<?php $first = true; ?>
|
||||
<?php foreach (array_keys($emojis) as $category): ?>
|
||||
<button type="button"
|
||||
class="emoji-tab btn btn-sm me-1 mb-1 <?php echo $first ? 'btn-primary' : 'btn-outline-secondary'; ?>"
|
||||
data-category="<?php echo $category; ?>">
|
||||
<?php echo $categoryNames[$category] ?? ucfirst($category); ?>
|
||||
</button>
|
||||
<?php $first = false; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Contenido de emojis por categoría -->
|
||||
<div class="emoji-categories">
|
||||
<?php $first = true; ?>
|
||||
<?php foreach ($emojis as $category => $emojiList): ?>
|
||||
<div class="emoji-category <?php echo $first ? 'd-block' : 'd-none'; ?>" id="emoji-<?php echo $category; ?>">
|
||||
<div class="d-flex flex-wrap">
|
||||
<?php
|
||||
foreach ($emojiList as $emoji):
|
||||
// Determinar si es un emoji largo (más de 2 caracteres)
|
||||
$isLongEmoji = mb_strlen($emoji) > 2;
|
||||
$emojiClass = $isLongEmoji ? 'long-emoji' : '';
|
||||
?>
|
||||
<span class="emoji-option d-flex align-items-center justify-content-center m-1 <?php echo $emojiClass; ?>"
|
||||
data-emoji="<?php echo htmlspecialchars($emoji); ?>"
|
||||
title="<?php echo htmlspecialchars($emoji); ?>">
|
||||
<?php echo $emoji; ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php $first = false; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#customEmojiInput {
|
||||
font-size: 1.2em;
|
||||
height: 45px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.emoji-option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
max-width: 100%;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
transition: all 0.2s;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
.emoji-option:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: scale(1.1);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Estilo para el contenedor de emojis */
|
||||
.emoji-categories {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Ajustes para emojis largos */
|
||||
.emoji-option.long-emoji {
|
||||
font-size: 16px;
|
||||
padding: 2px;
|
||||
white-space: normal;
|
||||
line-height: 1.1;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Manejar el botón de agregar emoji personalizado
|
||||
document.getElementById('addCustomEmoji').addEventListener('click', async function() {
|
||||
const emojiInput = document.getElementById('customEmojiInput');
|
||||
const emoji = emojiInput.value.trim();
|
||||
const button = this; // Mover la declaración de button al inicio
|
||||
let originalText = button.innerHTML; // Usar let en lugar de const
|
||||
|
||||
if (emoji) {
|
||||
try {
|
||||
// Mostrar indicador de carga
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Guardando...';
|
||||
|
||||
// Guardar el emoji en la sesión
|
||||
const response = await fetch('includes/emojis.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'emoji=' + encodeURIComponent(emoji),
|
||||
credentials: 'same-origin' // Asegurar que se envíen las cookies de sesión
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Insertar el emoji
|
||||
if (window.parent && window.parent.insertEmoji) {
|
||||
window.parent.insertEmoji(emoji + ' ');
|
||||
|
||||
// Recargar solo los emojis para actualizar la lista
|
||||
const emojiContainer = document.querySelector('.emoji-categories');
|
||||
if (emojiContainer) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Inicializar manejadores
|
||||
initEmojiHandlers();
|
||||
|
||||
// Enfocar el campo de entrada
|
||||
const emojiInput = document.getElementById('customEmojiInput');
|
||||
if (emojiInput) {
|
||||
emojiInput.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Inicializar manejadores de eventos
|
||||
function initEmojiHandlers() {
|
||||
// Manejador para agregar emojis personalizados
|
||||
const addButton = document.getElementById('addCustomEmoji');
|
||||
const emojiInput = document.getElementById('customEmojiInput');
|
||||
|
||||
if (addButton && emojiInput) {
|
||||
// Eliminar manejadores anteriores para evitar duplicados
|
||||
addButton.replaceWith(addButton.cloneNode(true));
|
||||
emojiInput.replaceWith(emojiInput.cloneNode(true));
|
||||
|
||||
// Manejador para el botón de agregar
|
||||
document.getElementById('addCustomEmoji').addEventListener('click', handleAddEmoji);
|
||||
|
||||
// Manejador para la tecla Enter
|
||||
document.getElementById('customEmojiInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddEmoji();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar manejadores de emojis y pestañas
|
||||
setupEmojiHandlers();
|
||||
}
|
||||
|
||||
// Manejar la adición de un nuevo emoji
|
||||
async function handleAddEmoji() {
|
||||
const emojiInput = document.getElementById('customEmojiInput');
|
||||
const button = document.getElementById('addCustomEmoji');
|
||||
const emoji = emojiInput.value.trim();
|
||||
|
||||
if (!emoji) {
|
||||
showAlert('Por favor, ingresa un emoji', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Deshabilitar el botón y mostrar indicador de carga
|
||||
const originalText = button.innerHTML;
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Guardando...';
|
||||
|
||||
try {
|
||||
// Usar una ruta relativa desde la ubicación actual
|
||||
const response = await fetch(window.location.pathname, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'emoji=' + encodeURIComponent(emoji)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error HTTP: ${response.status}`);
|
||||
|
||||
foreach ($items as $emoji) {
|
||||
$emojiHtml = htmlspecialchars($emoji);
|
||||
$html .= "<button type=\"button\" class=\"btn btn-outline-secondary btn-sm emoji-item\" data-emoji=\"{$emojiHtml}\">{$emojiHtml}</button>";
|
||||
}
|
||||
|
||||
$html .= '</div><hr class="my-3"></div>';
|
||||
}
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
// Formulario para agregar emojis personalizados
|
||||
$html .= '<div class="p-3 border-top">';
|
||||
$html .= '<form id="addCustomEmojiForm" class="d-flex gap-2">';
|
||||
$html .= '<input type="text" id="customEmojiInput" class="form-control form-control-sm" placeholder="Pega un emoji aquí..." required>';
|
||||
$html .= '<button type="submit" class="btn btn-primary btn-sm">Agregar</button>';
|
||||
$html .= '</form></div></div>';
|
||||
|
||||
// Devolver el HTML como una cadena de JavaScript
|
||||
$js = "
|
||||
document.getElementById('emojiContainer').innerHTML = " . json_encode($html) . ";
|
||||
";
|
||||
|
||||
// Agregar el código JavaScript
|
||||
$js .= "
|
||||
// Asegurarse de que la función no se defina múltiples veces
|
||||
if (typeof window.setupEmojiHandlers === 'undefined') {
|
||||
// Función para manejar los eventos de los emojis
|
||||
window.setupEmojiHandlers = function() {
|
||||
console.log('Inicializando manejadores de emojis...');
|
||||
|
||||
// Función para manejar clics en emojis
|
||||
function handleEmojiClick(e) {
|
||||
const emoji = e.target.closest('.emoji-item');
|
||||
if (!emoji) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const emojiChar = emoji.getAttribute('data-emoji');
|
||||
if (!emojiChar) return;
|
||||
|
||||
console.log('Emoji seleccionado:', emojiChar);
|
||||
|
||||
// Intentar encontrar el editor en diferentes contextos
|
||||
const contexts = [window, window.parent, window.opener, window.top];
|
||||
let emojiInserted = false;
|
||||
|
||||
for (const ctx of contexts) {
|
||||
try {
|
||||
if (ctx && ctx.$ && ctx.$('#messageContent').length > 0) {
|
||||
ctx.$('#messageContent').summernote('editor.saveRange');
|
||||
ctx.$('#messageContent').summernote('editor.restoreRange');
|
||||
ctx.$('#messageContent').summernote('editor.focus');
|
||||
ctx.$('#messageContent').summernote('editor.insertText', emojiChar);
|
||||
emojiInserted = true;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error al insertar emoji en contexto:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!emojiInserted) {
|
||||
console.warn('No se pudo insertar el emoji: No se encontró el editor Summernote');
|
||||
alert('No se pudo insertar el emoji. Asegúrate de que el editor esté disponible.');
|
||||
}
|
||||
|
||||
// Cerrar el modal después de seleccionar un emoji
|
||||
$('#emojiModal').modal('hide');
|
||||
}
|
||||
|
||||
// Función para manejar clics en pestañas
|
||||
function handleTabClick(e) {
|
||||
const tab = e.target.closest('.emoji-tab');
|
||||
if (!tab) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const category = tab.getAttribute('data-category');
|
||||
if (!category) return;
|
||||
|
||||
console.log('Cambiando a categoría:', category);
|
||||
|
||||
// Actualizar pestaña activa
|
||||
document.querySelectorAll('.emoji-tab').forEach(function(t) {
|
||||
t.classList.remove('btn-primary');
|
||||
t.classList.add('btn-outline-secondary');
|
||||
});
|
||||
|
||||
tab.classList.remove('btn-outline-secondary');
|
||||
tab.classList.add('btn-primary');
|
||||
|
||||
// Mostrar la categoría seleccionada
|
||||
document.querySelectorAll('.emoji-category').forEach(function(cat) {
|
||||
cat.classList.add('d-none');
|
||||
});
|
||||
|
||||
const targetCategory = document.getElementById('emoji-' + category);
|
||||
if (targetCategory) {
|
||||
targetCategory.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
// Eliminar manejadores anteriores para evitar duplicados
|
||||
document.removeEventListener('click', handleEmojiClick);
|
||||
document.removeEventListener('click', handleTabClick);
|
||||
|
||||
// Agregar nuevos manejadores
|
||||
document.addEventListener('click', handleEmojiClick);
|
||||
document.addEventListener('click', handleTabClick);
|
||||
|
||||
// Activar la primera pestaña por defecto si no hay ninguna activa
|
||||
const activeTab = document.querySelector('.emoji-tab.btn-primary');
|
||||
if (!activeTab) {
|
||||
const firstTab = document.querySelector('.emoji-tab');
|
||||
if (firstTab) {
|
||||
firstTab.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Inicializar los manejadores de eventos
|
||||
if (typeof setupEmojiHandlers === 'function') {
|
||||
setupEmojiHandlers();
|
||||
}";
|
||||
|
||||
// Devolver el código JavaScript
|
||||
echo $js;
|
||||
49
common/helpers/schedule_helpers.php
Executable file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('calculateNextSendTime')) {
|
||||
/**
|
||||
* Calcula la próxima fecha y hora de envío para un mensaje recurrente.
|
||||
*
|
||||
* @param array $recurringDays Un array de enteros representando los días de la semana (0=Domingo, 6=Sábado).
|
||||
* @param string $recurringTime La hora del día en formato HH:MM.
|
||||
* @param string $timezone La zona horaria a utilizar.
|
||||
* @return string La próxima fecha y hora de envío en formato Y-m-d H:i:s.
|
||||
* @throws Exception Si no se puede determinar la próxima fecha de envío.
|
||||
*/
|
||||
function calculateNextSendTime(array $recurringDays, string $recurringTime, string $timezone = 'America/Mexico_City'): string {
|
||||
$tz = new DateTimeZone($timezone);
|
||||
$now = new DateTime('now', $tz);
|
||||
list($hour, $minute) = explode(':', $recurringTime);
|
||||
|
||||
sort($recurringDays); // Ensure days are sorted
|
||||
|
||||
$nextSendTime = null;
|
||||
|
||||
// 1. Check for today's occurrence first
|
||||
$todayDayOfWeek = (int)$now->format('w');
|
||||
$potentialTodaySendTime = (clone $now)->setTime((int)$hour, (int)$minute, 0);
|
||||
|
||||
if (in_array($todayDayOfWeek, $recurringDays) && $potentialTodaySendTime > $now) {
|
||||
// If today is a recurring day and the time hasn't passed yet, this is the next send time
|
||||
$nextSendTime = $potentialTodaySendTime;
|
||||
} else {
|
||||
// 2. Look for the next recurring day, starting from tomorrow
|
||||
for ($i = 1; $i <= 7; $i++) { // Check next 7 days
|
||||
$checkDate = (clone $now)->modify("+$i days");
|
||||
$dayOfWeek = (int)$checkDate->format('w');
|
||||
|
||||
if (in_array($dayOfWeek, $recurringDays)) {
|
||||
$nextSendTime = (clone $checkDate)->setTime((int)$hour, (int)$minute, 0);
|
||||
break; // Found the earliest future recurring day
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($nextSendTime === null) {
|
||||
throw new Exception('No se pudo determinar la próxima fecha de envío para la configuración recurrente proporcionada.');
|
||||
}
|
||||
|
||||
return $nextSendTime->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
?>
|
||||
58
common/helpers/sender_factory.php
Executable file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* Factory para crear Senders según la plataforma
|
||||
*
|
||||
* Este factory simplifica la creación de senders para diferentes plataformas
|
||||
* usando la nueva estructura de directorios.
|
||||
*
|
||||
* Ubicación: /common/helpers/sender_factory.php
|
||||
* Fecha de creación: 2025-11-25
|
||||
*/
|
||||
|
||||
class SenderFactory {
|
||||
|
||||
/**
|
||||
* Crea un sender para la plataforma especificada
|
||||
*
|
||||
* @param string $platform 'telegram' o 'discord'
|
||||
* @param PDO $pdo Conexión a base de datos (requerida para Telegram)
|
||||
* @return TelegramSender|DiscordSender
|
||||
* @throws Exception Si la plataforma no es soportada
|
||||
*/
|
||||
public static function create($platform, $pdo = null) {
|
||||
switch(strtolower($platform)) {
|
||||
case 'telegram':
|
||||
if ($pdo === null) {
|
||||
throw new Exception("PDO es requerido para TelegramSender");
|
||||
}
|
||||
require_once __DIR__ . '/../../telegram/TelegramSender.php';
|
||||
return new TelegramSender(TELEGRAM_BOT_TOKEN, $pdo);
|
||||
|
||||
case 'discord':
|
||||
require_once __DIR__ . '/../../discord/DiscordSender.php';
|
||||
return new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
|
||||
default:
|
||||
throw new Exception("Plataforma no soportada: $platform. Use 'telegram' o 'discord'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si una plataforma es soportada
|
||||
*
|
||||
* @param string $platform
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported($platform) {
|
||||
return in_array(strtolower($platform), ['telegram', 'discord']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene lista de plataformas soportadas
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getSupportedPlatforms() {
|
||||
return ['telegram', 'discord'];
|
||||
}
|
||||
}
|
||||
19
composer.json
Executable file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"require": {
|
||||
"team-reflex/discord-php": "^7.0",
|
||||
"firebase/php-jwt": "^6.0",
|
||||
"monolog/monolog": "^3.9",
|
||||
"slim/slim": "^4.0",
|
||||
"react/promise-timer": "^1.8",
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "api/src/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
3132
composer.lock
generated
Executable file
13
config/discordbot.conf
Executable file
@@ -0,0 +1,13 @@
|
||||
# Configuración actualizada de Supervisor para Discord Bot
|
||||
# Usar: sudo cp config/discordbot_updated.conf /etc/supervisor/conf.d/discordbot.conf
|
||||
|
||||
[program:discordbot]
|
||||
command=/usr/bin/php /var/www/html/bot/discord/bot/discord_bot.php
|
||||
environment=APP_ENVIRONMENT="reod"
|
||||
directory=/var/www/html/bot/
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/www/html/bot/logs/discordbot.err.log
|
||||
stdout_logfile=/var/www/html/bot/logs/discordbot.out.log
|
||||
user=www-data
|
||||
redirect_stderr=false
|
||||
13
config/discordbot_updated.conf
Executable file
@@ -0,0 +1,13 @@
|
||||
# Configuración actualizada de Supervisor para Discord Bot
|
||||
# Usar: sudo cp config/discordbot_updated.conf /etc/supervisor/conf.d/discordbot.conf
|
||||
|
||||
[program:discordbot]
|
||||
command=/usr/bin/php /var/www/html/bot/discord/bot/discord_bot.php
|
||||
environment=APP_ENVIRONMENT="reod"
|
||||
directory=/var/www/html/bot/
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/www/html/bot/logs/discordbot.err.log
|
||||
stdout_logfile=/var/www/html/bot/logs/discordbot.out.log
|
||||
user=www-data
|
||||
redirect_stderr=false
|
||||
11
config/translation-worker.conf
Executable file
@@ -0,0 +1,11 @@
|
||||
[program:translation-worker]
|
||||
command=/usr/bin/php /var/www/html/bot/process_translation_queue.php
|
||||
directory=/var/www/html/bot/
|
||||
user=www-data
|
||||
autostart=true
|
||||
autorestart=true
|
||||
environment=APP_ENVIRONMENT="pruebas"
|
||||
stdout_logfile=/var/www/html/bot/logs/translation-worker.out.log
|
||||
stderr_logfile=/var/www/html/bot/logs/translation-worker.err.log
|
||||
redirect_stderr=true
|
||||
numprocs=1
|
||||
51
configure_webhook.php
Executable file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
// Configuración
|
||||
$botToken = '8469229183:AAEVIV5e7rjDXKNgFTX0dnCW6JWB88X4p2I';
|
||||
$webhookToken = 'webhook_secure_token_12345';
|
||||
$domain = 'pruebaspons.duckdns.org';
|
||||
$webhookUrl = "https://{$domain}/bot/telegram_bot_webhook.php?auth_token=" . urlencode($webhookToken);
|
||||
|
||||
// 1. Eliminar webhook existente
|
||||
$apiUrl = "https://api.telegram.org/bot{$botToken}/deleteWebhook";
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true]);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
echo "Webhook eliminado. Configurando nuevo webhook...\n";
|
||||
|
||||
// 2. Configurar nuevo webhook
|
||||
$apiUrl = "https://api.telegram.org/bot{$botToken}/setWebhook";
|
||||
$postData = [
|
||||
'url' => $webhookUrl,
|
||||
'max_connections' => 40,
|
||||
'allowed_updates' => json_encode(['message', 'callback_query'])
|
||||
];
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData,
|
||||
CURLOPT_TIMEOUT => 10
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$result = json_decode($response, true);
|
||||
curl_close($ch);
|
||||
|
||||
// Mostrar resultados
|
||||
echo "Respuesta (HTTP $httpCode): ";
|
||||
echo ($result['ok'] ?? false) ? "✅ Éxito" : "❌ Error: " . ($result['description'] ?? 'Desconocido');
|
||||
echo "\nURL: " . ($result['result']['url'] ?? 'N/A') . "\n";
|
||||
|
||||
// 3. Verificar configuración actual
|
||||
$apiUrl = "https://api.telegram.org/bot{$botToken}/getWebhookInfo";
|
||||
$result = json_decode(file_get_contents($apiUrl), true);
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
echo "\nEstado actual del webhook:\n";
|
||||
echo "URL: " . ($result['result']['url'] ?? 'No configurado') . "\n";
|
||||
echo "Errores recientes: " . ($result['result']['last_error_message'] ?? 'Ninguno') . "\n";
|
||||
}
|
||||
621
create_message.php
Executable file
@@ -0,0 +1,621 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
|
||||
// Fetch all recipients and group by platform
|
||||
$recipients_by_platform = ['discord' => ['channels' => [], 'users' => []], 'telegram' => ['channels' => [], 'users' => []]];
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT id, name, type, platform FROM recipients ORDER BY platform, type, name ASC");
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
if (isset($recipients_by_platform[$row['platform']])) {
|
||||
if ($row['type'] === 'channel') {
|
||||
$recipients_by_platform[$row['platform']]['channels'][] = $row;
|
||||
} else {
|
||||
$recipients_by_platform[$row['platform']]['users'][] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Error: No se pudieron cargar los destinatarios: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Fetch recurrent message templates
|
||||
$templates = [];
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT id, name, message_content FROM recurrent_messages ORDER BY name ASC");
|
||||
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
die("Error: No se pudieron cargar las plantillas de mensajes recurrentes.");
|
||||
}
|
||||
|
||||
// Initialize variables
|
||||
$isEditing = false;
|
||||
$scheduleId = null;
|
||||
$pageTitle = 'Crear Notificación';
|
||||
$submitButtonText = 'Guardar y Enviar';
|
||||
$submitButtonIcon = 'bi-send-fill';
|
||||
$submitAction = 'send';
|
||||
|
||||
$platform = 'discord'; // Default platform
|
||||
$reuseContent = '';
|
||||
$recipientId = '';
|
||||
$recipientType = 'channel';
|
||||
$scheduleType = 'now';
|
||||
$recurringDays = [];
|
||||
// Set default times in Mexico City timezone
|
||||
$timezone = new DateTimeZone('America/Mexico_City');
|
||||
$now = new DateTime('now', $timezone);
|
||||
$recurringTime = $now->format('H:i');
|
||||
$scheduleDateTime = $now->format('Y-m-d\TH:i');
|
||||
|
||||
// Check for reuse action from sent_messages.php
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'reuse') {
|
||||
$reuseContent = $_POST['messageContent'] ?? '';
|
||||
$recipientId = $_POST['recipientId'] ?? '';
|
||||
$recipientType = $_POST['recipientType'] ?? 'channel';
|
||||
// Set schedule to default for a new message
|
||||
$scheduleType = 'now';
|
||||
// Set default schedule time to current time in Mexico City timezone
|
||||
$timezone = new DateTimeZone('America/Mexico_City');
|
||||
$now = new DateTime('now', $timezone);
|
||||
$scheduleDateTime = $now->format('Y-m-d\TH:i');
|
||||
$recurringDays = [];
|
||||
$recurringTime = $now->format('H:i');
|
||||
}
|
||||
|
||||
// Check for edit action
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'edit' && isset($_GET['schedule_id'])) {
|
||||
$isEditing = true;
|
||||
$scheduleId = $_GET['schedule_id'];
|
||||
$pageTitle = 'Editar Notificación';
|
||||
$submitButtonText = 'Actualizar Mensaje';
|
||||
$submitButtonIcon = 'bi-pencil-square';
|
||||
$submitAction = 'update';
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT
|
||||
s.recipient_id,
|
||||
s.send_time,
|
||||
s.is_recurring,
|
||||
s.recurring_days,
|
||||
s.recurring_time,
|
||||
m.content,
|
||||
r.type as recipient_type,
|
||||
r.platform
|
||||
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]);
|
||||
$message_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($message_data) {
|
||||
$reuseContent = $message_data['content'];
|
||||
$recipientId = $message_data['recipient_id'];
|
||||
$recipientType = $message_data['recipient_type'];
|
||||
$platform = $message_data['platform'];
|
||||
|
||||
if ($message_data['is_recurring']) {
|
||||
$scheduleType = 'recurring';
|
||||
$recurringDays = !empty($message_data['recurring_days']) ? explode(',', $message_data['recurring_days']) : [];
|
||||
$recurringTime = substr($message_data['recurring_time'], 0, 5);
|
||||
} else {
|
||||
$scheduleType = 'later';
|
||||
if ($message_data['send_time']) {
|
||||
$scheduleDateTime = date('Y-m-d\TH:i', strtotime($message_data['send_time']));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
die("Error: Mensaje programado no encontrado.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
|
||||
|
||||
?>
|
||||
|
||||
<!-- Summernote CSS -->
|
||||
<link href="<?php echo asset('css/summernote-bs5.min.css'); ?>" rel="stylesheet">
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 data-translate="true" class="mt-4" ><?php echo $pageTitle; ?></h1>
|
||||
|
||||
<form action="includes/message_handler.php" method="POST" id="createMessageForm">
|
||||
<?php if ($isEditing): ?>
|
||||
<input type="hidden" name="schedule_id" value="<?php echo htmlspecialchars($scheduleId); ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Template Selection -->
|
||||
<div class="mb-3">
|
||||
<label for="templateSelector" class="form-label" data-translate="true">Cargar desde Plantilla</label>
|
||||
<select class="form-select" id="templateSelector">
|
||||
<option value="" data-translate="true">-- Selecciona una plantilla --</option>
|
||||
<?php foreach ($templates as $template): ?>
|
||||
<option value="<?php echo $template['id']; ?>" data-content="<?php echo htmlspecialchars($template['message_content']); ?>">
|
||||
<?php echo htmlspecialchars($template['name']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Message Content -->
|
||||
<div class="mb-3">
|
||||
<label data-translate="true" for="messageContent" class="form-label">Contenido del Mensaje</label>
|
||||
<textarea id="messageContent" name="messageContent" class="form-control"><?php echo htmlspecialchars($reuseContent); ?></textarea>
|
||||
<div id="characterCount" class="text-muted text-end mt-1">0 caracteres</div>
|
||||
</div>
|
||||
|
||||
<!-- Translation Option -->
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="enableTranslation" name="enableTranslation" value="true" checked>
|
||||
<label class="form-check-label" for="enableTranslation" data-translate="true">Habilitar traducción automática (Español/Portugués)</label>
|
||||
</div>
|
||||
|
||||
<!-- Platform Selection -->
|
||||
<div class="mb-3">
|
||||
<label data-translate="true" class="form-label">Plataforma</label>
|
||||
<div>
|
||||
<input type="radio" id="platformDiscord" name="platform" value="discord" <?php echo ($platform === 'discord') ? 'checked' : ''; ?>>
|
||||
<label for="platformDiscord" class="me-3" data-translate="true">Discord</label>
|
||||
<input type="radio" id="platformTelegram" name="platform" value="telegram" <?php echo ($platform === 'telegram') ? 'checked' : ''; ?>>
|
||||
<label for="platformTelegram" data-translate="true">Telegram</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recipient Selection -->
|
||||
<div class="mb-3">
|
||||
<label data-translate="true" class="form-label">Tipo de Destinatario</label>
|
||||
<div>
|
||||
<input type="radio" id="recipientTypeChannel" name="recipientType" value="channel" <?php echo $recipientType === 'channel' ? 'checked' : ''; ?>>
|
||||
<label for="recipientTypeChannel" class="me-3" data-translate="true">Canal</label>
|
||||
<input type="radio" id="recipientTypeUser" name="recipientType" value="user" <?php echo $recipientType === 'user' ? 'checked' : ''; ?>>
|
||||
<label for="recipientTypeUser" data-translate="true">Usuario</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel/User Selection -->
|
||||
<div class="mb-3">
|
||||
<label data-translate="true" for="recipientId" class="form-label">Seleccionar Destinatario</label>
|
||||
<div id="discord-recipients">
|
||||
<select class="form-select" id="recipientId_discord_channel" name="recipientId_discord_channel">
|
||||
<option value="" data-translate="true">Selecciona un canal de Discord</option>
|
||||
<?php foreach ($recipients_by_platform['discord']['channels'] as $channel): ?>
|
||||
<option value="<?php echo $channel['id']; ?>"><?php echo htmlspecialchars($channel['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select class="form-select d-none" id="recipientId_discord_user" name="recipientId_discord_user[]" multiple>
|
||||
<option value="" data-translate="true">Selecciona un usuario de Discord</option>
|
||||
<?php foreach ($recipients_by_platform['discord']['users'] as $user): ?>
|
||||
<option value="<?php echo $user['id']; ?>"><?php echo htmlspecialchars($user['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div id="telegram-recipients" class="d-none">
|
||||
<select class="form-select" id="recipientId_telegram_channel" name="recipientId_telegram_channel">
|
||||
<option value="" data-translate="true">Selecciona un canal de Telegram</option>
|
||||
<?php foreach ($recipients_by_platform['telegram']['channels'] as $channel): ?>
|
||||
<option value="<?php echo $channel['id']; ?>"><?php echo htmlspecialchars($channel['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select class="form-select d-none" id="recipientId_telegram_user" name="recipientId_telegram_user[]" multiple>
|
||||
<option value="" data-translate="true" data-translate="true">Selecciona un usuario de Telegram</option>
|
||||
<?php foreach ($recipients_by_platform['telegram']['users'] as $user): ?>
|
||||
<option value="<?php echo $user['id']; ?>"><?php echo htmlspecialchars($user['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Options -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label" data-translate="true">Programación</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="scheduleType" id="scheduleNow" value="now" <?php echo $scheduleType === 'now' ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="scheduleNow">
|
||||
<span data-translate="true">Enviar ahora</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="scheduleType" id="scheduleLater" value="later" <?php echo $scheduleType === 'later' ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="scheduleLater">
|
||||
<span data-translate="true">Programar para más tarde</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="scheduleType" id="scheduleRecurring" value="recurring" <?php echo $scheduleType === 'recurring' ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="scheduleRecurring">
|
||||
<span data-translate="true">Programación recurrente</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Later Options -->
|
||||
<div class="mb-3" id="scheduleLaterOptions" style="display: <?php echo $scheduleType === 'later' ? 'block' : 'none'; ?>;">
|
||||
<label for="scheduleDateTime" class="form-label" data-translate="true">Fecha y Hora de Envío</label>
|
||||
<input type="datetime-local" class="form-control" id="scheduleDateTime" name="scheduleDateTime"
|
||||
min="<?php echo date('Y-m-d\TH:i'); ?>"
|
||||
value="<?php echo $scheduleDateTime; ?>">
|
||||
</div>
|
||||
|
||||
<!-- Recurring Schedule Options -->
|
||||
<div class="mb-3" id="recurringOptions" style="display: <?php echo $scheduleType === 'recurring' ? 'block' : 'none'; ?>;">
|
||||
<div class="mb-2">
|
||||
<label class="form-label" data-translate="true">Días de la Semana</label>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<?php
|
||||
$days = [
|
||||
['value' => '1', 'label' => '<span data-translate="true">Lun</span>'],
|
||||
['value' => '2', 'label' => '<span data-translate="true">Mar</span>'],
|
||||
['value' => '3', 'label' => '<span data-translate="true">Mié</span>'],
|
||||
['value' => '4', 'label' => '<span data-translate="true">Jue</span>'],
|
||||
['value' => '5', 'label' => '<span data-translate="true">Vie</span>'],
|
||||
['value' => '6', 'label' => '<span data-translate="true">Sáb</span>'],
|
||||
['value' => '0', 'label' => '<span data-translate="true">Dom</span>']
|
||||
];
|
||||
foreach ($days as $day):
|
||||
$isChecked = in_array($day['value'], $recurringDays) ? 'checked' : '';
|
||||
?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input day-checkbox" type="checkbox"
|
||||
name="recurringDays[]" value="<?php echo $day['value']; ?>"
|
||||
id="day<?php echo $day['value']; ?>" <?php echo $isChecked; ?>>
|
||||
<label class="form-check-label" for="day<?php echo $day['value']; ?>">
|
||||
<?php echo $day['label']; ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="recurringTime" class="form-label" data-translate="true">Hora de Envío (cada día seleccionado)</label>
|
||||
<input type="time" class="form-control" id="recurringTime" name="recurringTime"
|
||||
value="<?php echo $recurringTime; ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button type="submit" value="<?php echo $submitAction; ?>" class="btn btn-success">
|
||||
<i class="bi <?php echo $submitButtonIcon; ?> me-1"></i> <?php echo $submitButtonText; ?>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
|
||||
<!-- Gallery Modal -->
|
||||
<div class="modal fade" id="galleryModal" tabindex="-1" aria-labelledby="galleryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="galleryModalLabel" data-translate="true">Galería de Imágenes</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<?php
|
||||
$gallery_path = __DIR__ . '/galeria';
|
||||
$files = array_diff(scandir($gallery_path), array('.', '..'));
|
||||
if (empty($files)) {
|
||||
echo '<p class="text-center text-muted" data-translate="true">No hay imágenes en la galería.</p>';
|
||||
} else {
|
||||
foreach ($files as $file) {
|
||||
if (is_file($gallery_path . '/' . $file)) {
|
||||
echo '<div class="col-lg-3 col-md-4 col-sm-6 mb-4 text-center"><img src="galeria/' . htmlspecialchars($file) . '" class="img-fluid img-thumbnail gallery-item" style="cursor:pointer;" alt="' . htmlspecialchars($file) . '"><p class="small text-muted mt-1">' . htmlspecialchars($file) . '</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
||||
<button type="button" class="btn btn-primary" id="insertImageFromGallery">Insertar Imagen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Custom Gallery Button
|
||||
var GalleryButton = function (context) {
|
||||
var ui = $.summernote.ui;
|
||||
var button = ui.button({
|
||||
contents: '<i class="bi bi-images"></i> Galería',
|
||||
tooltip: 'Insertar imagen desde la galería',
|
||||
click: function () {
|
||||
$('#galleryModal').modal('show');
|
||||
}
|
||||
});
|
||||
return button.render();
|
||||
}
|
||||
|
||||
// Initialize Summernote
|
||||
$('#messageContent').summernote({
|
||||
placeholder: 'Escribe tu mensaje aquí...',
|
||||
tabsize: 2,
|
||||
height: 300,
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'underline', 'clear']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', ['picture']],
|
||||
['view', ['codeview']],
|
||||
['mybutton', ['gallery']]
|
||||
],
|
||||
buttons: {
|
||||
gallery: GalleryButton
|
||||
},
|
||||
popover: {
|
||||
image: [
|
||||
['image', ['resizeFull', 'resizeHalf', 'resizeQuarter', 'resizeNone']],
|
||||
['float', ['floatLeft', 'floatRight', 'floatNone']],
|
||||
['remove', ['removeMedia']]
|
||||
],
|
||||
link: [
|
||||
['link', ['linkDialogShow', 'unlink']]
|
||||
],
|
||||
table: [
|
||||
['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
|
||||
['delete', ['deleteRow', 'deleteCol', 'deleteTable']],
|
||||
],
|
||||
air: [
|
||||
['color', ['color']],
|
||||
['font', ['bold', 'underline', 'clear']],
|
||||
['para', ['ul', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', ['link', 'picture']]
|
||||
]
|
||||
},
|
||||
callbacks: {
|
||||
onImageUpload: function(files) {
|
||||
var editor = $(this);
|
||||
var data = new FormData();
|
||||
data.append("file", files[0]);
|
||||
$.ajax({
|
||||
url: 'upload_editor_image.php',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.url) {
|
||||
editor.summernote('insertImage', response.url);
|
||||
} else {
|
||||
alert(response.error || 'Error al subir la imagen.');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error: No se pudo comunicar con el servidor para subir la imagen.');
|
||||
}
|
||||
});
|
||||
},
|
||||
onPaste: function(e) {
|
||||
var bufferText = ((e.originalEvent || e).clipboardData || window.clipboardData).getData('Text');
|
||||
e.preventDefault();
|
||||
// Firefox fix
|
||||
setTimeout(function () {
|
||||
document.execCommand('insertText', false, bufferText);
|
||||
}, 10);
|
||||
},
|
||||
onKeyup: function(e) {
|
||||
updateCharacterCount();
|
||||
},
|
||||
onInit: function() {
|
||||
updateCharacterCount();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle template selection
|
||||
$('#templateSelector').change(function() {
|
||||
var selectedOption = $(this).find('option:selected');
|
||||
var content = selectedOption.data('content');
|
||||
if (content) {
|
||||
$('#messageContent').summernote('code', content);
|
||||
} else {
|
||||
$('#messageContent').summernote('code', ''); // Clear if no content
|
||||
}
|
||||
updateCharacterCount(); // Update count after template load
|
||||
});
|
||||
|
||||
// Function to update character count
|
||||
function updateCharacterCount() {
|
||||
// Get the text directly from the editable area of Summernote
|
||||
var text = $('#messageContent').next('.note-editor').find('.note-editable').text();
|
||||
var count = text.length;
|
||||
console.log("Summernote text (from note-editable):", text);
|
||||
console.log("Character count:", count);
|
||||
$('#characterCount').text(count + ' caracteres');
|
||||
}
|
||||
|
||||
// Handle image selection in modal
|
||||
$(document).on('click', '.gallery-item', function() {
|
||||
$(this).toggleClass('border-primary');
|
||||
});
|
||||
|
||||
// Handle image insertion
|
||||
$('#insertImageFromGallery').click(function() {
|
||||
$('.gallery-item.border-primary').each(function(){
|
||||
var imageUrl = $(this).attr('src');
|
||||
$('#messageContent').summernote('insertImage', imageUrl);
|
||||
});
|
||||
$('#galleryModal').modal('hide');
|
||||
$('.gallery-item').removeClass('border-primary');
|
||||
});
|
||||
|
||||
// Toggle between channel and user selection
|
||||
const recipientTypeChannel = document.getElementById('recipientTypeChannel');
|
||||
const recipientTypeUser = document.getElementById('recipientTypeUser');
|
||||
const platformDiscord = document.getElementById('platformDiscord');
|
||||
const platformTelegram = document.getElementById('platformTelegram');
|
||||
|
||||
const discordRecipients = document.getElementById('discord-recipients');
|
||||
const telegramRecipients = document.getElementById('telegram-recipients');
|
||||
|
||||
const discordChannelSelect = document.getElementById('recipientId_discord_channel');
|
||||
const discordUserSelect = document.getElementById('recipientId_discord_user');
|
||||
const telegramChannelSelect = document.getElementById('recipientId_telegram_channel');
|
||||
const telegramUserSelect = document.getElementById('recipientId_telegram_user');
|
||||
|
||||
function updateRecipientVisibility() {
|
||||
const isDiscord = platformDiscord.checked;
|
||||
const isTelegram = platformTelegram.checked;
|
||||
const isChannel = recipientTypeChannel.checked;
|
||||
const isUser = recipientTypeUser.checked;
|
||||
|
||||
// Show/hide platform containers
|
||||
discordRecipients.classList.toggle('d-none', !isDiscord);
|
||||
telegramRecipients.classList.toggle('d-none', !isTelegram);
|
||||
|
||||
if (isDiscord) {
|
||||
// Handle Discord recipients
|
||||
discordChannelSelect.classList.toggle('d-none', !isChannel);
|
||||
discordChannelSelect.disabled = !isChannel;
|
||||
discordChannelSelect.required = isChannel;
|
||||
|
||||
discordUserSelect.classList.toggle('d-none', !isUser);
|
||||
discordUserSelect.disabled = !isUser;
|
||||
discordUserSelect.required = isUser;
|
||||
|
||||
// Disable Telegram selects
|
||||
telegramChannelSelect.disabled = true;
|
||||
telegramUserSelect.disabled = true;
|
||||
} else if (isTelegram) {
|
||||
// Handle Telegram recipients
|
||||
telegramChannelSelect.classList.toggle('d-none', !isChannel);
|
||||
telegramChannelSelect.disabled = !isChannel;
|
||||
telegramChannelSelect.required = isChannel;
|
||||
|
||||
telegramUserSelect.classList.toggle('d-none', !isUser);
|
||||
telegramUserSelect.disabled = !isUser;
|
||||
telegramUserSelect.required = isUser;
|
||||
|
||||
// Disable Discord selects
|
||||
discordChannelSelect.disabled = true;
|
||||
discordUserSelect.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
platformDiscord.addEventListener('change', updateRecipientVisibility);
|
||||
platformTelegram.addEventListener('change', updateRecipientVisibility);
|
||||
recipientTypeChannel.addEventListener('change', updateRecipientVisibility);
|
||||
recipientTypeUser.addEventListener('change', updateRecipientVisibility);
|
||||
|
||||
// Initial call to set the correct state
|
||||
updateRecipientVisibility();
|
||||
|
||||
// Toggle schedule options
|
||||
const scheduleNow = document.getElementById('scheduleNow');
|
||||
const scheduleLater = document.getElementById('scheduleLater');
|
||||
const scheduleRecurring = document.getElementById('scheduleRecurring');
|
||||
const scheduleLaterOptions = document.getElementById('scheduleLaterOptions');
|
||||
const recurringOptions = document.getElementById('recurringOptions');
|
||||
|
||||
function updateScheduleOptions() {
|
||||
const scheduleLaterOptions = document.getElementById('scheduleLaterOptions');
|
||||
const scheduleDateTime = document.getElementById('scheduleDateTime');
|
||||
const recurringOptions = document.getElementById('recurringOptions');
|
||||
const recurringTime = document.getElementById('recurringTime');
|
||||
const recurringDays = document.querySelectorAll('.day-checkbox');
|
||||
|
||||
if (scheduleNow.checked) {
|
||||
scheduleLaterOptions.style.display = 'none';
|
||||
scheduleDateTime.disabled = true;
|
||||
recurringOptions.style.display = 'none';
|
||||
recurringTime.disabled = true;
|
||||
recurringDays.forEach(day => day.disabled = true);
|
||||
} else if (scheduleLater.checked) {
|
||||
scheduleLaterOptions.style.display = 'block';
|
||||
scheduleDateTime.disabled = false;
|
||||
recurringOptions.style.display = 'none';
|
||||
recurringTime.disabled = true;
|
||||
recurringDays.forEach(day => day.disabled = true);
|
||||
} else if (scheduleRecurring.checked) {
|
||||
scheduleLaterOptions.style.display = 'none';
|
||||
scheduleDateTime.disabled = true;
|
||||
recurringOptions.style.display = 'block';
|
||||
recurringTime.disabled = false;
|
||||
recurringDays.forEach(day => day.disabled = false);
|
||||
}
|
||||
}
|
||||
|
||||
if (scheduleNow && scheduleLater && scheduleRecurring) {
|
||||
scheduleNow.addEventListener('change', updateScheduleOptions);
|
||||
scheduleLater.addEventListener('change', updateScheduleOptions);
|
||||
scheduleRecurring.addEventListener('change', updateScheduleOptions);
|
||||
updateScheduleOptions();
|
||||
}
|
||||
|
||||
// Form validation
|
||||
const form = document.getElementById('createMessageForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Prevent default submission to perform validation and data preparation
|
||||
e.preventDefault();
|
||||
|
||||
// 1. Validate Summernote content
|
||||
if ($('#messageContent').summernote('isEmpty')) {
|
||||
alert('El contenido del mensaje no puede estar vacío.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get message content from Summernote
|
||||
let messageContent = $('#messageContent').summernote('code');
|
||||
|
||||
// Check if translation is enabled
|
||||
const enableTranslationCheckbox = document.getElementById('enableTranslation');
|
||||
if (enableTranslationCheckbox && enableTranslationCheckbox.checked) {
|
||||
// Add data-translate attribute to the message content
|
||||
// This will be picked up by process_queue.php
|
||||
messageContent = '<div data-translate="true">' + messageContent + '</div>';
|
||||
}
|
||||
|
||||
// Add hidden field for messageContent
|
||||
$('<input>').attr({ type: 'hidden', name: 'messageContent', value: messageContent }).appendTo(form);
|
||||
|
||||
let channelIds = [];
|
||||
let userIds = [];
|
||||
|
||||
const isDiscord = platformDiscord.checked;
|
||||
const isTelegram = platformTelegram.checked;
|
||||
const isChannel = recipientTypeChannel.checked;
|
||||
const isUser = recipientTypeUser.checked;
|
||||
|
||||
if (isDiscord) {
|
||||
if (isChannel) {
|
||||
channelIds.push(discordChannelSelect.value);
|
||||
} else if (isUser) {
|
||||
userIds = $(discordUserSelect).val() || [];
|
||||
}
|
||||
} else if (isTelegram) {
|
||||
if (isChannel) {
|
||||
channelIds.push(telegramChannelSelect.value);
|
||||
} else if (isUser) {
|
||||
userIds = $(telegramUserSelect).val() || [];
|
||||
}
|
||||
}
|
||||
|
||||
if (channelIds.length === 0 && userIds.length === 0) {
|
||||
alert('Por favor selecciona al menos un destinatario.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add hidden fields to the form
|
||||
$('<input>').attr({ type: 'hidden', name: 'channelIds', value: JSON.stringify(channelIds) }).appendTo(form);
|
||||
$('<input>').attr({ type: 'hidden', name: 'userIds', value: JSON.stringify(userIds) }).appendTo(form);
|
||||
|
||||
// If all validation passes, submit the form
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
8295
db/bot.sql
Executable file
28
delete_image.php
Executable file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/activity_logger.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['filename'])) {
|
||||
$userId = $_SESSION['user_id'] ?? 0;
|
||||
$username = $_SESSION['username'] ?? 'Unknown';
|
||||
|
||||
$filename = basename($_POST['filename']); // Sanitize filename
|
||||
$filepath = __DIR__ . '/galeria/' . $filename;
|
||||
|
||||
if (file_exists($filepath) && is_file($filepath)) {
|
||||
if (unlink($filepath)) {
|
||||
log_activity($userId, 'Image Deleted', 'User ' . $username . ' deleted image: ' . $filename);
|
||||
header('Location: gallery.php?success=deleted');
|
||||
} else {
|
||||
log_activity($userId, 'Image Deletion Failed', 'User ' . $username . ' failed to delete image: ' . $filename);
|
||||
header('Location: gallery.php?error=delete_failed');
|
||||
}
|
||||
} else {
|
||||
log_activity($userId, 'Image Deletion Failed', 'User ' . $username . ' attempted to delete non-existent or invalid file: ' . $filename);
|
||||
header('Location: gallery.php?error=invalid_file');
|
||||
}
|
||||
} else {
|
||||
header('Location: gallery.php');
|
||||
}
|
||||
?>
|
||||
266
discord/DiscordSender.php
Executable file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
class DiscordSender
|
||||
{
|
||||
private const API_BASE_URL = 'https://discord.com/api/v10';
|
||||
private const MESSAGE_CHAR_LIMIT = 1990;
|
||||
private const LOG_FILE = __DIR__ . '/../logs/discord_api.log';
|
||||
|
||||
private string $token;
|
||||
|
||||
public function __construct(string $token)
|
||||
{
|
||||
custom_log('[DiscordSender] Initializing...');
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function sendMessage(string $discordId, string $content, string $recipientType = 'channel') {
|
||||
custom_log("[DiscordSender] sendMessage: Called for ID {$discordId} and recipient type {$recipientType}.");
|
||||
|
||||
try {
|
||||
if (empty(trim($content))) {
|
||||
$this->logMessage("Error: No se puede enviar un mensaje vacío");
|
||||
return false;
|
||||
}
|
||||
|
||||
$targetChannelId = $this->getTargetChannelId($discordId, $recipientType);
|
||||
custom_log("[DiscordSender] sendMessage: Target channel ID is {$targetChannelId}.");
|
||||
|
||||
$parts = [];
|
||||
|
||||
preg_match_all('/<img[^>]+src=[\'"]([^\'"]+)[\'"][^>]*>/i', $content, $imageMatches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
||||
|
||||
$lastPos = 0;
|
||||
|
||||
foreach ($imageMatches as $match) {
|
||||
$imageTag = $match[0][0];
|
||||
$imageUrl = $match[1][0];
|
||||
$imagePos = $match[0][1];
|
||||
|
||||
$textBefore = trim(substr($content, $lastPos, $imagePos - $lastPos));
|
||||
if (!empty($textBefore)) {
|
||||
$textWithNewlines = str_ireplace(['<p>', '</p>', '<br>', '<br />'], ["", "\n", "\n", "\n"], $textBefore);
|
||||
$text = trim(strip_tags($textWithNewlines));
|
||||
if (!empty($text)) {
|
||||
$parts[] = ['type' => 'text', 'content' => $text];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($imageUrl)) {
|
||||
$absoluteImageUrl = $imageUrl;
|
||||
if (strpos($imageUrl, 'http') !== 0 && strpos($imageUrl, '//') !== 0) {
|
||||
$base = rtrim(BOT_BASE_URL, '/');
|
||||
$path = ltrim($imageUrl, '/');
|
||||
$absoluteImageUrl = "{$base}/{$path}";
|
||||
}
|
||||
$parts[] = ['type' => 'image', 'url' => $absoluteImageUrl];
|
||||
}
|
||||
|
||||
$lastPos = $imagePos + strlen($imageTag);
|
||||
}
|
||||
|
||||
$textAfter = trim(substr($content, $lastPos));
|
||||
if (!empty($textAfter)) {
|
||||
$textWithNewlines = str_ireplace(['<p>', '</p>', '<br>', '<br />'], ["", "\n", "\n", "\n"], $textAfter);
|
||||
$text = trim(strip_tags($textWithNewlines));
|
||||
if (!empty($text)) {
|
||||
$parts[] = ['type' => 'text', 'content' => $text];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($parts)) {
|
||||
$textWithNewlines = str_ireplace(['<p>', '</p>', '<br>', '<br />'], ["", "\n", "\n", "\n"], $content);
|
||||
$text = trim(strip_tags($textWithNewlines));
|
||||
if (!empty($text)) {
|
||||
$parts[] = ['type' => 'text', 'content' => $text];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($parts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$messageIds = [];
|
||||
$allPartsSentSuccessfully = true;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if ($part['type'] === 'text') {
|
||||
$chunks = $this->splitMessage($part['content']);
|
||||
foreach ($chunks as $chunk) {
|
||||
$trimmedChunk = trim($chunk);
|
||||
if ($trimmedChunk === '') continue;
|
||||
|
||||
try {
|
||||
$response = $this->sendDiscordMessage($targetChannelId, ['content' => $trimmedChunk]);
|
||||
if (isset($response['id'])) {
|
||||
$messageIds[] = $response['id'];
|
||||
} else {
|
||||
$allPartsSentSuccessfully = false; break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error al enviar texto: " . $e->getMessage());
|
||||
$allPartsSentSuccessfully = false; break;
|
||||
}
|
||||
usleep(250000);
|
||||
}
|
||||
} elseif ($part['type'] === 'image') {
|
||||
try {
|
||||
$response = $this->sendDiscordMessage($targetChannelId, ['content' => $part['url']]);
|
||||
if (isset($response['id'])) {
|
||||
$messageIds[] = $response['id'];
|
||||
} else {
|
||||
$allPartsSentSuccessfully = false; break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error al enviar imagen como URL: " . $e->getMessage());
|
||||
$allPartsSentSuccessfully = false; break;
|
||||
}
|
||||
}
|
||||
if (!$allPartsSentSuccessfully) break;
|
||||
usleep(500000);
|
||||
}
|
||||
|
||||
return $allPartsSentSuccessfully ? $messageIds : false;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error in sendMessage: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function getTargetChannelId(string $discordId, string $recipientType): string {
|
||||
if ($recipientType === 'user') {
|
||||
return $this->createDMChannel($discordId);
|
||||
}
|
||||
return $discordId;
|
||||
}
|
||||
|
||||
private function createDMChannel(string $userId): string {
|
||||
$url = self::API_BASE_URL . '/users/@me/channels';
|
||||
$data = json_encode(['recipient_id' => $userId]);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bot ' . $this->token,
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($data)
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $data
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
throw new Exception('cURL error: ' . curl_error($ch));
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("Failed to create DM channel. HTTP code: $httpCode, Response: $response");
|
||||
}
|
||||
|
||||
$responseData = json_decode($response, true);
|
||||
return $responseData['id'];
|
||||
}
|
||||
|
||||
private function sendDiscordMessage(string $channelId, array $payload, array $files = []) {
|
||||
$url = self::API_BASE_URL . "/channels/{$channelId}/messages";
|
||||
|
||||
if (isset($payload['content'])) {
|
||||
$payload['content'] = trim($payload['content']);
|
||||
if ($payload['content'] === '') unset($payload['content']);
|
||||
}
|
||||
|
||||
if (empty($payload['content'] ?? '') && empty($payload['embeds'] ?? '') && empty($files)) {
|
||||
throw new Exception("No se puede enviar un mensaje vacío");
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
$headers = ['Authorization: Bot ' . $this->token, 'User-Agent: DiscordBot (v1.0)'] ;
|
||||
|
||||
if (empty($files)) {
|
||||
$headers[] = 'Content-Type: application/json';
|
||||
$postData = json_encode($payload);
|
||||
} else {
|
||||
// Multipart logic for files would go here if needed
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$responseData = json_decode($response, true);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
throw new Exception("Discord API error ({$httpCode}): " . ($responseData['message'] ?? 'Unknown error'));
|
||||
}
|
||||
|
||||
return $responseData;
|
||||
}
|
||||
|
||||
private function splitMessage(string $text, int $maxLength = self::MESSAGE_CHAR_LIMIT): array
|
||||
{
|
||||
$chunks = [];
|
||||
$text = str_replace(["\r\n", "\r"], "\n", $text);
|
||||
$lines = explode("\n", $text);
|
||||
$currentChunk = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (mb_strlen($currentChunk, 'UTF-8') + mb_strlen($line, 'UTF-8') + 1 > $maxLength) {
|
||||
$chunks[] = $currentChunk;
|
||||
$currentChunk = $line;
|
||||
} else {
|
||||
$currentChunk .= (empty($currentChunk) ? '' : "\n") . $line;
|
||||
}
|
||||
}
|
||||
if (!empty($currentChunk)) $chunks[] = $currentChunk;
|
||||
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
private function logMessage(string $message): void {
|
||||
$logMessage = date('[Y-m-d H:i:s] ') . $message . "\n";
|
||||
file_put_contents(self::LOG_FILE, $logMessage, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
public function sendRawMessage(string $channelId, string $content): ?array
|
||||
{
|
||||
custom_log("[DiscordSender] sendRawMessage: Called for channel ID {$channelId}.");
|
||||
try {
|
||||
return $this->sendDiscordMessage($channelId, ['content' => $content]);
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error in sendRawMessage: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía un Embed (construido como un array) a un canal de Discord.
|
||||
*
|
||||
* @param string $channelId El ID del canal de destino.
|
||||
* @param array $embedData El array que representa el embed.
|
||||
* @return array|null La respuesta de la API de Discord o null si hay un error.
|
||||
*/
|
||||
public function sendEmbedData(string $channelId, array $embedData): ?array
|
||||
{
|
||||
custom_log("[DiscordSender] sendEmbedData: Called for channel ID {$channelId}.");
|
||||
try {
|
||||
return $this->sendDiscordMessage($channelId, ['embeds' => [$embedData]]);
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage("Error in sendEmbedData: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
discord/actions/discord_actions.php
Executable file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/session_check.php';
|
||||
require_once __DIR__ . '/../../includes/db.php';
|
||||
require_once __DIR__ . '/../DiscordSender.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['action'])) {
|
||||
header('Location: ../sent_messages.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'delete_message') {
|
||||
if (!isset($_POST['sent_message_id'], $_POST['platform_message_id'], $_POST['channel_id'])) {
|
||||
header('Location: ../sent_messages.php?error=missing_data');
|
||||
exit();
|
||||
}
|
||||
|
||||
$sentMessageId = $_POST['sent_message_id'];
|
||||
$discordMessageIdsJson = $_POST['platform_message_id'];
|
||||
$channelId = $_POST['channel_id']; // The channel where the message was sent
|
||||
|
||||
$discordMessageIds = json_decode($discordMessageIdsJson, true);
|
||||
|
||||
// If decoding fails or the result is not an array, treat it as a single ID
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($discordMessageIds)) {
|
||||
$discordMessageIds = [$discordMessageIdsJson];
|
||||
}
|
||||
|
||||
$discordSender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
|
||||
try {
|
||||
// 1. Attempt to delete from Discord
|
||||
$all_deleted = true;
|
||||
foreach ($discordMessageIds as $discordMessageId) {
|
||||
// Skip if the ID is empty or invalid
|
||||
if (empty($discordMessageId) || !is_numeric($discordMessageId)) {
|
||||
error_log("Skipping invalid message chunk ID: " . var_export($discordMessageId, true));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
error_log("Attempting to delete message chunk ID: {$discordMessageId} in channel {$channelId}");
|
||||
$discordSender->deleteMessage($channelId, $discordMessageId);
|
||||
usleep(500000); // Wait 500ms to avoid rate limiting
|
||||
} catch (Exception $e) {
|
||||
error_log("Failed to delete message chunk {$discordMessageId}: " . $e->getMessage());
|
||||
$all_deleted = false; // Mark that at least one failed
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If all chunks were deleted (or if there was only one), delete from our database
|
||||
if ($all_deleted) {
|
||||
$stmt = $pdo->prepare("DELETE FROM sent_messages WHERE id = ?");
|
||||
$stmt->execute([$sentMessageId]);
|
||||
header('Location: ../sent_messages.php?success=deleted&platform=Discord');
|
||||
} else {
|
||||
// If some failed, we don't delete the entry, so it can be retried.
|
||||
// We could also add more sophisticated logic here, like storing partial success.
|
||||
header('Location: ../sent_messages.php?error=delete_failed_partial&platform=Discord');
|
||||
}
|
||||
exit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Discord message deletion failed: " . $e->getMessage());
|
||||
header('Location: ../sent_messages.php?error=delete_failed&platform=Discord&message=' . urlencode($e->getMessage()));
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback redirect
|
||||
header('Location: ../sent_messages.php');
|
||||
?>
|
||||
209
discord/admin/discord_translation_options.php
Executable file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
// Evitar caché del navegador
|
||||
header("Cache-Control: no-cache, must-revalidate");
|
||||
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Opciones de Traducción - Discord Bot</title>
|
||||
<style>
|
||||
:root {
|
||||
--discord-blurple: #5865F2;
|
||||
--discord-green: #57F287;
|
||||
--discord-yellow: #FEE75C;
|
||||
--discord-red: #ED4245;
|
||||
--discord-dark: #2c2f33;
|
||||
--discord-gray: #99aab5;
|
||||
--discord-light-gray: #f2f3f5;
|
||||
--discord-chat-bg: #36393f;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'gg sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background: var(--discord-dark);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
}
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { text-align: center; color: white; margin-bottom: 40px; }
|
||||
.header h1 { font-size: 2.5rem; margin-bottom: 10px; }
|
||||
.header p { font-size: 1.2rem; color: var(--discord-gray); }
|
||||
.options-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 25px; margin-bottom: 30px; }
|
||||
.option-card { background: var(--discord-chat-bg); border-radius: 8px; padding: 25px; box-shadow: 0 8px 20px rgba(0,0,0,0.3); transition: transform 0.3s ease; }
|
||||
.option-card:hover { transform: translateY(-5px); }
|
||||
.option-header { display: flex; align-items: center; gap: 15px; margin-bottom: 15px; }
|
||||
.option-icon { font-size: 28px; }
|
||||
.option-title { font-size: 1.5rem; font-weight: bold; color: white; }
|
||||
.option-description { color: var(--discord-gray); font-size: 1rem; margin-bottom: 20px; }
|
||||
.demo-area { background: var(--discord-dark); border-radius: 8px; padding: 20px; border: 1px solid #40444b; }
|
||||
.discord-message { display: flex; gap: 15px; }
|
||||
.avatar { width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0; }
|
||||
.message-content { display: flex; flex-direction: column; }
|
||||
.username { font-weight: 600; font-size: 1rem; margin-bottom: 5px; color: var(--discord-green); }
|
||||
.message-text { color: #dcddde; line-height: 1.4; }
|
||||
.discord-button { padding: 10px 16px; border: none; border-radius: 3px; font-weight: 600; cursor: pointer; font-size: 0.9rem; transition: background-color 0.2s; margin-top: 10px; background-color: #4f545c; color: white; }
|
||||
.discord-button:hover { background-color: #5d636b; }
|
||||
.pros-cons { margin-top: 20px; }
|
||||
.pros, .cons { padding: 12px; border-radius: 8px; margin-bottom: 10px; font-size: 0.9rem; }
|
||||
.pros { background: rgba(87, 242, 135, 0.1); border-left: 3px solid var(--discord-green); }
|
||||
.pros strong { color: var(--discord-green); }
|
||||
.cons { background: rgba(254, 231, 92, 0.1); border-left: 3px solid var(--discord-yellow); }
|
||||
.cons strong { color: var(--discord-yellow); }
|
||||
ul { color: var(--discord-gray); margin-left: 20px; margin-top: 5px; }
|
||||
.recommendation { background: var(--discord-blurple); color: white; padding: 30px; border-radius: 8px; text-align: center; }
|
||||
|
||||
/* Modal for Ephemeral Message */
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; align-items: center; justify-content: center; }
|
||||
.modal.active { display: flex; }
|
||||
.modal-content { background: var(--discord-chat-bg); padding: 20px; border-radius: 8px; max-width: 450px; width: 90%; box-shadow: 0 5px 15px rgba(0,0,0,0.5); }
|
||||
.modal-header { color: var(--discord-green); font-weight: bold; margin-bottom: 15px; }
|
||||
.modal-body { color: #dcddde; }
|
||||
.modal-body .translation { margin-bottom: 10px; }
|
||||
.modal-body .translation strong { color: white; }
|
||||
.modal-footer { font-size: 0.8rem; color: var(--discord-gray); margin-top: 15px; text-align: center; }
|
||||
|
||||
/* Spoiler styles */
|
||||
.spoiler { background-color: #202225; color: transparent; border-radius: 3px; cursor: pointer; padding: 0 2px; }
|
||||
.spoiler.revealed { background-color: transparent; color: inherit; }
|
||||
|
||||
/* Thread styles */
|
||||
.thread-link { color: var(--discord-blurple); font-weight: 600; cursor: pointer; font-size: 0.9rem; margin-top: 8px; }
|
||||
.thread-view { display: none; margin-top: 15px; padding-top: 15px; border-top: 1px solid #40444b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><img src="https://discord.com/assets/f8389ca1a741a115313a74fbbd352562.svg" alt="Discord Logo" width="40" style="vertical-align: middle;"> Opciones de Traducción para Discord</h1>
|
||||
<p>Demostración visual de métodos no invasivos para mostrar traducciones.</p>
|
||||
</div>
|
||||
|
||||
<div class="options-grid">
|
||||
<!-- Opción 1: Botón con Respuesta Efímera -->
|
||||
<div class="option-card">
|
||||
<div class="option-header"><span class="option-icon">🤫</span> <h2 class="option-title">Botón Efímero</h2></div>
|
||||
<p class="option-description">Un botón "Traducir" bajo el mensaje original. Al pulsarlo, se muestra una traducción que sólo el usuario que ha hecho clic puede ver.</p>
|
||||
<div class="demo-area">
|
||||
<div class="discord-message">
|
||||
<img src="https://cdn.discordapp.com/embed/avatars/1.png" alt="avatar" class="avatar">
|
||||
<div class="message-content">
|
||||
<span class="username">UserFromFrance</span>
|
||||
<div class="message-text">Bonjour tout le monde! C'est une démo.</div>
|
||||
<button class="discord-button" onclick="showPopup('ephemeral-popup')">🌐 Traducir</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><strong>✅ Ventajas:</strong><ul><li>No satura el chat general.</li><li>La traducción es bajo demanda.</li><li>Experiencia de usuario moderna y limpia.</li></ul></div>
|
||||
<div class="cons"><strong>⚠️ Consideración:</strong><p style="margin-left: 20px; color: var(--discord-gray);">Requiere manejar interacciones (clicks de botón) en el backend.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opción 2: Hilos -->
|
||||
<div class="option-card">
|
||||
<div class="option-header"><span class="option-icon">🧵</span> <h2 class="option-title">Hilos (Threads)</h2></div>
|
||||
<p class="option-description">Se crea un hilo a partir del mensaje original, y la traducción se publica dentro de ese hilo, manteniendo el canal principal limpio.</p>
|
||||
<div class="demo-area">
|
||||
<div class="discord-message">
|
||||
<img src="https://cdn.discordapp.com/embed/avatars/2.png" alt="avatar" class="avatar">
|
||||
<div class="message-content">
|
||||
<span class="username" style="color: #f1c40f;">UserFromGermany</span>
|
||||
<div class="message-text">Guten Tag, dies ist eine weitere Demo.</div>
|
||||
<div class="thread-link" onclick="toggleThread('thread-view')">↪️ Ver Hilo (1 respuesta)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="thread-view" id="thread-view">
|
||||
<div class="discord-message">
|
||||
<img src="https://www.google.com/s2/favicons?domain=discord.com" alt="avatar" class="avatar">
|
||||
<div class="message-content">
|
||||
<span class="username" style="color: #5865F2;">Translation Bot</span>
|
||||
<div class="message-text"><strong>Traducción (ES):</strong><br>Buenas tardes, esta es otra demo.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><strong>✅ Ventajas:</strong><ul><li>Mantiene el canal principal 100% limpio.</li><li>Permite discusiones sobre la traducción en el hilo.</li><li>Ideal para mensajes largos.</li></ul></div>
|
||||
<div class="cons"><strong>⚠️ Consideración:</strong><p style="margin-left: 20px; color: var(--discord-gray);">Puede generar muchas notificaciones de hilos si hay mucha actividad.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opción 3: Spoilers -->
|
||||
<div class="option-card">
|
||||
<div class="option-header"><span class="option-icon">⬛</span> <h2 class="option-title">Spoilers</h2></div>
|
||||
<p class="option-description">La traducción se añade al mensaje original o en una respuesta, pero oculta tras una etiqueta de spoiler que se revela al hacer clic.</p>
|
||||
<div class="demo-area">
|
||||
<div class="discord-message">
|
||||
<img src="https://cdn.discordapp.com/embed/avatars/3.png" alt="avatar" class="avatar">
|
||||
<div class="message-content">
|
||||
<span class="username" style="color: #e91e63;">UserFromItaly</span>
|
||||
<div class="message-text">
|
||||
Ciao! Benvenuto alla demo finale.<br>
|
||||
<strong>Traducción:</strong> <span class="spoiler" onclick="this.classList.toggle('revealed')">¡Hola! Bienvenido a la demo final.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><strong>✅ Ventajas:</strong><ul><li>Muy fácil de implementar.</li><li>No requiere interacciones complejas.</li><li>La traducción está en el mismo contexto.</li></ul></div>
|
||||
<div class="cons"><strong>⚠️ Consideración:</strong><p style="margin-left: 20px; color: var(--discord-gray);">Añade "ruido" visual al chat, aunque esté oculto.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recommendation">
|
||||
<h3>💡 Nuestra Recomendación</h3>
|
||||
<p>Para la mejor experiencia de usuario y mantener los canales limpios, la **Opción 1 (Botón Efímero)** es la más recomendada. Es moderna, eficiente y le da el control total al usuario sin interrumpir a los demás.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para la demo efímera -->
|
||||
<div class="modal" id="ephemeral-popup" onclick="closePopup('ephemeral-popup')">
|
||||
<div class="modal-content" onclick="event.stopPropagation()">
|
||||
<div class="modal-header">Traducción del mensaje</div>
|
||||
<div class="modal-body">
|
||||
<div class="translation">
|
||||
<strong>Inglés:</strong><br>
|
||||
<span>Hello everyone! This is a demo.</span>
|
||||
</div>
|
||||
<div class="translation">
|
||||
<strong>Español:</strong><br>
|
||||
<span>¡Hola a todos! Esto es una demo.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">Solo tú puedes ver este mensaje.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showPopup(id) {
|
||||
document.getElementById(id).classList.add('active');
|
||||
}
|
||||
function closePopup(id) {
|
||||
document.getElementById(id).classList.remove('active');
|
||||
}
|
||||
function toggleThread(id) {
|
||||
const threadView = document.getElementById(id);
|
||||
if (threadView.style.display === 'block') {
|
||||
threadView.style.display = 'none';
|
||||
} else {
|
||||
threadView.style.display = 'block';
|
||||
}
|
||||
}
|
||||
// Cerrar modal con la tecla Escape
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === "Escape") {
|
||||
const activeModal = document.querySelector('.modal.active');
|
||||
if (activeModal) {
|
||||
closePopup(activeModal.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
765
discord/bot/discord_bot.php.backup
Executable file
@@ -0,0 +1,765 @@
|
||||
<?php
|
||||
// discord_bot.php
|
||||
|
||||
// Pausa intencionada para evitar condiciones de carrera en el arranque
|
||||
sleep(3);
|
||||
|
||||
// Cargar configuración y dependencias
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
require_once __DIR__ . '/../../includes/db.php';
|
||||
require_once __DIR__ . '/../../includes/logger.php';
|
||||
require_once __DIR__ . '/../../src/DiscordSender.php';
|
||||
require_once __DIR__ . '/../../src/HtmlToDiscordMarkdownConverter.php';
|
||||
require_once __DIR__ . '/../../src/Translate.php';
|
||||
require_once __DIR__ . '/../../src/CommandLocker.php';
|
||||
|
||||
// Importar clases necesarias
|
||||
use Discord\Discord;
|
||||
use Discord\WebSockets\Intents;
|
||||
use Discord\WebSockets\Event;
|
||||
use Discord\Parts\Channel\Message;
|
||||
use Discord\Parts\Interactions\Interaction;
|
||||
use Discord\Parts\WebSockets\MessageReaction;
|
||||
use Discord\Builders\MessageBuilder;
|
||||
use Discord\Parts\Embed\Embed;
|
||||
use Discord\Parts\Guild\Member;
|
||||
use Discord\Builders\Components\ActionRow;
|
||||
use Discord\Builders\Components\Button;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
|
||||
$logger = new Logger('DiscordBot');
|
||||
$logger->pushHandler(new StreamHandler(__DIR__.'/../../logs/discord_bot.log', Logger::DEBUG));
|
||||
|
||||
$logger->info("Iniciando bot de Discord...");
|
||||
|
||||
if (!defined('DISCORD_BOT_TOKEN') || empty(DISCORD_BOT_TOKEN)) {
|
||||
$logger->error("Error Fatal: La constante DISCORD_BOT_TOKEN no está definida o está vacía.");
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda un destinatario (usuario o canal) en la base de datos si no existe.
|
||||
*
|
||||
* @param PDO $pdo La conexión a la base de datos.
|
||||
* @param Logger $logger El logger para registrar eventos.
|
||||
* @param string $platform La plataforma (ej. 'discord').
|
||||
* @param string $type El tipo de destinatario ('user' o 'channel').
|
||||
* @param string $platformId El ID de la plataforma.
|
||||
* @param string|null $name El nombre del destinatario.
|
||||
*/
|
||||
function saveRecipientIfNotExists(PDO $pdo, Logger $logger, string $platform, string $type, string $platformId, ?string $name): void {
|
||||
$logger->debug("[Registro BD] Verificando destinatario...", ['type' => $type, 'id' => $platformId, 'name' => $name]);
|
||||
|
||||
if (empty($platformId) || empty($name)) {
|
||||
$logger->warning("[Registro BD] Se omitió guardar destinatario por ID o nombre vacío.", ['type' => $type, 'id' => $platformId, 'name' => $name]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Verificar si el destinatario ya existe
|
||||
$stmt = $pdo->prepare("SELECT id FROM recipients WHERE platform = ? AND platform_id = ?");
|
||||
$stmt->execute([$platform, $platformId]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
// Si no existe, insertarlo
|
||||
$insertStmt = $pdo->prepare(
|
||||
"INSERT INTO recipients (platform, type, platform_id, name) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$insertStmt->execute([$platform, $type, $platformId, $name]);
|
||||
$logger->info("[Registro BD] Nuevo destinatario guardado: Tipo={$type}, ID={$platformId}, Nombre={$name}");
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// Registrar cualquier error durante la operación de base de datos
|
||||
$logger->error("[Registro BD] Error al guardar destinatario: " . $e->getMessage(), [
|
||||
'platformId' => $platformId,
|
||||
'name' => $name,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$discord = new Discord([
|
||||
'token' => DISCORD_BOT_TOKEN,
|
||||
'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS | Intents::MESSAGE_CONTENT,
|
||||
'logger' => $logger
|
||||
]);
|
||||
|
||||
$discord->on('ready', function (Discord $discord) {
|
||||
$discord->getLogger()->info("==================================================");
|
||||
$discord->getLogger()->info("Bot conectado y listo para escuchar!");
|
||||
$discord->getLogger()->info("Usuario: {$discord->user->username}#{$discord->user->discriminator}");
|
||||
$discord->getLogger()->info("==================================================");
|
||||
});
|
||||
|
||||
// Evento para nuevos miembros en el servidor
|
||||
// Nota: DiscordPHP puede enviar Discord\Parts\User\Member en lugar de Discord\Parts\Guild\Member
|
||||
// Por eso NO usamos type hint aquí
|
||||
$discord->on(Event::GUILD_MEMBER_ADD, function ($member, Discord $discord) use ($pdo, $logger) {
|
||||
$logger->info("[NUEVO MIEMBRO] Usuario {$member->user->username} ({$member->id}) se ha unido al servidor.");
|
||||
// Asegurarse de que el usuario existe en la BD
|
||||
saveRecipientIfNotExists($pdo, $logger, 'discord', 'user', $member->id, $member->user->username);
|
||||
|
||||
try {
|
||||
// 1. Obtener idiomas activos de la BD
|
||||
$stmt = $pdo->query("SELECT language_code, language_name, flag_emoji FROM supported_languages WHERE is_active = 1 ORDER BY language_name ASC");
|
||||
$activeLangs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($activeLangs)) {
|
||||
$logger->warning("[BIENVENIDA] No se envió mensaje de bienvenida porque no hay idiomas activos.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Construir el mensaje de bienvenida multi-idioma
|
||||
$translator = new Translate();
|
||||
$baseTitle = "👋 ¡Hola, {$member->user->username}! Bienvenido/a a el Discord de Cereal Kiiller.";
|
||||
$baseDescription = "Por favor, selecciona tu idioma preferido:";
|
||||
|
||||
$fullDescription = "";
|
||||
foreach($activeLangs as $lang) {
|
||||
$langCode = $lang['language_code'];
|
||||
$flag = $lang['flag_emoji'] ? $lang['flag_emoji'] . ' ' : '';
|
||||
|
||||
// Traducir si no es el idioma base (español)
|
||||
$translatedDesc = ($langCode === 'es')
|
||||
? $baseDescription
|
||||
: $translator->translateText($baseDescription, 'es', $langCode);
|
||||
|
||||
if ($translatedDesc) {
|
||||
$fullDescription .= $flag . $translatedDesc . "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
$embed = new Embed($discord);
|
||||
$embed->setTitle($baseTitle); // El título se mantiene simple y en español
|
||||
$embed->setDescription(trim($fullDescription));
|
||||
$embed->setColor("#5865F2"); // Discord Blurple
|
||||
if ($member->user && $member->user->avatar) {
|
||||
$avatarUrl = "https://cdn.discordapp.com/avatars/{$member->user->id}/{$member->user->avatar}.png";
|
||||
$embed->setThumbnail($avatarUrl);
|
||||
}
|
||||
|
||||
$builder = MessageBuilder::new()->addEmbed($embed);
|
||||
|
||||
// 3. Construir los botones en filas de 5
|
||||
$actionRows = [];
|
||||
$currentRow = ActionRow::new();
|
||||
$buttonCount = 0;
|
||||
|
||||
foreach ($activeLangs as $lang) {
|
||||
$button = Button::new(Button::STYLE_SECONDARY, 'set_lang_' . $lang['language_code'])
|
||||
->setLabel($lang['language_name']);
|
||||
if (!empty($lang['flag_emoji'])) {
|
||||
$button->setEmoji($lang['flag_emoji']);
|
||||
}
|
||||
$currentRow->addComponent($button);
|
||||
$buttonCount++;
|
||||
|
||||
if ($buttonCount % 5 === 0) {
|
||||
$actionRows[] = $currentRow;
|
||||
$currentRow = ActionRow::new();
|
||||
}
|
||||
}
|
||||
if ($buttonCount % 5 !== 0) {
|
||||
$actionRows[] = $currentRow;
|
||||
}
|
||||
|
||||
foreach ($actionRows as $row) {
|
||||
$builder->addComponent($row);
|
||||
}
|
||||
|
||||
// 4. Enviar el mensaje como Mensaje Directo (DM) al nuevo usuario.
|
||||
$member->sendMessage($builder)->done(function () use ($logger, $member) {
|
||||
$logger->info("[BIENVENIDA] Mensaje de selección de idioma enviado por DM a {$member->user->username}.");
|
||||
}, function ($error) use ($logger, $member) {
|
||||
$logger->error("[BIENVENIDA] No se pudo enviar DM de bienvenida a {$member->user->username}.", ['error' => $error]);
|
||||
});
|
||||
return;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("[BIENVENIDA] Error fatal al procesar nuevo miembro.", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||
}
|
||||
});
|
||||
|
||||
// Evento para manejar interacciones (clics en botones)
|
||||
$discord->on(Event::INTERACTION_CREATE, function (Interaction $interaction, Discord $discord) use ($pdo, $logger) {
|
||||
// Guardar usuario y canal que interactúan
|
||||
saveRecipientIfNotExists($pdo, $logger, 'discord', 'user', $interaction->user->id, $interaction->user->username);
|
||||
|
||||
$channelName = 'Canal Desconocido';
|
||||
if ($interaction->channel) {
|
||||
$channelName = $interaction->channel->name;
|
||||
if ($interaction->channel->guild) {
|
||||
$channelName = $interaction->channel->guild->name . ' - ' . $channelName;
|
||||
} elseif ($interaction->channel->is_private) {
|
||||
$channelName = 'DM con ' . $interaction->user->username;
|
||||
}
|
||||
}
|
||||
saveRecipientIfNotExists($pdo, $logger, 'discord', 'channel', $interaction->channel_id, $channelName);
|
||||
|
||||
// DiscordPHP v7: No existe isButton(). Botones llegan como MESSAGE_COMPONENT (type = 3) y component_type = 2
|
||||
$type = (int) ($interaction->type ?? 0); // 3 = MESSAGE_COMPONENT
|
||||
$componentType = (int) ($interaction->data->component_type ?? 0); // 2 = BUTTON
|
||||
if ($type !== 3 || $componentType !== 2) return;
|
||||
|
||||
$customId = $interaction->data->custom_id;
|
||||
$userId = $interaction->user->id;
|
||||
$logger->info("[INTERACCION] Usuario $userId hizo clic en el botón: $customId");
|
||||
|
||||
try {
|
||||
// MANEJAR SELECCIÓN DE IDIOMA DE BIENVENIDA
|
||||
if (strpos($customId, 'set_lang_') === 0) {
|
||||
try {
|
||||
$langCode = substr($customId, strlen('set_lang_'));
|
||||
|
||||
// Actualizar la base de datos
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET language_code = ? WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$langCode, $userId]);
|
||||
|
||||
// Obtener el nombre del idioma para el mensaje de confirmación
|
||||
$langNameStmt = $pdo->prepare("SELECT language_name FROM supported_languages WHERE language_code = ?");
|
||||
$langNameStmt->execute([$langCode]);
|
||||
$langName = $langNameStmt->fetchColumn() ?: strtoupper($langCode);
|
||||
|
||||
// Crear el mensaje de confirmación
|
||||
$confirmEmbed = new Embed($discord);
|
||||
$confirmEmbed->setTitle("✅ Idioma Configurado");
|
||||
$confirmEmbed->setDescription("Tu idioma ha sido establecido a: **" . htmlspecialchars($langName) . "**");
|
||||
$confirmEmbed->setColor("#57F287"); // Discord Green
|
||||
|
||||
// Actualizar el mensaje original para mostrar la confirmación y quitar los botones
|
||||
$builder = MessageBuilder::new()->addEmbed($confirmEmbed);
|
||||
|
||||
// Acknowledge the interaction and then edit the message
|
||||
$interaction->acknowledge()->done(function() use ($interaction, $builder, $logger, $langCode, $userId) {
|
||||
$interaction->message->edit($builder)->done(function() use ($logger, $langCode, $userId) {
|
||||
$logger->info("[BIENVENIDA] El usuario $userId ha establecido su idioma a '$langCode' y el mensaje fue editado.");
|
||||
}, function ($error) use ($logger, $langCode, $userId, $interaction) {
|
||||
$logger->error("[BIENVENIDA] Error al editar el mensaje original para {$userId}.", ['lang' => $langCode, 'error' => $error]);
|
||||
// Fallback response: if editing fails, send a new ephemeral message
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ Tu idioma ha sido guardado, pero no se pudo actualizar el mensaje de bienvenida. Error: " . $error->getMessage()), true);
|
||||
});
|
||||
}, function ($error) use ($logger, $langCode, $userId, $interaction) {
|
||||
$logger->error("[BIENVENIDA] Error al reconocer interacción para {$userId}.", ['lang' => $langCode, 'error' => $error]);
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ Error al procesar tu solicitud. Por favor, inténtalo de nuevo."), true);
|
||||
});
|
||||
|
||||
$logger->info("[BIENVENIDA] El usuario $userId ha establecido su idioma a '$langCode'.");
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("[BIENVENIDA] Error al procesar botón de idioma.", ['customId' => $customId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent("❌ Hubo un error al guardar tu selección de idioma."), true);
|
||||
}
|
||||
return; // Finalizar para no procesar otros ifs
|
||||
}
|
||||
|
||||
|
||||
if (strpos($customId, 'translate_auto:') === 0) {
|
||||
// Extraer el ID del mensaje original del custom_id
|
||||
$originalMessageId = substr($customId, strlen('translate_auto:'));
|
||||
|
||||
// Obtener el idioma preferido del usuario desde la tabla recipients
|
||||
$stmt = $pdo->prepare("SELECT language_code FROM recipients WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$userId]);
|
||||
$userLang = $stmt->fetchColumn();
|
||||
|
||||
// Si no tiene idioma configurado, usar español por defecto
|
||||
if (!$userLang) {
|
||||
$userLang = 'es';
|
||||
$logger->info("[TRANSLATION] Usuario {$userId} no tiene idioma configurado, usando 'es' por defecto");
|
||||
}
|
||||
|
||||
$targetLang = $userLang;
|
||||
|
||||
// Obtener el mensaje original usando su ID
|
||||
$interaction->channel->messages->fetch($originalMessageId)->done(function ($originalMessage) use ($interaction, $discord, $pdo, $logger, $userId, $targetLang) {
|
||||
$originalContent = trim((string) ($originalMessage->content ?? ''));
|
||||
|
||||
if (empty($originalContent)) {
|
||||
$interaction->respondWithMessage(
|
||||
MessageBuilder::new()->setContent("❌ No se encontró contenido para traducir."),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$translator = new Translate();
|
||||
$sourceLang = $translator->detectLanguage($originalContent) ?? 'es';
|
||||
|
||||
// Evitar traducir si ya está en el idioma destino
|
||||
if ($sourceLang === $targetLang) {
|
||||
$interaction->respondWithMessage(
|
||||
MessageBuilder::new()->setContent("⚠️ El mensaje ya está en tu idioma ({$targetLang})."),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$translatedText = $translator->translateText($originalContent, $sourceLang, $targetLang);
|
||||
|
||||
if (!empty($translatedText)) {
|
||||
// Obtener información del idioma
|
||||
$stmt = $pdo->prepare("SELECT flag_emoji, language_name FROM supported_languages WHERE language_code = ? AND is_active = 1");
|
||||
$stmt->execute([$targetLang]);
|
||||
$langInfo = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$flag = $langInfo['flag_emoji'] ?? '🏳️';
|
||||
$langName = $langInfo['language_name'] ?? strtoupper($targetLang);
|
||||
|
||||
// Crear embed para la traducción
|
||||
$embed = new Embed($discord);
|
||||
$embed->setTitle("{$flag} Traducción a {$langName}");
|
||||
$embed->setDescription($translatedText);
|
||||
$embed->setColor("#5865F2");
|
||||
$embed->setFooter("Traducido de {$sourceLang} • Solo tú puedes ver esto");
|
||||
|
||||
// Si el mensaje original tiene imágenes, agregar la primera como imagen del embed
|
||||
if (count($originalMessage->attachments) > 0) {
|
||||
$firstAttachment = $originalMessage->attachments->first();
|
||||
if ($firstAttachment && isset($firstAttachment->url)) {
|
||||
// Verificar si es una imagen
|
||||
$contentType = $firstAttachment->content_type ?? '';
|
||||
if (strpos($contentType, 'image/') === 0) {
|
||||
$embed->setImage($firstAttachment->url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enviar como mensaje EFÍMERO (solo visible para quien hizo clic)
|
||||
$builder = MessageBuilder::new()->addEmbed($embed);
|
||||
$interaction->respondWithMessage($builder, true);
|
||||
|
||||
$logger->info("[TRANSLATION] Usuario {$userId} tradujo mensaje de {$sourceLang} a {$targetLang}");
|
||||
} else {
|
||||
$interaction->respondWithMessage(
|
||||
MessageBuilder::new()->setContent("⚠️ No se pudo traducir el mensaje."),
|
||||
true
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$logger->error("[TRANSLATION] Error al traducir", ['error' => $e->getMessage()]);
|
||||
$interaction->respondWithMessage(
|
||||
MessageBuilder::new()->setContent("❌ Error al traducir: " . $e->getMessage()),
|
||||
true
|
||||
);
|
||||
}
|
||||
}, function ($error) use ($interaction, $logger) {
|
||||
$logger->error("[TRANSLATION] Error al obtener mensaje original", ['error' => $error->getMessage()]);
|
||||
$interaction->respondWithMessage(
|
||||
MessageBuilder::new()->setContent("❌ No se pudo obtener el mensaje original."),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Traducción de plantillas completas (comandos #)
|
||||
if (strpos($customId, 'translate_template:') === 0) {
|
||||
$payload = substr($customId, strlen('translate_template:'));
|
||||
$parts = explode(':', $payload, 2);
|
||||
if (count($parts) !== 2) {
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent('Formato de botón inválido.'), true);
|
||||
return;
|
||||
}
|
||||
[$commandKey, $targetLang] = $parts;
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent('⌛ Traduciendo plantilla...'), true);
|
||||
|
||||
try {
|
||||
// Obtener contenido HTML original de la plantilla
|
||||
$stmt = $pdo->prepare("SELECT message_content FROM recurrent_messages WHERE telegram_command = ?");
|
||||
$stmt->execute([$commandKey]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$template) {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . '> No se encontró la plantilla.');
|
||||
return;
|
||||
}
|
||||
|
||||
$originalHtml = $template['message_content'] ?? '';
|
||||
$converter = new HtmlToDiscordMarkdownConverter();
|
||||
$partsArr = $converter->convertToArray($originalHtml);
|
||||
|
||||
// Unir todos los segmentos de texto (las imágenes no se traducen)
|
||||
$fullText = '';
|
||||
foreach ($partsArr as $p) {
|
||||
if (($p['type'] ?? '') === 'text') {
|
||||
$fullText .= ($fullText === '' ? '' : "\n\n") . trim((string)$p['content']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($fullText === '') {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . '> No hay contenido de texto para traducir en la plantilla.');
|
||||
return;
|
||||
}
|
||||
|
||||
$translator = new Translate();
|
||||
$sourceLang = $translator->detectLanguage($fullText) ?? 'es';
|
||||
if ($sourceLang === $targetLang) {
|
||||
// Fallback: fuerza ES como origen si coincide con el destino (común en nuestras plantillas)
|
||||
$fallbackSrc = 'es';
|
||||
if ($fallbackSrc !== $targetLang) {
|
||||
$translated = $translator->translateText($fullText, $fallbackSrc, $targetLang);
|
||||
$sourceLang = $fallbackSrc;
|
||||
} else {
|
||||
$translated = '';
|
||||
}
|
||||
} else {
|
||||
$translated = $translator->translateText($fullText, $sourceLang, $targetLang);
|
||||
}
|
||||
if ($translated === '') {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . '> El contenido ya está en este idioma.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Bandera
|
||||
$flag = '';
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT flag_emoji FROM supported_languages WHERE language_code = ? AND is_active = 1");
|
||||
$stmt->execute([$targetLang]);
|
||||
$flag = $stmt->fetchColumn() ?: '';
|
||||
} catch (\Throwable $e) { /* noop */ }
|
||||
$flag = $flag !== '' ? $flag : '🏳️';
|
||||
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
// Pequeña espera para que el mensaje efímero aparezca primero
|
||||
usleep(300000);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . "> {$flag} Traducción plantilla ({$sourceLang} → {$targetLang}):\n" . $translated);
|
||||
} catch (\Throwable $e) {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . "> Error al traducir plantilla: " . $e->getMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($customId === 'platicar_bot' || $customId === 'usar_ia') {
|
||||
$newMode = ($customId === 'platicar_bot') ? 'bot' : 'ia';
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET chat_mode = ? WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$newMode, $userId]);
|
||||
|
||||
$responseText = $newMode === 'bot'
|
||||
? "🤖 Modo cambiado a 'Platicar con bot'. Ahora puedes usar los comandos normales como `/comandos`."
|
||||
: "🧠 Modo cambiado a 'Usar IA'. Todo lo que escribas será procesado por la IA.\n\nEscribe `/agente` para volver a este menú.";
|
||||
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent($responseText), true);
|
||||
$logger->info("[MODO AGENTE] Usuario $userId cambió al modo: $newMode");
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("[INTERACCION] Error al procesar un botón.", ['customId' => $customId, 'error' => $e->getMessage()]);
|
||||
// Mensaje genérico para otros botones que no sean de idioma
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent("Hubo un error al procesar esta acción."), true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Evento para manejar mensajes
|
||||
$discord->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) use ($pdo, $logger) {
|
||||
if ($message->author->bot) return;
|
||||
|
||||
// Guardar usuario
|
||||
saveRecipientIfNotExists($pdo, $logger, 'discord', 'user', $message->author->id, $message->author->username);
|
||||
|
||||
// Guardar canal con nombre mejorado
|
||||
$channelName = $message->channel->name;
|
||||
if ($message->channel->is_private) {
|
||||
$channelName = 'DM con ' . $message->author->username;
|
||||
} elseif ($message->channel->guild) {
|
||||
$channelName = $message->channel->guild->name . ' - ' . $channelName;
|
||||
}
|
||||
saveRecipientIfNotExists($pdo, $logger, 'discord', 'channel', $message->channel->id, $channelName);
|
||||
|
||||
$logger->info("[Mensaje Recibido] En canal '{$channelName}' de @{$message->author->username}: {$message->content}");
|
||||
|
||||
$isPrivateChat = $message->channel->is_private;
|
||||
$userId = $message->author->id;
|
||||
$content = $message->content;
|
||||
|
||||
try {
|
||||
if ($isPrivateChat) {
|
||||
// --- LÓGICA DE CHAT PRIVADO (AGENTE/IA/BOT) ---
|
||||
$logger->info("[MODO AGENTE] Mensaje en chat privado de $userId.");
|
||||
|
||||
$stmt = $pdo->prepare("SELECT chat_mode FROM recipients WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$userId]);
|
||||
$userChatMode = $stmt->fetchColumn();
|
||||
|
||||
if ($userChatMode === false) {
|
||||
// El registro ya se hizo con saveRecipientIfNotExists, solo necesitamos el modo
|
||||
$logger->info("[NUEVO USUARIO DM] Usuario $userId no tenía modo. Asignando 'agent'.");
|
||||
$updateStmt = $pdo->prepare("UPDATE recipients SET chat_mode = 'agent' WHERE platform_id = ? AND platform = 'discord'");
|
||||
$updateStmt->execute([$userId]);
|
||||
$userChatMode = 'agent';
|
||||
}
|
||||
|
||||
if (trim($content) === '/agente') {
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET chat_mode = 'agent' WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$userId]);
|
||||
$userChatMode = 'agent';
|
||||
$logger->info("[MODO AGENTE] Usuario $userId usó /agente. Reseteando a modo 'agent'.");
|
||||
}
|
||||
|
||||
switch ($userChatMode) {
|
||||
case 'agent':
|
||||
$builder = MessageBuilder::new()->setContent("👋 Hola! ¿Cómo quieres interactuar?");
|
||||
$actionRow = ActionRow::new()
|
||||
->addComponent(Button::new(Button::STYLE_PRIMARY, 'platicar_bot')->setLabel('🤖 Platicar con bot'))
|
||||
->addComponent(Button::new(Button::STYLE_SUCCESS, 'usar_ia')->setLabel('🧠 Usar IA'));
|
||||
$builder->addComponent($actionRow);
|
||||
$message->channel->sendMessage($builder);
|
||||
return;
|
||||
|
||||
case 'ia':
|
||||
$logger->info("[MODO IA] Mensaje de $userId para la IA: $content");
|
||||
$n8nWebhookUrl = $_ENV['N8N_IA_WEBHOOK_URL_DISCORD'] ?? null;
|
||||
if ($n8nWebhookUrl) {
|
||||
$postData = [
|
||||
'chat_id' => $message->channel_id,
|
||||
'user_id' => $userId,
|
||||
'message' => $content,
|
||||
'name' => $message->author->username
|
||||
];
|
||||
$ch = curl_init($n8nWebhookUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
$logger->info("[MODO IA] Mensaje reenviado a n8n.");
|
||||
} else {
|
||||
$logger->error("[MODO IA] La variable N8N_IA_WEBHOOK_URL no está configurada.");
|
||||
}
|
||||
return;
|
||||
|
||||
case 'bot':
|
||||
// Continuar con la lógica normal de bot abajo
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- LÓGICA DE BOT NORMAL (CHATS PÚBLICOS O MODO 'bot') ---
|
||||
if (strpos($content, '/') === 0 || strpos($content, '#') === 0) {
|
||||
handleDiscordCommand($message, $pdo, $logger);
|
||||
} else if (strtolower($content) === '!ping') {
|
||||
$message->reply('pong!');
|
||||
} else {
|
||||
handleDiscordTranslation($message, $pdo, $logger);
|
||||
}
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("!!!!!!!!!! ERROR FATAL CAPTURADO !!!!!!!!!!", [
|
||||
'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$discord->run();
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$logger->critical("!!!!!!!!!! ERROR FATAL AL INICIAR !!!!!!!!!!", [
|
||||
'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()
|
||||
]);
|
||||
die();
|
||||
}
|
||||
|
||||
function handleDiscordCommand(Message $message, PDO $pdo, Logger $logger)
|
||||
{
|
||||
$text = trim($message->content);
|
||||
$userId = $message->author->id;
|
||||
|
||||
if (strpos($text, '#') === 0) {
|
||||
$command = ltrim($text, '#');
|
||||
$logger->info("[Comando] Usuario @{$message->author->username} solicitó: #{$command}");
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT message_content FROM recurrent_messages WHERE telegram_command = ?");
|
||||
$stmt->execute([$command]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($template) {
|
||||
$logger->info("[Comando] Plantilla encontrada para #{$command}.");
|
||||
$originalHtml = $template['message_content'];
|
||||
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sentMessageIds = $sender->sendMessage($message->channel_id, $originalHtml);
|
||||
|
||||
if ($sentMessageIds && !empty($sentMessageIds)) {
|
||||
$firstMessageId = $sentMessageIds[0];
|
||||
$logger->info("Mensajes enviados, primer ID: {$firstMessageId}");
|
||||
|
||||
// Añadir botones de traducción dinámicos
|
||||
$message->channel->messages->fetch($firstMessageId)->done(function (Message $sentMessage) use ($pdo, $logger, $command) {
|
||||
$langStmt = $pdo->query("SELECT language_code, language_name, flag_emoji FROM supported_languages WHERE is_active = 1");
|
||||
$activeLangs = $langStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($activeLangs) > 1) {
|
||||
$components = [];
|
||||
$actionRow = ActionRow::new();
|
||||
$buttonCount = 0;
|
||||
|
||||
foreach ($activeLangs as $lang) {
|
||||
// Para plantillas, incluir el comando en el custom_id para traducir el contenido completo original
|
||||
$button = Button::new(Button::STYLE_SECONDARY, 'translate_template:' . $command . ':' . $lang['language_code'])
|
||||
->setLabel($lang['language_name']);
|
||||
if (!empty($lang['flag_emoji'])) {
|
||||
$button->setEmoji($lang['flag_emoji']);
|
||||
}
|
||||
$actionRow->addComponent($button);
|
||||
$buttonCount++;
|
||||
|
||||
// Discord permite máximo 5 botones por ActionRow
|
||||
if ($buttonCount % 5 === 0) {
|
||||
$components[] = $actionRow;
|
||||
$actionRow = ActionRow::new();
|
||||
}
|
||||
}
|
||||
if ($buttonCount % 5 !== 0) {
|
||||
$components[] = $actionRow;
|
||||
}
|
||||
|
||||
$builder = MessageBuilder::new()->setComponents($components);
|
||||
$sentMessage->edit($builder);
|
||||
$logger->info("Botones de traducción añadidos al mensaje {$sentMessage->id}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
$message->reply("El comando `#{$command}` no fue encontrado.");
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("[Error Comando] Procesando #{$command}", ['error' => $e->getMessage()]);
|
||||
$message->reply("Ocurrió un error inesperado al procesar tu comando.");
|
||||
}
|
||||
} elseif (strpos($text, '/setlang') === 0) {
|
||||
$parts = explode(' ', $text, 2);
|
||||
if (count($parts) < 2 || strlen(trim($parts[1])) !== 2) {
|
||||
$message->reply("❌ Formato incorrecto. Usa: `/setlang es`");
|
||||
return;
|
||||
}
|
||||
$newLangCode = strtolower(trim($parts[1]));
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET language_code = ? WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$newLangCode, $userId]);
|
||||
$message->reply("✅ Tu idioma ha sido establecido a '" . strtoupper($newLangCode) . "'.");
|
||||
|
||||
} elseif (strpos($text, '/bienvenida') === 0) {
|
||||
$configStmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
|
||||
$welcomeConfig = $configStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($welcomeConfig && $welcomeConfig['is_active']) {
|
||||
$messageText = str_replace('{user_name}', $message->author->username, $welcomeConfig['message_text']);
|
||||
$converter = new HtmlToDiscordMarkdownConverter();
|
||||
$markdownText = $converter->convert($messageText);
|
||||
$builder = MessageBuilder::new()->setContent($markdownText);
|
||||
$actionRow = ActionRow::new()
|
||||
->addComponent(Button::new(Button::STYLE_LINK, $welcomeConfig['group_invite_link'])->setLabel($welcomeConfig['button_text']));
|
||||
$builder->addComponent($actionRow);
|
||||
$message->channel->sendMessage($builder);
|
||||
} else {
|
||||
$message->reply("ℹ️ La función de bienvenida no está activa.");
|
||||
}
|
||||
|
||||
} elseif (strpos($text, '/comandos') === 0) {
|
||||
$stmt = $pdo->query("SELECT telegram_command, name FROM recurrent_messages WHERE telegram_command IS NOT NULL AND telegram_command != '' ORDER BY name ASC");
|
||||
$commands = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (empty($commands)) {
|
||||
$message->reply("ℹ️ No hay comandos personalizados disponibles.");
|
||||
} else {
|
||||
$response = "**LISTA DE COMANDOS DISPONIBLES**\n\n";
|
||||
foreach ($commands as $cmd) {
|
||||
$command = trim($cmd['telegram_command']);
|
||||
if (strpos($command, '#') !== 0) $command = '#' . $command;
|
||||
$name = trim($cmd['name']);
|
||||
$response .= "`" . $command . "` - " . $name . "\n";
|
||||
}
|
||||
$response .= "\nℹ️ Escribe el comando para usarlo.";
|
||||
$message->channel->sendMessage($response);
|
||||
}
|
||||
|
||||
} elseif (strpos($text, '/agente') === 0) {
|
||||
$prompt = trim(substr($text, strlen('/agente')));
|
||||
$logger->info("[Comando] Usuario @{$message->author->username} solicitó: /agente con prompt: '{$prompt}'");
|
||||
|
||||
if (empty($prompt)) {
|
||||
$message->reply("Por favor, escribe tu consulta después de /agente.");
|
||||
return;
|
||||
}
|
||||
|
||||
$n8nWebhookUrl = $_ENV['N8N_IA_WEBHOOK_URL_DISCORD'] ?? null;
|
||||
if ($n8nWebhookUrl) {
|
||||
$postData = [
|
||||
'chat_id' => $message->channel_id,
|
||||
'user_id' => $message->author->id,
|
||||
'message' => $prompt,
|
||||
'name' => $message->author->username
|
||||
];
|
||||
$ch = curl_init($n8nWebhookUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$logger->info("[Comando /agente] Prompt reenviado a n8n.");
|
||||
$message->reply("Tu solicitud ha sido enviada al agente. Recibirás una respuesta en breve.");
|
||||
} else {
|
||||
$logger->error("[Comando /agente] La variable N8N_IA_WEBHOOK_URL no está configurada.");
|
||||
$message->reply("No se pudo procesar tu solicitud. El servicio de agente no está configurado.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleDiscordTranslation(Message $message, PDO $pdo, Logger $logger)
|
||||
{
|
||||
try {
|
||||
$translator = new Translate();
|
||||
$text = $message->content;
|
||||
|
||||
// 1. Detectar idioma original
|
||||
$detectedLang = $translator->detectLanguage(strip_tags($text)) ?? 'es';
|
||||
|
||||
// 2. Obtener todos los idiomas activos con información completa
|
||||
$langStmt = $pdo->query("SELECT language_code, language_name, flag_emoji FROM supported_languages WHERE is_active = 1 ORDER BY language_name ASC");
|
||||
$activeLangs = $langStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Filtrar los idiomas de destino (todos los activos menos el original)
|
||||
$targetLangs = array_filter($activeLangs, function($lang) use ($detectedLang) {
|
||||
return $lang['language_code'] !== $detectedLang;
|
||||
});
|
||||
|
||||
// 4. Si no hay idiomas a los que traducir, no hacer nada
|
||||
if (empty($targetLangs)) {
|
||||
$logger->info("[TRANSLATION_BUTTONS] No se requieren botones de traducción para el mensaje de Discord #{$message->id} desde '$detectedLang'.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Crear un solo botón de traducción con el ID del mensaje original
|
||||
$button = Button::new(Button::STYLE_PRIMARY, 'translate_auto:' . $message->id)
|
||||
->setLabel('Traducir')
|
||||
->setEmoji('🌐');
|
||||
|
||||
$actionRow = ActionRow::new()->addComponent($button);
|
||||
|
||||
// 6. Enviar mensaje del bot con botón como respuesta al mensaje original
|
||||
$builder = MessageBuilder::new()
|
||||
->setContent('🌐 Traducción disponible')
|
||||
->setComponents([$actionRow]);
|
||||
|
||||
$message->reply($builder)->done(function () use ($logger, $message, $detectedLang) {
|
||||
$logger->info("[TRANSLATION_BUTTONS] Botones de traducción enviados para mensaje #{$message->id} (idioma detectado: $detectedLang)");
|
||||
}, function ($error) use ($logger, $message) {
|
||||
$logger->error("[TRANSLATION_BUTTONS] Error al enviar botones para mensaje #{$message->id}", ['error' => $error->getMessage()]);
|
||||
});
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("[TRANSLATION_BUTTONS] Error al procesar mensaje para botones de traducción", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||
}
|
||||
}
|
||||
|
||||
209
discord/converters/HtmlToDiscordMarkdownConverter.php
Executable file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
// Cargar configuración
|
||||
require_once __DIR__ . '/../../config/config.php';
|
||||
|
||||
class HtmlToDiscordMarkdownConverter
|
||||
{
|
||||
private const DISCORD_MESSAGE_LIMIT = 2000;
|
||||
|
||||
public function convert(string $html): string
|
||||
{
|
||||
$dom = new DOMDocument();
|
||||
libxml_use_internal_errors(true); // Suppress warnings for malformed HTML
|
||||
// Use LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD to prevent adding html/body tags
|
||||
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
$markdown = '';
|
||||
|
||||
foreach ($dom->childNodes as $node) {
|
||||
$markdown .= $this->processNode($node);
|
||||
}
|
||||
|
||||
// Clean up extra newlines
|
||||
$markdown = preg_replace('/\n{3,}/', "\n\n", $markdown);
|
||||
$markdown = trim($markdown);
|
||||
|
||||
return $markdown;
|
||||
}
|
||||
|
||||
private function processNode(DOMNode $node): string
|
||||
{
|
||||
$output = '';
|
||||
|
||||
switch ($node->nodeType) {
|
||||
case XML_TEXT_NODE:
|
||||
$output .= $this->decodeHtmlEntities($node->nodeValue);
|
||||
break;
|
||||
case XML_ELEMENT_NODE:
|
||||
switch (strtolower($node->nodeName)) {
|
||||
case 'b':
|
||||
case 'strong':
|
||||
$output .= '**' . $this->processChildren($node) . '**';
|
||||
break;
|
||||
case 'i':
|
||||
case 'em':
|
||||
$output .= '*' . $this->processChildren($node) . '*';
|
||||
break;
|
||||
case 'u':
|
||||
$output .= '__' . $this->processChildren($node) . '__';
|
||||
break;
|
||||
case 'a':
|
||||
$href = $node->getAttribute('href');
|
||||
|
||||
// Ignorar nodos de texto con solo espacios en blanco para un análisis más preciso.
|
||||
$realChildNodes = [];
|
||||
foreach ($node->childNodes as $child) {
|
||||
if ($child->nodeType === XML_TEXT_NODE && trim($child->nodeValue) === '') {
|
||||
continue;
|
||||
}
|
||||
$realChildNodes[] = $child;
|
||||
}
|
||||
|
||||
// Si el único hijo real es una imagen, procesarla directamente.
|
||||
if (count($realChildNodes) === 1 && strtolower($realChildNodes[0]->nodeName) === 'img') {
|
||||
$output .= $this->processChildren($node);
|
||||
} else {
|
||||
// Si no, trátalo como un enlace de texto normal.
|
||||
$text = $this->processChildren($node);
|
||||
$output .= "[{$text}]({$href})";
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
$output .= $this->processChildren($node) . "\n\n";
|
||||
break;
|
||||
case 'br':
|
||||
$output .= "\n";
|
||||
break;
|
||||
case 'ul':
|
||||
case 'ol':
|
||||
$listContent = $this->processChildren($node);
|
||||
$listItems = explode("\n", trim($listContent));
|
||||
$formattedList = [];
|
||||
$counter = 1;
|
||||
foreach($listItems as $item) {
|
||||
if(empty(trim($item))) continue;
|
||||
if (strtolower($node->nodeName) === 'ul') {
|
||||
$formattedList[] = '- ' . trim($item);
|
||||
} else {
|
||||
$formattedList[] = ($counter++) . '. ' . trim($item);
|
||||
}
|
||||
}
|
||||
$output .= implode("\n", $formattedList) . "\n\n";
|
||||
break;
|
||||
case 'li':
|
||||
$output .= $this->processChildren($node);
|
||||
break;
|
||||
case 'img':
|
||||
$src = $node->getAttribute('src');
|
||||
if (!empty($src)) {
|
||||
$absoluteImageUrl = $src;
|
||||
// Convertir URL relativa a absoluta si es necesario
|
||||
if (strpos($src, 'http') !== 0 && strpos($src, '//') !== 0) {
|
||||
$base = rtrim(BOT_BASE_URL, '/');
|
||||
$path = ltrim($src, '/');
|
||||
$absoluteImageUrl = "{$base}/{$path}";
|
||||
}
|
||||
// Dejar solo la URL para que Discord la renderice
|
||||
$output .= "\n" . $absoluteImageUrl . "\n";
|
||||
}
|
||||
break;
|
||||
case 'div':
|
||||
$output .= $this->processChildren($node);
|
||||
break;
|
||||
default:
|
||||
// For unknown tags, just process their children
|
||||
$output .= $this->processChildren($node);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function processChildren(DOMNode $node): string
|
||||
{
|
||||
$childrenOutput = '';
|
||||
foreach ($node->childNodes as $child) {
|
||||
$childrenOutput .= $this->processNode($child);
|
||||
}
|
||||
return $childrenOutput;
|
||||
}
|
||||
|
||||
private function decodeHtmlEntities(string $encodedString): string
|
||||
{
|
||||
return html_entity_decode($encodedString, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
public function convertToArray(string $html): array
|
||||
{
|
||||
$parts = [];
|
||||
$dom = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
foreach ($dom->childNodes as $node) {
|
||||
$this->processNodeForArray($node, $parts);
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
private function processNodeForArray(DOMNode $node, array &$parts)
|
||||
{
|
||||
if ($node->nodeType === XML_TEXT_NODE) {
|
||||
$this->addTextPart($parts, $this->decodeHtmlEntities($node->nodeValue));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node->nodeType !== XML_ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (strtolower($node->nodeName)) {
|
||||
case 'img':
|
||||
$src = $node->getAttribute('src');
|
||||
if (!empty($src)) {
|
||||
$absoluteImageUrl = $src;
|
||||
if (strpos($src, 'http') !== 0 && strpos($src, '//') !== 0) {
|
||||
$base = rtrim(BOT_BASE_URL, '/');
|
||||
$path = ltrim($src, '/');
|
||||
$absoluteImageUrl = "{$base}/{$path}";
|
||||
}
|
||||
$parts[] = ['type' => 'image', 'url' => $absoluteImageUrl];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
case 'div':
|
||||
if ($node->hasChildNodes()) {
|
||||
foreach ($node->childNodes as $child) {
|
||||
$this->processNodeForArray($child, $parts);
|
||||
}
|
||||
}
|
||||
$this->addTextPart($parts, "\n\n");
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($node->hasChildNodes()) {
|
||||
foreach ($node->childNodes as $child) {
|
||||
$this->processNodeForArray($child, $parts);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function addTextPart(array &$parts, string $text)
|
||||
{
|
||||
if (empty($text)) return;
|
||||
|
||||
// Si la última parte fue texto, la unimos a ella.
|
||||
if (!empty($parts) && end($parts)['type'] === 'text') {
|
||||
$parts[key($parts)]['content'] .= $text;
|
||||
} else {
|
||||
$parts[] = ['type' => 'text', 'content' => $text];
|
||||
}
|
||||
}
|
||||
}
|
||||
535
discord_bot.php
Executable file
@@ -0,0 +1,535 @@
|
||||
<?php
|
||||
// discord_bot.php
|
||||
|
||||
// Cargar configuración y dependencias
|
||||
require_once __DIR__ . '/config/config.php';
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/logger.php';
|
||||
require_once __DIR__ . '/discord/DiscordSender.php';
|
||||
require_once __DIR__ . '/discord/converters/HtmlToDiscordMarkdownConverter.php';
|
||||
require_once __DIR__ . '/includes/Translate.php';
|
||||
|
||||
// Importar clases necesarias
|
||||
use Discord\Discord;
|
||||
use Discord\WebSockets\Intents;
|
||||
use Discord\WebSockets\Event;
|
||||
use Discord\Parts\Channel\Message;
|
||||
use Discord\Parts\Interactions\Interaction;
|
||||
use Discord\Parts\WebSockets\MessageReaction;
|
||||
use Discord\Builders\MessageBuilder;
|
||||
use Discord\Parts\Embed\Embed;
|
||||
use Discord\Parts\Guild\Member;
|
||||
use Discord\Builders\Components\ActionRow;
|
||||
use Discord\Builders\Components\Button;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
|
||||
$logger = new Logger('DiscordBot');
|
||||
$logger->pushHandler(new StreamHandler(__DIR__.'/logs/discord_bot.log', Logger::DEBUG));
|
||||
|
||||
$logger->info("Iniciando bot de Discord...");
|
||||
|
||||
if (!defined('DISCORD_BOT_TOKEN') || empty(DISCORD_BOT_TOKEN)) {
|
||||
$logger->error("Error Fatal: La constante DISCORD_BOT_TOKEN no está definida o está vacía.");
|
||||
die();
|
||||
}
|
||||
|
||||
try {
|
||||
$discord = new Discord([
|
||||
'token' => DISCORD_BOT_TOKEN,
|
||||
'intents' => Intents::GUILDS | Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::GUILD_MEMBERS | Intents::GUILD_MESSAGE_REACTIONS,
|
||||
'logger' => $logger
|
||||
]);
|
||||
|
||||
$discord->on('ready', function (Discord $discord) {
|
||||
$discord->getLogger()->info("==================================================");
|
||||
$discord->getLogger()->info("Bot conectado y listo para escuchar!");
|
||||
$discord->getLogger()->info("Usuario: {$discord->user->username}#{$discord->user->discriminator}");
|
||||
$discord->getLogger()->info("==================================================");
|
||||
});
|
||||
|
||||
// Evento para nuevos miembros en el servidor
|
||||
$discord->on(Event::GUILD_MEMBER_ADD, function (Member $member, Discord $discord) use ($pdo, $logger) {
|
||||
$logger->info("[NUEVO MIEMBRO] Usuario {$member->user->username} ({$member->id}) se ha unido al servidor.");
|
||||
try {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO recipients (platform_id, name, type, platform, language_code)
|
||||
VALUES (?, ?, 'user', 'discord', 'es')
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name)"
|
||||
);
|
||||
$stmt->execute([$member->id, $member->user->username]);
|
||||
$logger->info("[NUEVO MIEMBRO] Usuario {$member->user->username} registrado/actualizado en la base de datos.");
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("[NUEVO MIEMBRO] Error al registrar al usuario.", ['error' => $e->getMessage()]);
|
||||
}
|
||||
});
|
||||
|
||||
// Evento para manejar interacciones (clics en botones)
|
||||
$discord->on(Event::INTERACTION_CREATE, function (Interaction $interaction, Discord $discord) use ($pdo, $logger) {
|
||||
// DiscordPHP v7: No existe isButton(). Botones llegan como MESSAGE_COMPONENT (type = 3) y component_type = 2
|
||||
$type = (int) ($interaction->type ?? 0); // 3 = MESSAGE_COMPONENT
|
||||
$componentType = (int) ($interaction->data->component_type ?? 0); // 2 = BUTTON
|
||||
if ($type !== 3 || $componentType !== 2) return;
|
||||
|
||||
$customId = $interaction->data->custom_id;
|
||||
$userId = $interaction->user->id;
|
||||
$logger->info("[INTERACCION] Usuario $userId hizo clic en el botón: $customId");
|
||||
|
||||
try {
|
||||
if (strpos($customId, 'translate_manual:') === 0) {
|
||||
$targetLang = substr($customId, strlen('translate_manual:'));
|
||||
$originalMessage = $interaction->message;
|
||||
$channelId = $originalMessage->channel_id;
|
||||
|
||||
// Responder de inmediato para evitar timeout de interacción
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent('⌛ Procesando traducción...'), true);
|
||||
|
||||
// Extraer contenido del mensaje: primero content plano, luego embeds como fallback
|
||||
$originalContent = trim((string) ($originalMessage->content ?? ''));
|
||||
if ($originalContent === '' && count($originalMessage->embeds) > 0) {
|
||||
foreach ($originalMessage->embeds as $embed) {
|
||||
$originalContent .= trim((string) ($embed->description ?? '')) . "\n";
|
||||
}
|
||||
$originalContent = trim($originalContent);
|
||||
}
|
||||
|
||||
if (!empty($originalContent)) {
|
||||
try {
|
||||
$translator = new Translate();
|
||||
$sourceLang = $translator->detectLanguage($originalContent) ?? 'es';
|
||||
if ($sourceLang === $targetLang) {
|
||||
// Fallback: muchas plantillas están en ES; intenta forzar ES como origen
|
||||
$fallbackSrc = 'es';
|
||||
if ($fallbackSrc !== $targetLang) {
|
||||
$translatedText = $translator->translateText($originalContent, $fallbackSrc, $targetLang);
|
||||
$sourceLang = $fallbackSrc;
|
||||
} else {
|
||||
$translatedText = null;
|
||||
}
|
||||
} else {
|
||||
$translatedText = $translator->translateText($originalContent, $sourceLang, $targetLang);
|
||||
}
|
||||
|
||||
// Obtener bandera desde supported_languages
|
||||
$flag = '';
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT flag_emoji FROM supported_languages WHERE language_code = ? AND is_active = 1");
|
||||
$stmt->execute([$targetLang]);
|
||||
$flag = $stmt->fetchColumn() ?: '';
|
||||
} catch (\Throwable $e) { /* noop */ }
|
||||
|
||||
$flag = $flag !== '' ? $flag : '🏳️';
|
||||
|
||||
// Enviar resultado como mensaje normal al canal, mencionando al usuario
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$mention = '<@' . $userId . '>';
|
||||
$content = "{$mention} {$flag} Traducción ({$sourceLang} → {$targetLang}):\n> " . $translatedText;
|
||||
// Pequeña espera para que el mensaje efímero aparezca primero
|
||||
usleep(300000);
|
||||
$sender->sendRawMessage($channelId, $content);
|
||||
if (empty($translatedText)) {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($channelId, '<@' . $userId . '> El mensaje ya está en este idioma.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($channelId, '<@' . $userId . "> Error al traducir: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($channelId, '<@' . $userId . '> No se encontró contenido para traducir.');
|
||||
}
|
||||
return; // Salir después de manejar la interacción
|
||||
}
|
||||
|
||||
// Traducción de plantillas completas (comandos #)
|
||||
if (strpos($customId, 'translate_template:') === 0) {
|
||||
$payload = substr($customId, strlen('translate_template:'));
|
||||
$parts = explode(':', $payload, 2);
|
||||
if (count($parts) !== 2) {
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent('Formato de botón inválido.'), true);
|
||||
return;
|
||||
}
|
||||
[$commandKey, $targetLang] = $parts;
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent('⌛ Traduciendo plantilla...'), true);
|
||||
|
||||
try {
|
||||
// Obtener contenido HTML original de la plantilla
|
||||
$stmt = $pdo->prepare("SELECT message_content FROM recurrent_messages WHERE telegram_command = ?");
|
||||
$stmt->execute([$commandKey]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$template) {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . '> No se encontró la plantilla.');
|
||||
return;
|
||||
}
|
||||
|
||||
$originalHtml = $template['message_content'] ?? '';
|
||||
$converter = new HtmlToDiscordMarkdownConverter();
|
||||
$partsArr = $converter->convertToArray($originalHtml);
|
||||
|
||||
// Unir todos los segmentos de texto (las imágenes no se traducen)
|
||||
$fullText = '';
|
||||
foreach ($partsArr as $p) {
|
||||
if (($p['type'] ?? '') === 'text') {
|
||||
$fullText .= ($fullText === '' ? '' : "\n\n") . trim((string)$p['content']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($fullText === '') {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . '> No hay contenido de texto para traducir en la plantilla.');
|
||||
return;
|
||||
}
|
||||
|
||||
$translator = new Translate();
|
||||
$sourceLang = $translator->detectLanguage($fullText) ?? 'es';
|
||||
if ($sourceLang === $targetLang) {
|
||||
// Fallback: fuerza ES como origen si coincide con el destino (común en nuestras plantillas)
|
||||
$fallbackSrc = 'es';
|
||||
if ($fallbackSrc !== $targetLang) {
|
||||
$translated = $translator->translateText($fullText, $fallbackSrc, $targetLang);
|
||||
$sourceLang = $fallbackSrc;
|
||||
} else {
|
||||
$translated = '';
|
||||
}
|
||||
} else {
|
||||
$translated = $translator->translateText($fullText, $sourceLang, $targetLang);
|
||||
}
|
||||
if ($translated === '') {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . '> El contenido ya está en este idioma.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Bandera
|
||||
$flag = '';
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT flag_emoji FROM supported_languages WHERE language_code = ? AND is_active = 1");
|
||||
$stmt->execute([$targetLang]);
|
||||
$flag = $stmt->fetchColumn() ?: '';
|
||||
} catch (\Throwable $e) { /* noop */ }
|
||||
$flag = $flag !== '' ? $flag : '🏳️';
|
||||
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
// Pequeña espera para que el mensaje efímero aparezca primero
|
||||
usleep(300000);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . "> {$flag} Traducción plantilla ({$sourceLang} → {$targetLang}):\n" . $translated);
|
||||
} catch (\Throwable $e) {
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sender->sendRawMessage($interaction->channel_id, '<@' . $userId . "> Error al traducir plantilla: " . $e->getMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($customId === 'platicar_bot' || $customId === 'usar_ia') {
|
||||
$newMode = ($customId === 'platicar_bot') ? 'bot' : 'ia';
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET chat_mode = ? WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$newMode, $userId]);
|
||||
|
||||
$responseText = $newMode === 'bot'
|
||||
? "🤖 Modo cambiado a 'Platicar con bot'. Ahora puedes usar los comandos normales como `/comandos`."
|
||||
: "🧠 Modo cambiado a 'Usar IA'. Todo lo que escribas será procesado por la IA.\n\nEscribe `/agente` para volver a este menú.";
|
||||
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent($responseText), true);
|
||||
$logger->info("[MODO AGENTE] Usuario $userId cambió al modo: $newMode");
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("[INTERACCION] Error al procesar el botón.", ['error' => $e->getMessage()]);
|
||||
$interaction->respondWithMessage(MessageBuilder::new()->setContent("Hubo un error al cambiar de modo."), true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Evento para manejar mensajes
|
||||
$discord->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) use ($pdo, $logger) {
|
||||
if ($message->author->bot) return;
|
||||
|
||||
$logger->info("[Mensaje Recibido] En canal '{$message->channel->name}' de @{$message->author->username}: {$message->content}");
|
||||
|
||||
$isPrivateChat = $message->channel->is_private;
|
||||
$userId = $message->author->id;
|
||||
$content = $message->content;
|
||||
|
||||
try {
|
||||
if ($isPrivateChat) {
|
||||
// --- LÓGICA DE CHAT PRIVADO (AGENTE/IA/BOT) ---
|
||||
$logger->info("[MODO AGENTE] Mensaje en chat privado de $userId.");
|
||||
|
||||
$stmt = $pdo->prepare("SELECT chat_mode FROM recipients WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$userId]);
|
||||
$userChatMode = $stmt->fetchColumn();
|
||||
|
||||
if ($userChatMode === false) {
|
||||
// --- Nuevo Usuario en DM ---
|
||||
$logger->info("[NUEVO USUARIO DM] Usuario $userId no encontrado. Registrando...");
|
||||
$userName = $message->author->username;
|
||||
|
||||
$insertStmt = $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)"
|
||||
);
|
||||
$insertStmt->execute([$userId, $userName]);
|
||||
$logger->info("[NUEVO USUARIO DM] Usuario $userId ($userName) registrado con modo 'agent'.");
|
||||
$userChatMode = 'agent'; // Forzar modo agente para la primera interacción
|
||||
}
|
||||
|
||||
if (trim($content) === '/agente') {
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET chat_mode = 'agent' WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$userId]);
|
||||
$userChatMode = 'agent';
|
||||
$logger->info("[MODO AGENTE] Usuario $userId usó /agente. Reseteando a modo 'agent'.");
|
||||
}
|
||||
|
||||
switch ($userChatMode) {
|
||||
case 'agent':
|
||||
$builder = MessageBuilder::new()->setContent("👋 Hola! ¿Cómo quieres interactuar?");
|
||||
$actionRow = ActionRow::new()
|
||||
->addComponent(Button::new(Button::STYLE_PRIMARY, 'platicar_bot')->setLabel('🤖 Platicar con bot'))
|
||||
->addComponent(Button::new(Button::STYLE_SUCCESS, 'usar_ia')->setLabel('🧠 Usar IA'));
|
||||
$builder->addComponent($actionRow);
|
||||
$message->channel->sendMessage($builder);
|
||||
return;
|
||||
|
||||
case 'ia':
|
||||
$logger->info("[MODO IA] Mensaje de $userId para la IA: $content");
|
||||
$n8nWebhookUrl = $_ENV['N8N_IA_WEBHOOK_URL_DISCORD'] ?? null;
|
||||
if ($n8nWebhookUrl) {
|
||||
$postData = [
|
||||
'chat_id' => $message->channel_id,
|
||||
'user_id' => $userId,
|
||||
'message' => $content,
|
||||
'name' => $message->author->username
|
||||
];
|
||||
$ch = curl_init($n8nWebhookUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
$logger->info("[MODO IA] Mensaje reenviado a n8n.");
|
||||
} else {
|
||||
$logger->error("[MODO IA] La variable N8N_IA_WEBHOOK_URL no está configurada.");
|
||||
}
|
||||
return;
|
||||
|
||||
case 'bot':
|
||||
// Continuar con la lógica normal de bot abajo
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- LÓGICA DE BOT NORMAL (CHATS PÚBLICOS O MODO 'bot') ---
|
||||
if (strpos($content, '/') === 0 || strpos($content, '#') === 0) {
|
||||
handleDiscordCommand($message, $pdo, $logger);
|
||||
} else if (strtolower($content) === '!ping') {
|
||||
$message->reply('pong!');
|
||||
} else {
|
||||
handleDiscordTranslation($message, $pdo, $logger);
|
||||
}
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("!!!!!!!!!! ERROR FATAL CAPTURADO !!!!!!!!!!", [
|
||||
'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$discord->run();
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$logger->critical("!!!!!!!!!! ERROR FATAL AL INICIAR !!!!!!!!!!", [
|
||||
'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()
|
||||
]);
|
||||
die();
|
||||
}
|
||||
|
||||
function handleDiscordCommand(Message $message, PDO $pdo, Logger $logger)
|
||||
{
|
||||
$text = trim($message->content);
|
||||
$userId = $message->author->id;
|
||||
|
||||
if (strpos($text, '#') === 0) {
|
||||
$command = ltrim($text, '#');
|
||||
$logger->info("[Comando] Usuario @{$message->author->username} solicitó: #{$command}");
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT message_content FROM recurrent_messages WHERE telegram_command = ?");
|
||||
$stmt->execute([$command]);
|
||||
$template = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($template) {
|
||||
$logger->info("[Comando] Plantilla encontrada para #{$command}.");
|
||||
$originalHtml = $template['message_content'];
|
||||
|
||||
$sender = new DiscordSender(DISCORD_BOT_TOKEN);
|
||||
$sentMessageIds = $sender->sendMessage($message->channel_id, $originalHtml);
|
||||
|
||||
if ($sentMessageIds && !empty($sentMessageIds)) {
|
||||
$firstMessageId = $sentMessageIds[0];
|
||||
$logger->info("Mensajes enviados, primer ID: {$firstMessageId}");
|
||||
|
||||
// Añadir botones de traducción dinámicos
|
||||
$message->channel->messages->fetch($firstMessageId)->done(function (Message $sentMessage) use ($pdo, $logger, $command) {
|
||||
$langStmt = $pdo->query("SELECT language_code, language_name, flag_emoji FROM supported_languages WHERE is_active = 1");
|
||||
$activeLangs = $langStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($activeLangs) > 1) {
|
||||
$components = [];
|
||||
$actionRow = ActionRow::new();
|
||||
$buttonCount = 0;
|
||||
|
||||
foreach ($activeLangs as $lang) {
|
||||
// Para plantillas, incluir el comando en el custom_id para traducir el contenido completo original
|
||||
$button = Button::new(Button::STYLE_SECONDARY, 'translate_template:' . $command . ':' . $lang['language_code'])
|
||||
->setLabel($lang['language_name']);
|
||||
if (!empty($lang['flag_emoji'])) {
|
||||
$button->setEmoji($lang['flag_emoji']);
|
||||
}
|
||||
$actionRow->addComponent($button);
|
||||
$buttonCount++;
|
||||
|
||||
// Discord permite máximo 5 botones por ActionRow
|
||||
if ($buttonCount % 5 === 0) {
|
||||
$components[] = $actionRow;
|
||||
$actionRow = ActionRow::new();
|
||||
}
|
||||
}
|
||||
if ($buttonCount % 5 !== 0) {
|
||||
$components[] = $actionRow;
|
||||
}
|
||||
|
||||
$builder = MessageBuilder::new()->setComponents($components);
|
||||
$sentMessage->edit($builder);
|
||||
$logger->info("Botones de traducción añadidos al mensaje {$sentMessage->id}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
$message->reply("El comando `#{$command}` no fue encontrado.");
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("[Error Comando] Procesando #{$command}", ['error' => $e->getMessage()]);
|
||||
$message->reply("Ocurrió un error inesperado al procesar tu comando.");
|
||||
}
|
||||
} elseif (strpos($text, '/setlang') === 0) {
|
||||
$parts = explode(' ', $text, 2);
|
||||
if (count($parts) < 2 || strlen(trim($parts[1])) !== 2) {
|
||||
$message->reply("❌ Formato incorrecto. Usa: `/setlang es`");
|
||||
return;
|
||||
}
|
||||
$newLangCode = strtolower(trim($parts[1]));
|
||||
$stmt = $pdo->prepare("UPDATE recipients SET language_code = ? WHERE platform_id = ? AND platform = 'discord'");
|
||||
$stmt->execute([$newLangCode, $userId]);
|
||||
$message->reply("✅ Tu idioma ha sido establecido a '" . strtoupper($newLangCode) . "'.");
|
||||
|
||||
} elseif (strpos($text, '/bienvenida') === 0) {
|
||||
$configStmt = $pdo->query("SELECT * FROM telegram_bot_messages WHERE id = 1");
|
||||
$welcomeConfig = $configStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($welcomeConfig && $welcomeConfig['is_active']) {
|
||||
$messageText = str_replace('{user_name}', $message->author->username, $welcomeConfig['message_text']);
|
||||
$converter = new HtmlToDiscordMarkdownConverter();
|
||||
$markdownText = $converter->convert($messageText);
|
||||
$builder = MessageBuilder::new()->setContent($markdownText);
|
||||
$actionRow = ActionRow::new()
|
||||
->addComponent(Button::new(Button::STYLE_LINK, $welcomeConfig['group_invite_link'])->setLabel($welcomeConfig['button_text']));
|
||||
$builder->addComponent($actionRow);
|
||||
$message->channel->sendMessage($builder);
|
||||
} else {
|
||||
$message->reply("ℹ️ La función de bienvenida no está activa.");
|
||||
}
|
||||
|
||||
} elseif (strpos($text, '/comandos') === 0) {
|
||||
$stmt = $pdo->query("SELECT telegram_command, name FROM recurrent_messages WHERE telegram_command IS NOT NULL AND telegram_command != '' ORDER BY name ASC");
|
||||
$commands = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (empty($commands)) {
|
||||
$message->reply("ℹ️ No hay comandos personalizados disponibles.");
|
||||
} else {
|
||||
$response = "**LISTA DE COMANDOS DISPONIBLES**\n\n";
|
||||
foreach ($commands as $cmd) {
|
||||
$command = trim($cmd['telegram_command']);
|
||||
if (strpos($command, '#') !== 0) $command = '#' . $command;
|
||||
$name = trim($cmd['name']);
|
||||
$response .= "`" . $command . "` - " . $name . "\n";
|
||||
}
|
||||
$response .= "\nℹ️ Escribe el comando para usarlo.";
|
||||
$message->channel->sendMessage($response);
|
||||
}
|
||||
|
||||
} elseif (strpos($text, '/agente') === 0) {
|
||||
$prompt = trim(substr($text, strlen('/agente')));
|
||||
$logger->info("[Comando] Usuario @{$message->author->username} solicitó: /agente con prompt: '{$prompt}'");
|
||||
|
||||
if (empty($prompt)) {
|
||||
$message->reply("Por favor, escribe tu consulta después de /agente.");
|
||||
return;
|
||||
}
|
||||
|
||||
$n8nWebhookUrl = $_ENV['N8N_IA_WEBHOOK_URL_DISCORD'] ?? null;
|
||||
if ($n8nWebhookUrl) {
|
||||
$postData = [
|
||||
'chat_id' => $message->channel_id,
|
||||
'user_id' => $message->author->id,
|
||||
'message' => $prompt,
|
||||
'name' => $message->author->username
|
||||
];
|
||||
$ch = curl_init($n8nWebhookUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$logger->info("[Comando /agente] Prompt reenviado a n8n.");
|
||||
$message->reply("Tu solicitud ha sido enviada al agente. Recibirás una respuesta en breve.");
|
||||
} else {
|
||||
$logger->error("[Comando /agente] La variable N8N_IA_WEBHOOK_URL no está configurada.");
|
||||
$message->reply("No se pudo procesar tu solicitud. El servicio de agente no está configurado.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscordTranslation(Message $message, PDO $pdo, Logger $logger)
|
||||
{
|
||||
try {
|
||||
$translator = new Translate();
|
||||
|
||||
$text = $message->content;
|
||||
$detectedLang = $translator->detectLanguage(strip_tags($text)) ?? 'es';
|
||||
|
||||
// Obtener idiomas activos y encolar traducciones
|
||||
$langStmt = $pdo->query("SELECT language_code FROM supported_languages WHERE is_active = 1");
|
||||
$activeLangs = $langStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (in_array($detectedLang, $activeLangs)) {
|
||||
foreach ($activeLangs as $targetLang) {
|
||||
if ($detectedLang === $targetLang) {
|
||||
continue; // No traducir al mismo idioma
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO translation_queue (platform, message_id, chat_id, user_id, text_to_translate, source_lang, target_lang) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
'discord',
|
||||
$message->id,
|
||||
$message->channel_id,
|
||||
$message->author->id,
|
||||
$text,
|
||||
$detectedLang,
|
||||
$targetLang
|
||||
]);
|
||||
|
||||
$logger->info("[QUEUE] Mensaje de Discord #{$message->id} encolado para traducción de '$detectedLang' a '$targetLang'.");
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$logger->error("[Error Encolando Traducción Discord]", ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||
}
|
||||
}
|
||||
84
docker/Dockerfile
Executable file
@@ -0,0 +1,84 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
LABEL maintainer="bot-lastwar"
|
||||
LABEL description="Bot LastWar - Discord/Telegram Bot with Web Interface"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
apache2 \
|
||||
php \
|
||||
php-cli \
|
||||
php-mysql \
|
||||
php-mbstring \
|
||||
php-xml \
|
||||
php-curl \
|
||||
php-zip \
|
||||
php-json \
|
||||
libapache2-mod-php \
|
||||
composer \
|
||||
nodejs \
|
||||
npm \
|
||||
supervisor \
|
||||
curl \
|
||||
git \
|
||||
unzip \
|
||||
nano \
|
||||
libzip-dev \
|
||||
bash \
|
||||
bash-completion \
|
||||
readline-common \
|
||||
less \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN a2enmod rewrite headers env
|
||||
|
||||
RUN npm install -g yarn
|
||||
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
|
||||
RUN mkdir -p /var/www/html/bot/logs \
|
||||
&& mkdir -p /var/www/html/bot/galeria \
|
||||
&& mkdir -p /var/run/supervisor \
|
||||
&& mkdir -p /etc/supervisor/conf.d
|
||||
|
||||
COPY docker/apache/*.conf /etc/apache2/sites-available/
|
||||
COPY docker/supervisor/*.conf /etc/supervisor/conf.d/
|
||||
|
||||
RUN a2ensite reod-dragon.ddns.net.conf || true
|
||||
RUN a2dissite 000-default.conf || true
|
||||
|
||||
WORKDIR /var/www/html/bot
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
COPY docker/zimaos.env.template /var/www/html/bot/docker/
|
||||
|
||||
COPY . . /var/www/html/bot/
|
||||
|
||||
# Eliminar archivos .env de desarrollo que vienen del código fuente
|
||||
# En Docker usaremos el archivo .env generado desde variables de entorno
|
||||
RUN rm -f /var/www/html/bot/.env.reod /var/www/html/bot/.env.pruebas /var/www/html/bot/.env.example 2>/dev/null || true
|
||||
|
||||
RUN rm -rf /var/www/html/bot/docker/Dockerfile \
|
||||
/var/www/html/bot/docker-compose*.yml 2>/dev/null || true
|
||||
|
||||
RUN chmod -R 777 /var/www/html/bot/logs \
|
||||
/var/www/html/bot/galeria \
|
||||
/var/run/supervisor
|
||||
|
||||
RUN touch /var/log/apache2/error.log \
|
||||
/var/log/apache2/access.log \
|
||||
&& chmod 666 /var/log/apache2/*.log
|
||||
|
||||
# Set index.php as first DirectoryIndex
|
||||
RUN cat > /etc/apache2/mods-enabled/dir.conf << 'EOF'
|
||||
<IfModule mod_dir.c>
|
||||
DirectoryIndex index.php index.html index.cgi index.pl index.xhtml index.htm
|
||||
</IfModule>
|
||||
EOF
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
||||
165
docker/README-ZimaOS.md
Executable file
@@ -0,0 +1,165 @@
|
||||
# Docker Configuration for ZimaOS
|
||||
|
||||
## Archivos Incluidos
|
||||
|
||||
```
|
||||
docker/
|
||||
├── Dockerfile # Imagen base Ubuntu 22.04 (incluye todo el proyecto)
|
||||
├── docker-compose.yml # Desarrollo local
|
||||
├── docker-compose.local.yml # Pruebas locales con volumen
|
||||
├── zimaos-docker-compose.yml # Producción ZimaOS
|
||||
├── zimaos.env.template # Template de variables
|
||||
├── entrypoint.sh # Script de inicio (genera .env)
|
||||
├── supervisord.conf # Configuración Supervisor
|
||||
├── supervisor/
|
||||
│ ├── discordbot.conf # Bot de Discord
|
||||
│ └── translation-worker.conf # Worker de traducciones
|
||||
├── apache/
|
||||
│ ├── ponsprueba.ddns.net.conf # Apache desarrollo
|
||||
│ └── reod-dragon.ddns.net.conf # Apache producción
|
||||
└── www/
|
||||
└── index.html # Página de bienvenida
|
||||
```
|
||||
|
||||
## Uso en ZimaOS
|
||||
|
||||
### 1. Preparar Archivos
|
||||
|
||||
Copia `zimaos-docker-compose.yml` a tu almacenamiento en ZimaOS.
|
||||
|
||||
### 2. Configurar Variables
|
||||
|
||||
Edita `zimaos-docker-compose.yml` y ajusta las variables de entorno:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- DB_HOST=tu_host_db
|
||||
- DB_PORT=tu_puerto
|
||||
- DB_NAME=tu_db
|
||||
- DB_USER=tu_usuario
|
||||
- DB_PASS=tu_password
|
||||
|
||||
- DISCORD_BOT_TOKEN=tu_token
|
||||
- TELEGRAM_BOT_TOKEN=tu_token
|
||||
|
||||
- APP_BASE_URL=http://tu-dominio:8086
|
||||
# ... otras variables
|
||||
```
|
||||
|
||||
### 3. Ejecutar
|
||||
|
||||
```bash
|
||||
# Descargar imagen
|
||||
docker compose -f zimaos-docker-compose.yml pull
|
||||
|
||||
# Iniciar contenedor
|
||||
docker compose -f zimaos-docker-compose.yml up -d
|
||||
|
||||
# Ver logs
|
||||
docker logs -f bot_lastwar_funcionando
|
||||
```
|
||||
|
||||
### 4. Detener
|
||||
|
||||
```bash
|
||||
docker compose -f zimaos-docker-compose.yml down
|
||||
```
|
||||
|
||||
## Actualizar a nueva versión
|
||||
|
||||
```bash
|
||||
docker compose -f zimaos-docker-compose.yml pull
|
||||
docker compose -f zimaos-docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Variables de Entorno
|
||||
|
||||
| Variable | Descripción | Ejemplo |
|
||||
|----------|-------------|---------|
|
||||
| `APP_ENVIRONMENT` | Entorno (pruebas\|reod) | pruebas |
|
||||
| `USE_LOCALHOST` | Usar URLs localhost | false |
|
||||
| `DB_*` | Configuración base de datos | Ver template |
|
||||
| `DISCORD_*` | Credenciales Discord | Ver template |
|
||||
| `TELEGRAM_*` | Credenciales Telegram | Ver template |
|
||||
| `N8N_*` | URLs de N8N | Ver template |
|
||||
| `JWT_SECRET` | Secreto para sesiones | `openssl rand -hex 32` |
|
||||
| `INTERNAL_API_KEY` | API key interna | `openssl rand -hex 32` |
|
||||
| `APP_BASE_URL` | URL base pública | http://tu-ip:8086 |
|
||||
|
||||
## Desarrollo Local
|
||||
|
||||
Para desarrollar localmente con volúmenes (los cambios se ven sin rebuild):
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml up -d
|
||||
```
|
||||
|
||||
Para producción (imagen autocontenida):
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml build
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Puertos
|
||||
|
||||
- **8086** → Puerto HTTP del contenedor (80)
|
||||
|
||||
## Servicios
|
||||
|
||||
- **Apache2** - Servidor web
|
||||
- **Discord Bot** - Bot de Discord
|
||||
- **Translation Worker** - Procesador de traducciones
|
||||
|
||||
## Volúmenes Persistentes
|
||||
|
||||
Los volúmenes para logs y galería se crean automáticamente con Docker:
|
||||
|
||||
- `bot-logs` → /var/www/html/bot/logs
|
||||
- `bot-galeria` → /var/www/html/bot/galeria
|
||||
|
||||
Para verlos:
|
||||
```bash
|
||||
docker volume ls | grep bot
|
||||
```
|
||||
|
||||
## Solución de Problemas
|
||||
|
||||
### El contenedor no inicia
|
||||
|
||||
```bash
|
||||
docker logs bot_lastwar_funcionando
|
||||
```
|
||||
|
||||
### Verificar variables de entorno
|
||||
|
||||
```bash
|
||||
docker exec bot_lastwar_funcionando env | grep -E "DB_|DISCORD_|TELEGRAM_|APP_"
|
||||
```
|
||||
|
||||
### Verificar archivo .env generado
|
||||
|
||||
```bash
|
||||
docker exec bot_lastwar_funcionando cat /var/www/html/bot/.env
|
||||
```
|
||||
|
||||
### Base de datos no conecta
|
||||
|
||||
Verificar credenciales en variables de entorno.
|
||||
|
||||
### Reiniciar contenedor
|
||||
|
||||
```bash
|
||||
docker restart bot_lastwar_funcionando
|
||||
```
|
||||
|
||||
## Construcción de imagen (para actualizar)
|
||||
|
||||
```bash
|
||||
# En la carpeta del proyecto
|
||||
docker compose build
|
||||
|
||||
# Subir al registry
|
||||
docker tag bot-bot-lastwar:latest 10.10.4.3:5000/bot-lastwar:latest
|
||||
docker push 10.10.4.3:5000/bot-lastwar:latest
|
||||
```
|
||||
72
docker/README.md
Executable file
@@ -0,0 +1,72 @@
|
||||
# Docker Configuration for Bot LastWar
|
||||
|
||||
## Estructura de Archivos
|
||||
|
||||
```
|
||||
docker/
|
||||
├── Dockerfile # Imagen base Ubuntu con Apache, PHP, Composer, Node, Supervisor
|
||||
├── docker-compose.yml # Orquestación del contenedor
|
||||
├── entrypoint.sh # Script de inicio
|
||||
├── supervisord.conf # Configuración principal de Supervisor
|
||||
├── supervisor/
|
||||
│ ├── discordbot.conf # Proceso del bot de Discord
|
||||
│ └── translation-worker.conf # Worker de traducciones
|
||||
├── apache/
|
||||
│ ├── ponsprueba.ddns.net.conf # Configuración Apache desarrollo
|
||||
│ └── reod-dragon.ddns.net.conf # Configuración Apache producción
|
||||
├── www/
|
||||
│ └── index.html # Página de bienvenida
|
||||
└── zimaos.env.template # Template de variables para ZimaOS
|
||||
```
|
||||
|
||||
## Configuración para ZimaOS
|
||||
|
||||
1. Copia `docker/zimaos.env.template` a `.env` en ZimaOS
|
||||
2. Ajusta las variables según tu red
|
||||
3. La variable clave es `APP_ENVIRONMENT`:
|
||||
- `pruebas` → usa `.env.pruebas`
|
||||
- `reod` → usa `.env.reod`
|
||||
|
||||
## Construcción y Ejecución
|
||||
|
||||
```bash
|
||||
# Construir la imagen
|
||||
docker-compose build
|
||||
|
||||
# Ejecutar el contenedor
|
||||
docker-compose up -d
|
||||
|
||||
# Ver logs
|
||||
docker logs bot_lastwar_funcionando
|
||||
|
||||
# Detener
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Puertos
|
||||
|
||||
- **8086 (host)** → **80 (contenedor)**
|
||||
- Accede a: http://localhost:8086
|
||||
|
||||
## Servicios Incluidos
|
||||
|
||||
- **Apache2** - Servidor web en puerto 80
|
||||
- **PHP** - Interpreter para la aplicación
|
||||
- **Composer** - Gestor de dependencias PHP
|
||||
- **Node.js** - Runtime JavaScript
|
||||
- **Supervisor** - Gestión de procesos (bot + worker)
|
||||
- **Discord Bot** - Bot de Discord (configurable via APP_ENVIRONMENT)
|
||||
- **Translation Worker** - Procesador de cola de traducciones
|
||||
|
||||
## Variables de Entorno
|
||||
|
||||
| Variable | Descripción | Default |
|
||||
|----------|-------------|---------|
|
||||
| `APP_ENVIRONMENT` | Entorno (pruebas\|reod) | pruebas |
|
||||
| `TZ` | Zona horaria | UTC |
|
||||
|
||||
## Notas
|
||||
|
||||
- Los logs se almacenan en `./logs/`
|
||||
- La galería de imágenes está en `./galeria/`
|
||||
- El contenedor copia automáticamente el .env correspondiente según `APP_ENVIRONMENT`
|
||||
21
docker/apache/reod-dragon.ddns.net.conf
Executable file
@@ -0,0 +1,21 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName reod-dragon.ddns.net
|
||||
SetEnv APP_ENVIRONMENT "reod"
|
||||
SetEnv DOCKER_CONTAINER "1"
|
||||
|
||||
# Content Security Policy para permitir jsdelivr CDN
|
||||
Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://code.jquery.com 'unsafe-inline'; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; connect-src 'self' https://cdn.jsdelivr.net; img-src 'self' data: https:; font-src 'self' https://cdn.jsdelivr.net data:;"
|
||||
|
||||
ErrorLog /var/www/html/bot/logs/reod_error.log
|
||||
CustomLog /var/www/html/bot/logs/reod_access.log combined
|
||||
|
||||
DocumentRoot /var/www/html/bot/
|
||||
<Directory /var/www/html/bot/>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
php_admin_value upload_max_filesize 20M
|
||||
php_admin_value post_max_size 20M
|
||||
</VirtualHost>
|
||||
38
docker/deploy.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================
|
||||
# Script para construir y subir imagen Docker
|
||||
# Target: 10.10.4.3:5000/bot-lastwar:v2
|
||||
# ============================================
|
||||
|
||||
set -e
|
||||
|
||||
# Cambiar al directorio del proyecto (carpeta padre de donde está este script)
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "=========================================="
|
||||
echo "Construyendo imagen Docker..."
|
||||
echo "Directorio: $(pwd)"
|
||||
echo "=========================================="
|
||||
|
||||
# Construir la imagen usando el Dockerfile en docker/
|
||||
docker build -t bot-lastwar:latest -f docker/Dockerfile .
|
||||
|
||||
echo "=========================================="
|
||||
echo "Etiquetando imagen para el registry..."
|
||||
echo "=========================================="
|
||||
|
||||
# Etiquetar la imagen con la versión v2
|
||||
docker tag bot-lastwar:latest 10.10.4.3:5000/bot-lastwar:v2
|
||||
|
||||
echo "=========================================="
|
||||
echo "Subiendo imagen al registry..."
|
||||
echo "=========================================="
|
||||
|
||||
# Subir al registry
|
||||
docker push 10.10.4.3:5000/bot-lastwar:v2
|
||||
|
||||
echo "=========================================="
|
||||
echo "Imagen subida exitosamente:"
|
||||
echo "10.10.4.3:5000/bot-lastwar:v2"
|
||||
echo "=========================================="
|
||||
60
docker/diagnostico_zimaos.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
# Comandos para diagnosticar problemas en ZimaOS
|
||||
# Copia y pega estos comandos en la terminal de ZimaOS
|
||||
|
||||
# 1. Ver logs del discordbot
|
||||
docker exec bot_lastwar_funcionando cat /var/www/html/bot/logs/discordbot.out.log
|
||||
|
||||
# 2. Ver logs del discordbot (errores)
|
||||
docker exec bot_lastwar_funcionando cat /var/www/html/bot/logs/discordbot.err.log
|
||||
|
||||
# 3. Ver logs del translation-worker
|
||||
docker exec bot_lastwar_funcionando cat /var/www/html/bot/logs/translation-worker.out.log
|
||||
|
||||
# 4. Verificar si el archivo discord_bot.php existe
|
||||
docker exec bot_lastwar_funcionando ls -la /var/www/html/bot/discord_bot.php
|
||||
|
||||
# 5. Verificar el .env generado
|
||||
docker exec bot_lastwar_funcionando cat /var/www/html/bot/.env
|
||||
|
||||
# 6. Verificar si los archivos del proyecto están montados
|
||||
docker exec bot_lastwar_funcionando ls -la /var/www/html/bot/ | head -20
|
||||
|
||||
# 7. Verificar permisos de la carpeta logs
|
||||
docker exec bot_lastwar_funcionando ls -la /var/www/html/bot/logs/
|
||||
|
||||
# 8. Ver todos los procesos en ejecución
|
||||
docker exec bot_lastwar_funcionando ps aux
|
||||
|
||||
# 9. Ver estado de supervisor
|
||||
docker exec bot_lastwar_funcionando supervisorctl status
|
||||
|
||||
# ==== DIAGNÓSTICO DE APACHE ====
|
||||
# 10. Ver si Apache está escuchando en puerto 80
|
||||
docker exec bot_lastwar_funcionando netstat -tulpn | grep :80
|
||||
|
||||
# 11. Ver procesos de Apache
|
||||
docker exec bot_lastwar_funcionando ps aux | grep apache
|
||||
|
||||
# 12. Ver log de error de Apache
|
||||
docker exec bot_lastwar_funcionando cat /var/log/apache2/error.log
|
||||
|
||||
# 13. Ver log de error del sitio
|
||||
docker exec bot_lastwar_funcionando cat /var/www/html/bot/logs/apache2.err.log
|
||||
|
||||
# 14. Probar Apache directamente dentro del contenedor
|
||||
docker exec bot_lastwar_funcionando curl -v http://localhost:80/ 2>&1 | head -20
|
||||
|
||||
# 15. Ver configuración de Apache
|
||||
docker exec bot_lastwar_funcionando apache2ctl -S 2>&1
|
||||
|
||||
# 16. Ver módulos de Apache cargados
|
||||
docker exec bot_lastwar_funcionando apache2ctl -M 2>&1 | grep php
|
||||
|
||||
# 17. Verificar si hay algo escuchando en el puerto publicado
|
||||
netstat -tulpn | grep 8086
|
||||
|
||||
# 18. Ver logs del contenedor
|
||||
docker logs bot_lastwar_funcionando 2>&1 | tail -30
|
||||
|
||||
# 19. Reiniciar el contenedor (si necesitas hacer cambios)
|
||||
docker restart bot_lastwar_funcionando
|
||||
20
docker/docker-compose.local.yml
Executable file
@@ -0,0 +1,20 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot-lastwar:
|
||||
container_name: bot_lastwar_funcionando
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
ports:
|
||||
- "8086:80"
|
||||
environment:
|
||||
- APP_ENVIRONMENT=pruebas
|
||||
- USE_LOCALHOST=true
|
||||
- TZ=UTC
|
||||
volumes:
|
||||
- .:/var/www/html/bot:rw
|
||||
- ./docker/supervisor/supervisord.conf:/etc/supervisor/supervisord.conf:ro
|
||||
working_dir: /var/www/html/bot
|
||||
restart: unless-stopped
|
||||
command: ["/entrypoint.sh"]
|
||||
66
docker/entrypoint.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "Bot LastWar - Docker Startup Script"
|
||||
echo "=========================================="
|
||||
|
||||
ENVIRONMENT=${APP_ENVIRONMENT:-reod}
|
||||
echo "Environment: $ENVIRONMENT"
|
||||
|
||||
echo "Generando archivo .env desde variables de entorno..."
|
||||
|
||||
# Eliminar todos los archivos .env existentes para evitar conflictos
|
||||
rm -f /var/www/html/bot/.env* 2>/dev/null || true
|
||||
|
||||
env | grep -E "^(DB_|JWT_|DISCORD_|TELEGRAM_|LIBRETRANSLATE_|N8N_|APP_|INTERNAL_API_KEY|TEST_ENV_LOAD)" > /tmp/env_vars.txt
|
||||
|
||||
# Determinar el nombre del archivo .env según el entorno
|
||||
if [ "$ENVIRONMENT" = "reod" ]; then
|
||||
ENV_FILE="/var/www/html/bot/.env"
|
||||
else
|
||||
ENV_FILE="/var/www/html/bot/.env.${ENVIRONMENT}"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Auto-generado desde variables de entorno"
|
||||
echo "# NO editar manualmente - los cambios se perderán al reiniciar"
|
||||
echo ""
|
||||
while IFS='=' read -r name value; do
|
||||
[ -z "$name" ] && continue
|
||||
# Renombrar APP_BASE_URL a APP_URL para compatibilidad con el código
|
||||
if [ "$name" = "APP_BASE_URL" ]; then
|
||||
echo "APP_URL=${value}"
|
||||
else
|
||||
echo "${name}=${value}"
|
||||
fi
|
||||
done < /tmp/env_vars.txt
|
||||
} > "$ENV_FILE"
|
||||
|
||||
rm -f /tmp/env_vars.txt
|
||||
|
||||
echo "Archivo .env generado"
|
||||
|
||||
if [ -f /var/www/html/bot/composer.json ]; then
|
||||
echo "Instalando dependencias de Composer..."
|
||||
composer install --no-interaction --optimize-autoloader --no-dev 2>/dev/null || composer install --no-interaction
|
||||
fi
|
||||
|
||||
mkdir -p /var/www/html/bot/logs 2>/dev/null || true
|
||||
mkdir -p /var/www/html/bot/galeria 2>/dev/null || true
|
||||
mkdir -p /var/run/supervisor 2>/dev/null || true
|
||||
chmod -R 777 /var/www/html/bot/logs /var/log/apache2 2>/dev/null || true
|
||||
touch /var/log/apache2/error.log 2>/dev/null || true
|
||||
touch /var/log/apache2/access.log 2>/dev/null || true
|
||||
chmod 666 /var/log/apache2/*.log 2>/dev/null || true
|
||||
chown -R www-data:www-data /var/www/html/bot/logs /var/log/apache2 2>/dev/null || true
|
||||
|
||||
echo "Configurando sitio Apache..."
|
||||
if [ "$ENVIRONMENT" = "reod" ]; then
|
||||
a2ensite reod-dragon.ddns.net.conf 2>/dev/null || true
|
||||
else
|
||||
a2dissite reod-dragon.ddns.net.conf 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Iniciando Supervisor..."
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
||||
9
docker/supervisor/discordbot.conf
Executable file
@@ -0,0 +1,9 @@
|
||||
[program:discordbot]
|
||||
command=/usr/bin/php /var/www/html/bot/discord_bot.php
|
||||
directory=/var/www/html/bot/
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/www/html/bot/logs/discordbot.err.log
|
||||
stdout_logfile=/var/www/html/bot/logs/discordbot.out.log
|
||||
user=root
|
||||
redirect_stderr=false
|
||||
17
docker/supervisor/supervisord.conf
Executable file
@@ -0,0 +1,17 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/var/www/html/bot/logs/supervisor.log
|
||||
loglevel=info
|
||||
pidfile=/var/run/supervisor/supervisord.pid
|
||||
childlogdir=/var/www/html/bot/logs
|
||||
user=root
|
||||
|
||||
[program:apache2]
|
||||
command=/bin/bash -c "source /etc/apache2/envvars && exec apache2 -D FOREGROUND"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/www/html/bot/logs/apache2.out.log
|
||||
stderr_logfile=/var/www/html/bot/logs/apache2.err.log
|
||||
|
||||
[include]
|
||||
files=/etc/supervisor/conf.d/*.conf
|
||||
19
docker/supervisor/translation-worker.conf
Executable file
@@ -0,0 +1,19 @@
|
||||
[program:translation-worker]
|
||||
command=/usr/bin/php /var/www/html/bot/process_translation_queue.php
|
||||
directory=/var/www/html/bot/
|
||||
user=root
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/www/html/bot/logs/translation-worker.out.log
|
||||
stderr_logfile=/var/www/html/bot/logs/translation-worker.err.log
|
||||
redirect_stderr=true
|
||||
|
||||
[program:queue-processor]
|
||||
command=/bin/bash -c "while true; do sleep 60; /usr/bin/php /var/www/html/bot/process_queue.php; done"
|
||||
directory=/var/www/html/bot/
|
||||
user=root
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/www/html/bot/logs/queue-processor.out.log
|
||||
stderr_logfile=/var/www/html/bot/logs/queue-processor.err.log
|
||||
redirect_stderr=true
|
||||
73
docker/zimaos-docker-compose-corrected.yml
Executable file
@@ -0,0 +1,73 @@
|
||||
name: bot-lastwar
|
||||
services:
|
||||
bot-lastwar:
|
||||
cpu_shares: 90
|
||||
command:
|
||||
- /entrypoint.sh
|
||||
container_name: bot_lastwar_funcionando
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 16663138304
|
||||
reservations:
|
||||
devices: []
|
||||
environment:
|
||||
- APP_BASE_URL=https://reod-dragon.ddns.net
|
||||
- APP_ENV=production
|
||||
- APP_ENVIRONMENT=reod
|
||||
- APP_URL=https://reod-dragon.ddns.net
|
||||
- DB_DIALECT=mysql
|
||||
- DB_HOST=10.10.4.17
|
||||
- DB_NAME=bot
|
||||
- DB_PASS=MiPo6425@@
|
||||
- DB_PORT=3390
|
||||
- DB_USER=nickpons666
|
||||
- DISCORD_BOT_TOKEN=MTM4NTc5MDM0NDU5NDk4NTA2MQ.GvobiS.TRQM9dX7vDjmuGVa3Ckp6YRtGEWxdW0gBDbvCI
|
||||
- DISCORD_CLIENT_ID=1385790344594985061
|
||||
- DISCORD_CLIENT_SECRET=hK9SNiYdenHQVxakt8Mx3RoMkZ5oOJvk
|
||||
- DISCORD_GUILD_ID=1338327171013541999
|
||||
- INTERNAL_API_KEY=b5dda33b8eb062e06e100c98a8947c0248b6e38973dfd689e81f725af238d23c
|
||||
- JWT_ALGORITHM=HS256
|
||||
- JWT_EXPIRATION=3600
|
||||
- JWT_SECRET=19c5020fa8207d2c3b9e82f430784667e001f1eb733848922f7bcb9be98f93c2
|
||||
- LIBRETRANSLATE_URL=http://10.10.4.17:5000
|
||||
- N8N_IA_WEBHOOK_URL=https://n8n-dragon.ddns.net/webhook/ia
|
||||
- N8N_IA_WEBHOOK_URL_DISCORD=https://n8n-dragon.ddns.net/webhook/42e803ae-8aee-4b1c-858a-6c6d3fbb6230
|
||||
- N8N_PROCESS_QUEUE_WEBHOOK_URL=https://n8n-dragon.ddns.net/webhook/telegram-unified
|
||||
- N8N_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4MWY4YjU3YS0wMTg2LTQ1NTctOWZlMC1jYWUxNjZlYzZlMTkiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzU1OTMwODM5fQ.2tLbddyhMTKplp9n-qVNiAgQCUj2YEvVASwLnNjgCt0
|
||||
- N8N_URL=https://n8n-dragon.ddns.net
|
||||
- TELEGRAM_BOT_TOKEN=8469229183:AAEVIV5e7rjDXKNgFTX0dnCW6JWB88X4p2I
|
||||
- TELEGRAM_WEBHOOK_TOKEN=webhook_secure_token_12345
|
||||
- TEST_ENV_LOAD=caos_cargado
|
||||
- TZ=America/Mexico_City
|
||||
- USE_LOCALHOST=false
|
||||
image: 10.10.4.3:5000/bot-lastwar:latest
|
||||
labels:
|
||||
icon: https://icon.casaos.io/main/all/bot-lastwar.png
|
||||
ports:
|
||||
- target: 80
|
||||
published: "8086"
|
||||
protocol: tcp
|
||||
restart: unless-stopped
|
||||
user: 0:0
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /media/ZimaOS-HD/AppData/bot_lastwar/logs
|
||||
target: /var/www/html/bot/logs
|
||||
devices: []
|
||||
cap_add: []
|
||||
network_mode: bridge
|
||||
privileged: false
|
||||
x-casaos:
|
||||
author: self
|
||||
category: self
|
||||
hostname: ""
|
||||
icon: https://icon.casaos.io/main/all/bot-lastwar.png
|
||||
index: /
|
||||
is_uncontrolled: false
|
||||
port_map: "8086"
|
||||
scheme: http
|
||||
store_app_id: bot-lastwar
|
||||
title:
|
||||
custom: ""
|
||||
en_us: bot-lastwar
|
||||
77
docker/zimaos-docker-compose.yml
Executable file
@@ -0,0 +1,77 @@
|
||||
# ZimaOS Docker Compose Configuration for Bot LastWar
|
||||
# Contenedor: bot_lastwar_funcionando
|
||||
# Puerto: 8086 -> 80
|
||||
#
|
||||
# INSTRUCCIONES PARA ZIMAOS:
|
||||
# 1. Copia este archivo en ZimaOS
|
||||
# 2. Ajusta todas las variables de entorno según tu entorno
|
||||
# 3. La imagen ya contiene todos los archivos del proyecto
|
||||
# 4. Ejecuta: docker compose -f zimaos-docker-compose.yml up -d
|
||||
#
|
||||
# La imagen se descarga automáticamente del registry: 10.10.4.3:5000/bot-lastwar:latest
|
||||
|
||||
services:
|
||||
bot-lastwar:
|
||||
image: 10.10.4.3:5000/bot-lastwar:latest
|
||||
container_name: bot_lastwar_funcionando
|
||||
ports:
|
||||
- "8086:80"
|
||||
environment:
|
||||
# Configuración de la aplicación
|
||||
- APP_ENV=production
|
||||
- APP_ENVIRONMENT=reod
|
||||
- APP_DEBUG=false
|
||||
- USE_LOCALHOST=false
|
||||
- TZ=America/Mexico_City
|
||||
- APP_URL=https://reod-dragon.ddns.net
|
||||
|
||||
# Configuración de la base de datos
|
||||
- DB_HOST=10.10.4.17
|
||||
- DB_PORT=3390
|
||||
- DB_NAME=bot
|
||||
- DB_USER=nickpons666
|
||||
- DB_PASS=MiPo6425@@
|
||||
- DB_DIALECT=mysql
|
||||
|
||||
# Configuración de JWT
|
||||
- JWT_SECRET=19c5020fa8207d2c3b9e82f430784667e001f1eb733848922f7bcb9be98f93c2
|
||||
- JWT_ALGORITHM=HS256
|
||||
- JWT_EXPIRATION=3600
|
||||
|
||||
# Configuración de Discord
|
||||
- DISCORD_GUILD_ID=1338327171013541999
|
||||
- DISCORD_CLIENT_ID=1385790344594985061
|
||||
- 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
|
||||
- TEST_ENV_LOAD=caos_cargado
|
||||
|
||||
# LibreTranslate
|
||||
- LIBRETRANSLATE_URL=http://10.10.4.17:5000
|
||||
|
||||
# N8N Integration
|
||||
- N8N_URL=https://n8n-dragon.ddns.net
|
||||
- N8N_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4MWY4YjU3YS0wMTg2LTQ1NTctOWZlMC1jYWUxNjZlYzZlMTkiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzU1OTMwODM5fQ.2tLbddyhMTKplp9n-qVNiAgQCUj2YEvVASwLnNjgCt0
|
||||
|
||||
# Configuración para migración a n8n
|
||||
- APP_BASE_URL=https://reod-dragon.ddns.net
|
||||
- INTERNAL_API_KEY="b5dda33b8eb062e06e100c98a8947c0248b6e38973dfd689e81f725af238d23c"
|
||||
- N8N_PROCESS_QUEUE_WEBHOOK_URL="https://n8n-dragon.ddns.net/webhook/telegram-unified"
|
||||
- N8N_IA_WEBHOOK_URL="https://n8n-dragon.ddns.net/webhook/ia"
|
||||
- N8N_IA_WEBHOOK_URL_DISCORD="https://n8n-dragon.ddns.net/webhook/42e803ae-8aee-4b1c-858a-6c6d3fbb6230"
|
||||
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
command: ["/entrypoint.sh"]
|
||||
|
||||
volumes:
|
||||
# Volúmenes opcionales para persistencia (puedes quitarlos si no los necesitas)
|
||||
- bot-logs:/var/www/html/bot/logs
|
||||
- bot-galeria:/var/www/html/bot/galeria
|
||||
|
||||
volumes:
|
||||
bot-logs:
|
||||
bot-galeria:
|
||||
50
docker/zimaos.env.template
Executable file
@@ -0,0 +1,50 @@
|
||||
# ============================================
|
||||
# Plantilla de variables de entorno para Docker
|
||||
# Copiar este archivo a .env y configurar
|
||||
# ============================================
|
||||
|
||||
# Configuración de la aplicación
|
||||
APP_ENV=production
|
||||
APP_ENVIRONMENT=reod
|
||||
APP_DEBUG=false
|
||||
USE_LOCALHOST=false
|
||||
TZ=America/Mexico_City
|
||||
APP_URL=https://reod-dragon.ddns.net
|
||||
|
||||
# Configuración de la base de datos
|
||||
DB_HOST=10.10.4.17
|
||||
DB_PORT=3390
|
||||
DB_NAME=bot
|
||||
DB_USER=nickpons666
|
||||
DB_PASS=MiPo6425@@
|
||||
DB_DIALECT=mysql
|
||||
|
||||
# Configuración de JWT
|
||||
JWT_SECRET=19c5020fa8207d2c3b9e82f430784667e001f1eb733848922f7bcb9be98f93c2
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRATION=3600
|
||||
|
||||
# Configuración de Discord
|
||||
DISCORD_GUILD_ID=1338327171013541999
|
||||
DISCORD_CLIENT_ID=1385790344594985061
|
||||
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
|
||||
TEST_ENV_LOAD=caos_cargado
|
||||
|
||||
# LibreTranslate
|
||||
LIBRETRANSLATE_URL=http://10.10.4.17:5000
|
||||
|
||||
# N8N Integration
|
||||
N8N_URL=https://n8n-dragon.ddns.net
|
||||
N8N_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4MWY4YjU3YS0wMTg2LTQ1NTctOWZlMC1jYWUxNjZlYzZlMTkiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzU1OTMwODM5fQ.2tLbddyhMTKplp9n-qVNiAgQCUj2YEvVASwLnNjgCt0
|
||||
|
||||
# Configuración para migración a n8n
|
||||
APP_BASE_URL=https://reod-dragon.ddns.net
|
||||
INTERNAL_API_KEY=b5dda33b8eb062e06e100c98a8947c0248b6e38973dfd689e81f725af238d23c
|
||||
N8N_PROCESS_QUEUE_WEBHOOK_URL=https://n8n-dragon.ddns.net/webhook/telegram-unified
|
||||
N8N_IA_WEBHOOK_URL=https://n8n-dragon.ddns.net/webhook/ia
|
||||
N8N_IA_WEBHOOK_URL_DISCORD=https://n8n-dragon.ddns.net/webhook/42e803ae-8aee-4b1c-858a-6c6d3fbb6230
|
||||
93
downgrade_docker.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==============================================================================
|
||||
# Script para hacer Downgrade de Docker en sistemas basados en Debian/Ubuntu (v2)
|
||||
# ==============================================================================
|
||||
#
|
||||
# ADVERTENCIA: Este script desinstalará la versión actual de Docker
|
||||
# y eliminará los datos de Docker existentes (imágenes, contenedores, volúmenes).
|
||||
# Se recomienda hacer una copia de seguridad si tienes datos importantes.
|
||||
#
|
||||
# Uso:
|
||||
# 1. Guarda este script como 'downgrade_docker.sh'.
|
||||
# 2. Dale permisos de ejecución: chmod +x downgrade_docker.sh
|
||||
# 3. Ejecútalo con sudo: sudo ./downgrade_docker.sh
|
||||
#
|
||||
# ==============================================================================
|
||||
|
||||
set -e # Termina el script si algún comando falla
|
||||
|
||||
# --- 1. Definir la versión de Docker a instalar ---
|
||||
# Cambia esta variable si deseas una versión diferente.
|
||||
# Busca versiones disponibles con: apt-cache madison docker-ce
|
||||
# El formato debe ser como "5:27.1.1-1~ubuntu.22.04~jammy"
|
||||
# Dejaremos esto en blanco por ahora para que puedas elegir de la lista.
|
||||
TARGET_DOCKER_VERSION=""
|
||||
|
||||
echo "--- Iniciando el proceso de downgrade de Docker (v2) ---"
|
||||
|
||||
# --- 2. Desbloquear (unhold) paquetes de Docker ---
|
||||
echo "Paso 1: Desbloqueando paquetes de Docker existentes..."
|
||||
sudo apt-mark unhold docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin || echo "No se encontraron paquetes de Docker bloqueados, continuando."
|
||||
|
||||
# --- 3. Detener los servicios de Docker ---
|
||||
echo "Paso 2: Deteniendo los servicios de Docker..."
|
||||
sudo systemctl stop docker.service
|
||||
sudo systemctl stop docker.socket
|
||||
sudo systemctl stop containerd
|
||||
|
||||
# --- 4. Desinstalar la versión actual de Docker ---
|
||||
echo "Paso 3: Desinstalando los paquetes de Docker actuales..."
|
||||
# Se agrega --allow-change-held-packages para asegurar la desinstalación
|
||||
sudo apt-get remove --allow-change-held-packages -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
|
||||
sudo apt-get purge -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
|
||||
sudo apt-get autoremove -y --purge
|
||||
|
||||
# --- 5. Limpiar directorios residuales (¡CUIDADO!) ---
|
||||
echo "Paso 4: Limpiando directorios de Docker (esto eliminará imágenes y contenedores)..."
|
||||
# Comentado por seguridad. Descomenta si realmente quieres una limpieza completa.
|
||||
# sudo rm -rf /var/lib/docker
|
||||
# sudo rm -rf /var/lib/containerd
|
||||
|
||||
# --- 6. Asegurar que el repositorio de Docker esté configurado ---
|
||||
echo "Paso 5: Configurando el repositorio de Docker..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ca-certificates curl
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# Añadir el repositorio a las fuentes de Apt
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
||||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update
|
||||
|
||||
# --- 7. Listar las versiones disponibles y solicitar al usuario que elija ---
|
||||
echo "Paso 6: Versiones de Docker disponibles en el repositorio:"
|
||||
apt-cache madison docker-ce | awk '{print $3}'
|
||||
|
||||
echo -e "\nPor favor, copia y pega la versión exacta que deseas instalar de la lista anterior (ej: 5:27.1.1-1~ubuntu.22.04~jammy):"
|
||||
read -r TARGET_DOCKER_VERSION
|
||||
|
||||
if [ -z "$TARGET_DOCKER_VERSION" ]; then
|
||||
echo "No se ha seleccionado ninguna versión. Abortando."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 8. Instalar la versión de Docker seleccionada ---
|
||||
echo "Paso 7: Instalando la versión $TARGET_DOCKER_VERSION..."
|
||||
sudo apt-get install -y docker-ce=$TARGET_DOCKER_VERSION docker-ce-cli=$TARGET_DOCKER_VERSION containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
# --- 9. Verificar la instalación ---
|
||||
echo "Paso 8: Verificando la versión de Docker instalada..."
|
||||
docker --version
|
||||
|
||||
# --- 10. Bloquear la versión de Docker (opcional pero recomendado) ---
|
||||
echo "Paso 9: Bloqueando la versión de Docker para evitar actualizaciones automáticas."
|
||||
sudo apt-mark hold docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
echo -e "\n--- Proceso de downgrade de Docker completado. ---"
|
||||
echo "Recuerda reiniciar los servicios que dependen de Docker, como CasaOS o los contenedores específicos."
|
||||
echo "Puedes reiniciar el servicio de Docker con: sudo systemctl restart docker"
|
||||
222
edit_message.php
Executable file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
|
||||
if (!isset($_GET['schedule_id'])) {
|
||||
header('Location: scheduled_messages.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$scheduleId = $_GET['schedule_id'];
|
||||
|
||||
// Fetch the message details
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT s.*, m.content, dr.type as recipient_type
|
||||
FROM schedules s
|
||||
JOIN messages m ON s.message_id = m.id
|
||||
JOIN discord_recipients dr ON s.recipient_id = dr.id
|
||||
WHERE s.id = ?"
|
||||
);
|
||||
$stmt->execute([$scheduleId]);
|
||||
$message = $stmt->fetch();
|
||||
|
||||
if (!$message) {
|
||||
header('Location: scheduled_messages.php?error=not_found');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch all recipients for the dropdowns
|
||||
$channels = [];
|
||||
$users = [];
|
||||
try {
|
||||
$recipientStmt = $pdo->query("SELECT id, name, type FROM discord_recipients ORDER BY name ASC");
|
||||
while ($row = $recipientStmt->fetch()) {
|
||||
if ($row['type'] === 'channel') {
|
||||
$channels[] = $row;
|
||||
} else {
|
||||
$users[] = $row;
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Error: No se pudieron cargar los destinatarios.");
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<!-- Summernote CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs5.min.css" rel="stylesheet">
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4">Editar Notificación</h1>
|
||||
|
||||
<form action="includes/message_handler_edit.php" method="POST" id="editMessageForm">
|
||||
<input type="hidden" name="schedule_id" value="<?php echo $message['id']; ?>">
|
||||
<input type="hidden" name="message_id" value="<?php echo $message['message_id']; ?>">
|
||||
|
||||
<!-- Message Content -->
|
||||
<div class="mb-3">
|
||||
<label for="messageContent" class="form-label">Contenido del Mensaje</label>
|
||||
<textarea id="messageContent" name="messageContent" class="form-control"><?php echo htmlspecialchars($message['content']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Recipient Selection -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tipo de Destinatario</label>
|
||||
<div>
|
||||
<input type="radio" id="recipientTypeChannel" name="recipientType" value="channel" <?php echo ($message['recipient_type'] === 'channel') ? 'checked' : ''; ?>>
|
||||
<label for="recipientTypeChannel">Canal</label>
|
||||
<input type="radio" id="recipientTypeUser" name="recipientType" value="user" class="ms-3" <?php echo ($message['recipient_type'] === 'user') ? 'checked' : ''; ?>>
|
||||
<label for="recipientTypeUser">Usuario</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 <?php echo ($message['recipient_type'] === 'user') ? 'd-none' : ''; ?>" id="channelSelectWrapper">
|
||||
<label for="channelId" class="form-label">Seleccionar Canal</label>
|
||||
<select id="channelId" name="recipientId_channel" class="form-select">
|
||||
<?php foreach ($channels as $channel): ?>
|
||||
<option value="<?php echo $channel['id']; ?>" <?php echo ($message['recipient_id'] == $channel['id']) ? 'selected' : ''; ?>><?php echo htmlspecialchars($channel['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 <?php echo ($message['recipient_type'] === 'channel') ? 'd-none' : ''; ?>" id="userSelectWrapper">
|
||||
<label for="userId" class="form-label">Seleccionar Usuario</label>
|
||||
<select id="userId" name="recipientId_user" class="form-select">
|
||||
<?php foreach ($users as $user): ?>
|
||||
<option value="<?php echo $user['id']; ?>" <?php echo ($message['recipient_id'] == $user['id']) ? 'selected' : ''; ?>><?php echo htmlspecialchars($user['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Scheduling Options -->
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h6">Opciones de Envío</legend>
|
||||
<!-- Current implementation only allows re-scheduling. 'Send Now' could be added. -->
|
||||
<div>
|
||||
<input type="radio" id="scheduleLater" name="scheduleType" value="later" checked>
|
||||
<label for="scheduleLater">Programar para más tarde</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="mb-3" id="scheduleDateTimeWrapper">
|
||||
<label for="scheduleDateTime" class="form-label">Fecha y Hora de Envío</label>
|
||||
<input type="datetime-local" id="scheduleDateTime" name="scheduleDateTime" class="form-control" value="<?php echo date('Y-m-d\TH:i', strtotime($message['send_time'])); ?>">
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button type="submit" class="btn btn-primary">Actualizar Mensaje</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
|
||||
<!-- Gallery Modal -->
|
||||
<div class="modal fade" id="galleryModal" tabindex="-1" aria-labelledby="galleryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="galleryModalLabel">Galería de Imágenes</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<?php
|
||||
$gallery_path = __DIR__ . '/galeria';
|
||||
$files = array_diff(scandir($gallery_path), array('.', '..'));
|
||||
if (empty($files)) {
|
||||
echo '<p class="text-center text-muted">No hay imágenes en la galería.</p>';
|
||||
} else {
|
||||
foreach ($files as $file) {
|
||||
if (is_file($gallery_path . '/' . $file)) {
|
||||
echo '<div class="col-lg-3 col-md-4 col-sm-6 mb-4 text-center"><img src="galeria/' . htmlspecialchars($file) . '" class="img-fluid img-thumbnail gallery-item" style="cursor:pointer;" alt="' . htmlspecialchars($file) . '"><p class="small text-muted mt-1">' . htmlspecialchars($file) . '</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
||||
<button type="button" class="btn btn-primary" id="insertImageFromGallery">Insertar Imagen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Summernote JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs5.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Custom Gallery Button
|
||||
var GalleryButton = function (context) {
|
||||
var ui = $.summernote.ui;
|
||||
var button = ui.button({
|
||||
contents: '<i class="bi bi-images"></i> Galería',
|
||||
tooltip: 'Insertar imagen desde la galería',
|
||||
click: function () {
|
||||
$('#galleryModal').modal('show');
|
||||
}
|
||||
});
|
||||
return button.render();
|
||||
}
|
||||
|
||||
// Initialize Summernote
|
||||
$('#messageContent').summernote({
|
||||
placeholder: 'Escribe tu mensaje aquí...',
|
||||
tabsize: 2,
|
||||
height: 300,
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'underline', 'clear']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', ['link', 'picture', 'video']],
|
||||
['view', ['fullscreen', 'codeview', 'help']],
|
||||
['mybutton', ['gallery']]
|
||||
],
|
||||
buttons: {
|
||||
gallery: GalleryButton
|
||||
}
|
||||
});
|
||||
|
||||
// Handle image selection in modal
|
||||
$(document).on('click', '.gallery-item', function() {
|
||||
$(this).toggleClass('border-primary');
|
||||
});
|
||||
|
||||
// Handle image insertion
|
||||
$('#insertImageFromGallery').click(function() {
|
||||
$('.gallery-item.border-primary').each(function(){
|
||||
var imageUrl = $(this).attr('src');
|
||||
$('#messageContent').summernote('insertImage', imageUrl);
|
||||
});
|
||||
$('#galleryModal').modal('hide');
|
||||
$('.gallery-item').removeClass('border-primary');
|
||||
});
|
||||
|
||||
// Function to toggle recipient selects
|
||||
function toggleRecipientFields() {
|
||||
if ($('input[name="recipientType"]:checked').val() === 'channel') {
|
||||
$('#channelSelectWrapper').removeClass('d-none');
|
||||
$('#userSelectWrapper').addClass('d-none');
|
||||
$('#channelId').prop('name', 'recipientId');
|
||||
$('#userId').prop('name', 'recipientId_user'); // Unset name to avoid submission
|
||||
} else {
|
||||
$('#channelSelectWrapper').addClass('d-none');
|
||||
$('#userSelectWrapper').removeClass('d-none');
|
||||
$('#channelId').prop('name', 'recipientId_channel'); // Unset name
|
||||
$('#userId').prop('name', 'recipientId');
|
||||
}
|
||||
}
|
||||
|
||||
// Initial toggle on page load
|
||||
toggleRecipientFields();
|
||||
|
||||
// Add change event listener
|
||||
$('input[name="recipientType"]').change(toggleRecipientFields);
|
||||
});
|
||||
</script>
|
||||
203
edit_recurrent_message.php
Executable file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
|
||||
$base_url = rtrim(BOT_BASE_URL, '/');
|
||||
|
||||
$message_id = $_GET['id'] ?? 0;
|
||||
if (!$message_id) {
|
||||
header('Location: recurrentes.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Fetch the recurrent message data
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM recurrent_messages WHERE id = ?");
|
||||
$stmt->execute([$message_id]);
|
||||
$message = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$message) {
|
||||
die("Mensaje recurrente no encontrado.");
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Error al cargar el mensaje: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Recipient selection is handled on the 'send from template' page, not here.
|
||||
|
||||
$current_days = explode(',', $message['recurring_days']);
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<!-- Summernote CSS -->
|
||||
<link href="<?php echo asset('css/summernote-bs5.min.css'); ?>" rel="stylesheet">
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4">Editar Mensaje Recurrente</h1>
|
||||
|
||||
<div class="card card-body p-4">
|
||||
<form action="includes/recurrent_message_handler.php" method="POST" id="editRecurrentMessageForm">
|
||||
<input type="hidden" name="action" value="update">
|
||||
<input type="hidden" name="id" value="<?php echo $message['id']; ?>">
|
||||
|
||||
<!-- Message Name -->
|
||||
<div class="mb-3">
|
||||
<label for="messageName" class="form-label">Nombre del Mensaje</label>
|
||||
<input type="text" class="form-control" id="messageName" name="messageName" value="<?php echo htmlspecialchars($message['name']); ?>" required>
|
||||
</div>
|
||||
|
||||
<!-- Telegram Command -->
|
||||
<div class="mb-3">
|
||||
<label for="telegramCommand" class="form-label">Comando de Telegram</label>
|
||||
<input type="text" class="form-control" id="telegramCommand" name="telegram_command" value="<?php echo htmlspecialchars($message['telegram_command'] ?? ''); ?>" placeholder="Ej: Dia6 (sin #)">
|
||||
<small class="form-text text-muted">Opcional. El comando que los usuarios usarán en Telegram (sin el #).</small>
|
||||
</div>
|
||||
|
||||
<!-- Message Content -->
|
||||
<div class="mb-3">
|
||||
<label for="messageContent" class="form-label">Contenido del Mensaje</label>
|
||||
<textarea id="messageContent" name="messageContent" class="form-control"><?php echo htmlspecialchars($message['message_content']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save-fill me-1"></i> Actualizar Mensaje
|
||||
</button>
|
||||
<a href="recurrentes.php" class="btn btn-secondary">Cancelar</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery Modal (same as create_message.php) -->
|
||||
<div class="modal fade" id="galleryModal" tabindex="-1" aria-labelledby="galleryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="galleryModalLabel">Galería de Imágenes</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<?php
|
||||
$gallery_path = __DIR__ . '/galeria';
|
||||
$files = array_diff(scandir($gallery_path), array('.', '..'));
|
||||
if (empty($files)) {
|
||||
echo '<p class="text-center text-muted">No hay imágenes en la galería.</p>';
|
||||
} else {
|
||||
foreach ($files as $file) {
|
||||
if (is_file($gallery_path . '/' . $file)) {
|
||||
echo '<div class="col-lg-3 col-md-4 col-sm-6 mb-4 text-center"><img src="galeria/' . htmlspecialchars($file) . '" class="img-fluid img-thumbnail gallery-item" style="cursor:pointer;" alt="' . htmlspecialchars($file) . '"><p class="small text-muted mt-1">' . htmlspecialchars($file) . '</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
||||
<button type="button" class="btn btn-primary" id="insertImageFromGallery">Insertar Imagen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="previewModalLabel">Vista Previa del Mensaje</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="previewModalBody">
|
||||
<!-- Content will be injected here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Custom Gallery Button
|
||||
var GalleryButton = function (context) {
|
||||
var ui = $.summernote.ui;
|
||||
var button = ui.button({
|
||||
contents: '<i class="bi bi-images"></i> Galería',
|
||||
tooltip: 'Insertar imagen desde la galería',
|
||||
click: function () {
|
||||
$('#galleryModal').modal('show');
|
||||
}
|
||||
});
|
||||
return button.render();
|
||||
}
|
||||
|
||||
// Initialize Summernote
|
||||
$('#messageContent').summernote({
|
||||
placeholder: 'Escribe tu mensaje aquí...',
|
||||
tabsize: 2,
|
||||
height: 300,
|
||||
toolbar: [
|
||||
['style', ['style']], ['font', ['bold', 'underline', 'clear']], ['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']], ['table', ['table']],
|
||||
['insert', ['picture']], ['view', ['codeview']], ['mybutton', ['gallery']]
|
||||
],
|
||||
buttons: { gallery: GalleryButton },
|
||||
callbacks: {
|
||||
onImageUpload: function(files) {
|
||||
var editor = $(this);
|
||||
var data = new FormData();
|
||||
data.append("file", files[0]);
|
||||
$.ajax({
|
||||
url: 'upload_editor_image.php', method: 'POST', data: data,
|
||||
processData: false, contentType: false,
|
||||
success: function(response) {
|
||||
if (response.url) {
|
||||
editor.summernote('insertImage', response.url);
|
||||
} else {
|
||||
alert(response.error || 'Error al subir la imagen.');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error: No se pudo comunicar con el servidor para subir la imagen.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle image selection and insertion from gallery
|
||||
$(document).on('click', '.gallery-item', function() { $(this).toggleClass('border-primary'); });
|
||||
$('#insertImageFromGallery').click(function() {
|
||||
$('.gallery-item.border-primary').each(function(){
|
||||
$('#messageContent').summernote('insertImage', $(this).attr('src'));
|
||||
});
|
||||
$('#galleryModal').modal('hide');
|
||||
$('.gallery-item').removeClass('border-primary');
|
||||
});
|
||||
|
||||
// No recipient logic is needed on this page.
|
||||
|
||||
// Form validation
|
||||
$('#editRecurrentMessageForm').submit(function(e) {
|
||||
if ($('#messageContent').summernote('isEmpty')) {
|
||||
e.preventDefault();
|
||||
alert('El contenido del mensaje no puede estar vacío.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// Preview message function
|
||||
function previewMessage(content) {
|
||||
const previewModalBody = document.getElementById('previewModalBody');
|
||||
// Replace image URLs with absolute paths for preview
|
||||
const baseUrl = "<?php echo $base_url; ?>";
|
||||
let processedContent = content.replace(/src="galeria\//g, 'src="' + baseUrl + 'galeria/');
|
||||
previewModalBody.innerHTML = processedContent;
|
||||
var myModal = new bootstrap.Modal(document.getElementById('previewModal'));
|
||||
myModal.show();
|
||||
}
|
||||
</script>
|
||||
168
enviar_plantilla.php
Executable file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/session_check.php';
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
|
||||
// Fetch recipients
|
||||
$discord_channels = [];
|
||||
$discord_users = [];
|
||||
$telegram_chats = [];
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT id, name, type, platform FROM recipients ORDER BY platform, type, name ASC");
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
if ($row['platform'] === 'discord') {
|
||||
if ($row['type'] === 'channel') $discord_channels[] = $row;
|
||||
else $discord_users[] = $row;
|
||||
} elseif ($row['platform'] === 'telegram') {
|
||||
$telegram_chats[] = $row;
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Error: No se pudieron cargar los destinatarios.");
|
||||
}
|
||||
|
||||
// Fetch templates
|
||||
$templates = [];
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT id, name FROM recurrent_messages ORDER BY name ASC");
|
||||
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
die("Error: No se pudieron cargar las plantillas.");
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4">Enviar Mensaje desde Plantilla</h1>
|
||||
|
||||
<form action="includes/message_handler.php" method="POST" id="sendMessageForm">
|
||||
<input type="hidden" name="submit" value="send_from_template">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- Template Selection -->
|
||||
<div class="mb-3">
|
||||
<label for="recurrent_message_id" class="form-label">1. Seleccionar Plantilla</label>
|
||||
<select class="form-select" id="recurrent_message_id" name="recurrent_message_id" required>
|
||||
<option value="" selected disabled>Elige una plantilla...</option>
|
||||
<?php foreach ($templates as $template): ?>
|
||||
<option value="<?php echo $template['id']; ?>"><?php echo htmlspecialchars($template['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Platform Selection -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">2. Seleccionar Plataforma</label>
|
||||
<div>
|
||||
<input type="radio" class="btn-check" name="platform" id="platform_discord" value="discord" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="platform_discord">Discord</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="platform" id="platform_telegram" value="telegram" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="platform_telegram">Telegram</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recipient Selection -->
|
||||
<div class="mb-3">
|
||||
<label for="recipient_id" class="form-label">3. Seleccionar Destinatarios</label>
|
||||
|
||||
<!-- Discord Channels -->
|
||||
<select class="form-select recipient-select" id="recipient_id_discord_channel" name="recipient_id[]" multiple>
|
||||
<optgroup label="Canales de Discord">
|
||||
<?php foreach ($discord_channels as $c): ?><option value="<?php echo $c['id']; ?>"><?php echo htmlspecialchars($c['name']); ?></option><?php endforeach; ?>
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<!-- Discord Users -->
|
||||
<select class="form-select recipient-select mt-2" id="recipient_id_discord_user" name="recipient_id[]" multiple>
|
||||
<optgroup label="Usuarios de Discord">
|
||||
<?php foreach ($discord_users as $u): ?><option value="<?php echo $u['id']; ?>"><?php echo htmlspecialchars($u['name']); ?></option><?php endforeach; ?>
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<!-- Telegram Chats -->
|
||||
<select class="form-select recipient-select d-none mt-2" id="recipient_id_telegram" name="recipient_id[]" multiple>
|
||||
<optgroup label="Chats de Telegram">
|
||||
<?php foreach ($telegram_chats as $t): ?><option value="<?php echo $t['id']; ?>"><?php echo htmlspecialchars($t['name']); ?></option><?php endforeach; ?>
|
||||
</optgroup>
|
||||
</select>
|
||||
<small class="form-text text-muted">Mantén Ctrl (o Cmd) para seleccionar varios. Solo se enviará a los de la plataforma elegida.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- Schedule Options -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">4. Programar Envío</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="scheduleType" id="scheduleNow" value="now" checked>
|
||||
<label class="form-check-label" for="scheduleNow">Enviar ahora</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="scheduleType" id="scheduleLater" value="later">
|
||||
<label class="form-check-label" for="scheduleLater">Programar para más tarde</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="scheduleLaterOptions" style="display: none;">
|
||||
<label for="scheduleDateTime" class="form-label">Fecha y Hora</label>
|
||||
<input type="datetime-local" class="form-control" id="scheduleDateTime" name="scheduleDateTime" min="<?php echo date('Y-m-d\TH:i'); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success"><i class="bi bi-send-fill me-1"></i> Enviar Mensaje</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/templates/footer.php'; ?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const platformDiscord = $('#platform_discord');
|
||||
const platformTelegram = $('#platform_telegram');
|
||||
const discordChannelSelect = $('#recipient_id_discord_channel');
|
||||
const discordUserSelect = $('#recipient_id_discord_user');
|
||||
const telegramSelect = $('#recipient_id_telegram');
|
||||
|
||||
function togglePlatformView() {
|
||||
if (platformDiscord.is(':checked')) {
|
||||
discordChannelSelect.removeClass('d-none');
|
||||
discordUserSelect.removeClass('d-none');
|
||||
telegramSelect.addClass('d-none');
|
||||
} else {
|
||||
discordChannelSelect.addClass('d-none');
|
||||
discordUserSelect.addClass('d-none');
|
||||
telegramSelect.removeClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
platformDiscord.on('change', togglePlatformView);
|
||||
platformTelegram.on('change', togglePlatformView);
|
||||
togglePlatformView(); // Initial call
|
||||
|
||||
// Toggle schedule options
|
||||
$('input[name="scheduleType"]').on('change', function() {
|
||||
$('#scheduleLaterOptions').toggle(this.value === 'later');
|
||||
});
|
||||
|
||||
// Form validation
|
||||
$('#sendMessageForm').submit(function(e) {
|
||||
let selectedRecipients = 0;
|
||||
if (platformDiscord.is(':checked')) {
|
||||
selectedRecipients += discordChannelSelect.val().length;
|
||||
selectedRecipients += discordUserSelect.val().length;
|
||||
} else {
|
||||
selectedRecipients += telegramSelect.val().length;
|
||||
}
|
||||
|
||||
if (selectedRecipients === 0) {
|
||||
e.preventDefault();
|
||||
alert('Debes seleccionar al menos un destinatario para la plataforma elegida.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
BIN
galeria/Asedio.jpg
Executable file
|
After Width: | Height: | Size: 50 KiB |
BIN
galeria/CAOS.png
Executable file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
galeria/Carbon.png
Executable file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
galeria/Dia1.jpg
Executable file
|
After Width: | Height: | Size: 148 KiB |
BIN
galeria/Dia2.jpg
Executable file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
galeria/Dia3.jpg
Executable file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
galeria/Dia4.jpg
Executable file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
galeria/Dia5.jpg
Executable file
|
After Width: | Height: | Size: 131 KiB |
BIN
galeria/Dia6.jpg
Executable file
|
After Width: | Height: | Size: 139 KiB |
BIN
galeria/Escudo.jpg
Executable file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
galeria/Escudos.png
Executable file
|
After Width: | Height: | Size: 107 KiB |
BIN
galeria/Radar.png
Executable file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
galeria/S1-Skill-Points.png
Executable file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
galeria/Schuyler-exclusive-weapon-shard.png
Executable file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
galeria/Screenshot-from-2024-02-26-16-34-21.png
Executable file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-11-06.png
Executable file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-15-35.png
Executable file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-18-40.png
Executable file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-30-48.png
Executable file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
galeria/Screenshot-from-2024-03-07-17-32-10.png
Executable file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
galeria/Screenshot-from-2024-03-10-23-36-14.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
galeria/Screenshot-from-2024-03-19-18-30-28.png
Executable file
|
After Width: | Height: | Size: 21 KiB |
BIN
galeria/Screenshot-from-2024-03-19-18-32-53.png
Executable file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
galeria/Screenshot-from-2024-03-19-18-41-51.png
Executable file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
galeria/Screenshot-from-2024-03-25-23-18-07.png
Executable file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
galeria/Screenshot-from-2024-04-05-04-04-58.png
Executable file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
galeria/Screenshot-from-2024-05-06-04-02-00.png
Executable file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
galeria/Screenshot-from-2024-06-10-10-54-29.png
Executable file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
galeria/Screenshot-from-2024-08-19-13-37-42.png
Executable file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
galeria/Screenshot-from-2024-08-19-13-38-33.png
Executable file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
galeria/Screenshot-from-2024-08-19-13-38-44.png
Executable file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
galeria/Screenshot-from-2024-08-19-13-38-52.png
Executable file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
galeria/Screenshot-from-2024-10-08-11-48-47.png
Executable file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
galeria/Screenshot-from-2025-01-06-06-13-18.png
Executable file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
galeria/Screenshot_2025-06-27-19-29-45-460_com.fun.lastwar.gp.jpg
Executable file
|
After Width: | Height: | Size: 1.4 MiB |