Primer commit del sistema separado falta mejorar mucho
This commit is contained in:
35
discord/api/commands/create.php
Executable file
35
discord/api/commands/create.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$comando = trim($input['comando'] ?? '');
|
||||
$descripcion = trim($input['descripcion'] ?? '');
|
||||
$plantilla_id = $input['plantilla_id'] ?? null;
|
||||
|
||||
if (empty($comando) || empty($plantilla_id)) {
|
||||
jsonResponse(['success' => false, 'error' => 'Faltan datos requeridos'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO comandos_discord (comando, descripcion, plantilla_id)
|
||||
VALUES (?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$comando, $descripcion, $plantilla_id]);
|
||||
|
||||
jsonResponse(['success' => true]);
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == 23000) {
|
||||
jsonResponse(['success' => false, 'error' => 'Este comando ya existe'], 409);
|
||||
}
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
27
discord/api/commands/delete.php
Executable file
27
discord/api/commands/delete.php
Executable file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
jsonResponse(['success' => false, 'error' => 'ID requerido'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("DELETE FROM comandos_discord WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
jsonResponse(['success' => true]);
|
||||
} catch (PDOException $e) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
104
discord/api/messages/delete.php
Executable file
104
discord/api/messages/delete.php
Executable file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* API - Eliminar/Deshabilitar Mensaje Discord
|
||||
* Cambia el estado del mensaje a 'deshabilitado' y opcionalmente lo elimina de Discord.
|
||||
*/
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Habilitar errores para debug (quitar en producción estricta)
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$messageId = $input['id'] ?? null;
|
||||
|
||||
if (empty($messageId) || !is_numeric($messageId)) {
|
||||
jsonResponse(['success' => false, 'error' => 'ID de mensaje inválido o no proporcionado'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
|
||||
// 1. Obtener detalles del mensaje para verificar permisos y Discord ID
|
||||
$stmt = $db->prepare("SELECT usuario_id, canal_id, mensaje_discord_id, tipo_envio FROM mensajes_discord WHERE id = ?");
|
||||
$stmt->execute([$messageId]);
|
||||
$message = $stmt->fetch();
|
||||
|
||||
if (!$message) {
|
||||
jsonResponse(['success' => false, 'error' => 'Mensaje no encontrado'], 404);
|
||||
}
|
||||
|
||||
// Verificar permisos: solo Admin o el propietario pueden eliminar/deshabilitar
|
||||
if ($userData->rol !== 'Admin' && $message['usuario_id'] != $userData->userId) {
|
||||
jsonResponse(['success' => false, 'error' => 'No tiene permisos para eliminar este mensaje'], 403);
|
||||
}
|
||||
|
||||
// 2. Intentar eliminar de Discord si existe un mensaje_discord_id y fue un envío inmediato
|
||||
$discordDeletionSuccess = true;
|
||||
$discordDeletionError = null;
|
||||
|
||||
if (!empty($message['mensaje_discord_id']) && $message['tipo_envio'] === 'inmediato') {
|
||||
$botToken = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||
if (!$botToken) {
|
||||
logToFile('discord/errors.log', "Error eliminando mensaje de Discord: Token de bot no configurado.", 'ERROR');
|
||||
$discordDeletionError = "Token de bot no configurado, no se pudo eliminar de Discord.";
|
||||
$discordDeletionSuccess = false;
|
||||
} else {
|
||||
$url = "https://discord.com/api/v10/channels/{$message['canal_id']}/messages/{$message['mensaje_discord_id']}";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $botToken,
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError) {
|
||||
$discordDeletionError = "Error cURL al eliminar de Discord: " . $curlError;
|
||||
$discordDeletionSuccess = false;
|
||||
} elseif ($httpCode !== 204) { // 204 No Content es la respuesta esperada para DELETE exitoso
|
||||
$responseJson = json_decode($response, true);
|
||||
$discordDeletionError = $responseJson['message'] ?? 'Error desconocido de Discord al eliminar';
|
||||
$discordDeletionSuccess = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Deshabilitar el mensaje en la base de datos
|
||||
// Si es un mensaje programado/recurrente pendiente, simplemente lo deshabilitamos sin intentar borrar de Discord
|
||||
$updateStmt = $db->prepare("UPDATE mensajes_discord SET estado = 'deshabilitado' WHERE id = ?");
|
||||
$updateStmt->execute([$messageId]);
|
||||
|
||||
logToFile('discord/messages.log', "Mensaje deshabilitado (ID: {$messageId}) por Usuario: {$userData->username}. Discord deletion: " . ($discordDeletionSuccess ? 'OK' : 'Fallido/' . $discordDeletionError));
|
||||
|
||||
jsonResponse([
|
||||
'success' => true,
|
||||
'message' => 'Mensaje deshabilitado correctamente.',
|
||||
'discord_deletion_attempted' => !empty($message['mensaje_discord_id']) && $message['tipo_envio'] === 'inmediato',
|
||||
'discord_deletion_success' => $discordDeletionSuccess,
|
||||
'discord_deletion_error' => $discordDeletionError
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
logToFile('discord/errors.log', "Error deshabilitando mensaje: " . $e->getMessage(), 'ERROR');
|
||||
jsonResponse(['success' => false, 'error' => 'Ocurrió un error en el servidor.'], 500);
|
||||
}
|
||||
7
discord/api/messages/php_errors.log
Executable file
7
discord/api/messages/php_errors.log
Executable file
@@ -0,0 +1,7 @@
|
||||
[30-Nov-2025 16:17:06 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[30-Nov-2025 16:20:55 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[30-Nov-2025 16:22:33 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[30-Nov-2025 16:34:09 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[30-Nov-2025 16:34:29 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[30-Nov-2025 17:02:10 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[03-Dec-2025 20:51:19 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/messages.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
49
discord/api/messages/retry.php
Executable file
49
discord/api/messages/retry.php
Executable file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* API - Reintentar Mensaje Discord
|
||||
* Cambia el estado de un mensaje a 'pendiente'.
|
||||
*/
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
// Verificar permiso (reutilizamos 'send_messages' ya que es una acción relacionada)
|
||||
if (!hasPermission('send_messages', 'discord')) {
|
||||
jsonResponse(['success' => false, 'error' => 'No tienes permiso para gestionar mensajes de Discord.'], 403);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$messageId = $input['id'] ?? null;
|
||||
|
||||
if (empty($messageId)) {
|
||||
jsonResponse(['success' => false, 'error' => 'Falta el ID del mensaje.'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
|
||||
// Cambiar el estado del mensaje a 'pendiente'
|
||||
$stmt = $db->prepare("UPDATE mensajes_discord SET estado = 'pendiente' WHERE id = ?");
|
||||
$stmt->execute([$messageId]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonResponse(['success' => true, 'message' => 'Mensaje marcado como pendiente.']);
|
||||
} else {
|
||||
jsonResponse(['success' => false, 'error' => 'No se encontró el mensaje o no se pudo actualizar.']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
jsonResponse(['success' => false, 'error' => 'Error en la base de datos: ' . $e->getMessage()], 500);
|
||||
}
|
||||
221
discord/api/messages/send.php
Executable file
221
discord/api/messages/send.php
Executable file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
/**
|
||||
* API - Enviar Mensaje Discord
|
||||
*/
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Habilitar errores para debug (quitar en producción estricta)
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
// Verificar permiso
|
||||
if (!hasPermission('send_messages', 'discord')) {
|
||||
jsonResponse(['success' => false, 'error' => 'No tienes permiso para enviar mensajes de Discord.'], 403);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$destinatariosInput = $input['destinatario_id'] ?? []; // Puede ser un string o un array
|
||||
$contenido = $input['contenido'] ?? '';
|
||||
$tipoEnvio = $input['tipo_envio'] ?? 'inmediato'; // 'inmediato', 'programado', 'recurrente'
|
||||
|
||||
// Asegurarse de que $destinatariosInput sea siempre un array
|
||||
if (!is_array($destinatariosInput)) {
|
||||
$destinatariosInput = [$destinatariosInput];
|
||||
}
|
||||
$destinatarios = array_filter(array_map('trim', $destinatariosInput)); // Limpiar y filtrar vacíos
|
||||
|
||||
if (empty($destinatarios) || empty($contenido)) {
|
||||
jsonResponse(['success' => false, 'error' => 'Faltan datos requeridos: destinatario(s) y contenido'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Procesar contenido HTML para extraer imágenes y limpiar texto (una vez para todos)
|
||||
$dom = new DOMDocument();
|
||||
// Suprimir errores de HTML mal formado y usar UTF-8
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $contenido, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
$embeds = [];
|
||||
$images = $dom->getElementsByTagName('img');
|
||||
|
||||
// Extraer hasta 10 imágenes (límite de Discord)
|
||||
$count = 0;
|
||||
foreach ($images as $img) {
|
||||
if ($count >= 10) break;
|
||||
$src = $img->getAttribute('src');
|
||||
if ($src) {
|
||||
$embeds[] = [
|
||||
'image' => ['url' => $src]
|
||||
];
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Limpiar texto: convertir saltos de línea y eliminar tags
|
||||
$cleanContent = str_replace(['<br>', '<br/>', '<p>'], ["\n", "\n", "\n"], $contenido);
|
||||
$cleanContent = strip_tags($cleanContent);
|
||||
$cleanContent = html_entity_decode($cleanContent);
|
||||
$cleanContent = trim($cleanContent);
|
||||
|
||||
$db = getDB();
|
||||
$botToken = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||
|
||||
if (!$botToken) {
|
||||
throw new Exception("Token de bot no configurado");
|
||||
}
|
||||
|
||||
$allResults = [];
|
||||
|
||||
foreach ($destinatarios as $destinatarioId) {
|
||||
$messageStatus = 'pendiente'; // Estado por defecto para programados/recurrentes
|
||||
$discordMessageId = null;
|
||||
$errorMessage = null;
|
||||
|
||||
if ($tipoEnvio === 'inmediato') {
|
||||
$url = "https://discord.com/api/v10/channels/{$destinatarioId}/messages";
|
||||
|
||||
$data = [
|
||||
'content' => $cleanContent
|
||||
];
|
||||
|
||||
if (!empty($embeds)) {
|
||||
$data['embeds'] = $embeds;
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $botToken,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$responseJson = json_decode($response, true);
|
||||
|
||||
if ($curlError) {
|
||||
$errorMessage = "Error cURL: " . $curlError;
|
||||
} elseif ($httpCode >= 400) {
|
||||
$errorMessage = $responseJson['message'] ?? 'Error desconocido de Discord';
|
||||
if (isset($responseJson['errors'])) {
|
||||
$errorMessage .= ' - ' . json_encode($responseJson['errors']);
|
||||
}
|
||||
} else {
|
||||
$messageStatus = 'enviado';
|
||||
$discordMessageId = $responseJson['id'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Guardar en base de datos (Historial) para CADA destinatario
|
||||
// Inmediato: estado 'enviado' o 'fallido'
|
||||
// Programado/Recurrente: estado 'pendiente'
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO mensajes_discord (usuario_id, canal_id, contenido, estado, mensaje_discord_id, fecha_envio, tipo_envio)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
|
||||
$scheduledSendTime = null;
|
||||
if ($tipoEnvio === 'programado' && isset($input['fecha_envio'])) {
|
||||
// Convertir la fecha local a UTC para guardar en DB
|
||||
$datetime = new DateTime($input['fecha_envio'], new DateTimeZone($_ENV['TIME_ZONE_ENVIOS'] ?? 'UTC'));
|
||||
$datetime->setTimezone(new DateTimeZone('UTC'));
|
||||
$scheduledSendTime = $datetime->format('Y-m-d H:i:s');
|
||||
} elseif ($tipoEnvio === 'inmediato') {
|
||||
$scheduledSendTime = date('Y-m-d H:i:s'); // La fecha de envío es ahora
|
||||
}
|
||||
|
||||
$stmt->execute([
|
||||
$userData->userId,
|
||||
$destinatarioId,
|
||||
$contenido,
|
||||
($tipoEnvio === 'inmediato' && $messageStatus === 'fallido') ? 'fallido' : $messageStatus, // Estado correcto para inmediato/fallido
|
||||
$discordMessageId,
|
||||
$scheduledSendTime,
|
||||
$tipoEnvio
|
||||
]);
|
||||
|
||||
$messageDbId = $db->lastInsertId();
|
||||
|
||||
// Si es recurrente, guardar también en la tabla recurrentes_discord
|
||||
if ($tipoEnvio === 'recurrente') {
|
||||
// Validar y obtener hora de envío
|
||||
$horaEnvio = $input['hora_envio'] ?? '09:00:00';
|
||||
if (!preg_match('/^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/', $horaEnvio) && !preg_match('/^([01]\d|2[0-3]):([0-5]\d)$/', $horaEnvio)) {
|
||||
$horaEnvio = '09:00:00'; // Valor por defecto si es inválido
|
||||
}
|
||||
if (strlen($horaEnvio) === 5) { // Si viene sin segundos, añadir
|
||||
$horaEnvio .= ':00';
|
||||
}
|
||||
|
||||
$nextSendTime = null; // Esto debería ser calculado por un demonio de scheduling
|
||||
|
||||
$stmtRecur = $db->prepare("
|
||||
INSERT INTO recurrentes_discord (mensaje_id, frecuencia, hora_envio, dia_semana, dia_mes, activo, proximo_envio)
|
||||
VALUES (?, ?, ?, ?, ?, 1, ?)
|
||||
");
|
||||
$stmtRecur->execute([
|
||||
$messageDbId,
|
||||
$input['frecuencia'] ?? 'diario',
|
||||
$horaEnvio,
|
||||
$input['dia_semana'] ?? null,
|
||||
$input['dia_mes'] ?? null,
|
||||
$nextSendTime // Será calculado por el demonio
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
$allResults[] = [
|
||||
'destinatario_id' => $destinatarioId,
|
||||
'message_db_id' => $messageDbId,
|
||||
'status' => ($tipoEnvio === 'inmediato' && $messageStatus === 'fallido') ? 'fallido' : $messageStatus,
|
||||
'error' => $errorMessage,
|
||||
'discord_message_id' => $discordMessageId,
|
||||
'tipo_envio' => $tipoEnvio
|
||||
];
|
||||
|
||||
if ($messageStatus === 'enviado') {
|
||||
logToFile('discord/messages.log', "Mensaje enviado a {$destinatarioId} por {$userData->username}");
|
||||
} elseif ($messageStatus === 'pendiente') {
|
||||
logToFile('discord/messages.log', "Mensaje {$tipoEnvio} guardado para {$destinatarioId} por {$userData->username}");
|
||||
} else {
|
||||
logToFile('discord/errors.log', "Error enviando mensaje a {$destinatarioId}: {$errorMessage}", 'ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
// Determinar el éxito general de la operación
|
||||
$overallSuccess = array_reduce($allResults, function($carry, $item) {
|
||||
// Considerar éxito si al menos un mensaje fue enviado o está pendiente
|
||||
return $carry || ($item['status'] === 'enviado' || $item['status'] === 'pendiente');
|
||||
}, false);
|
||||
|
||||
jsonResponse([
|
||||
'success' => $overallSuccess,
|
||||
'message' => 'Procesamiento de mensajes completado.',
|
||||
'details' => $allResults,
|
||||
'overall_status' => $overallSuccess ? 'partial_success_or_pending' : 'all_failed'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
logToFile('discord/errors.log', "Error general en el envío de mensajes: " . $e->getMessage(), 'ERROR');
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
41
discord/api/recipients/create.php
Executable file
41
discord/api/recipients/create.php
Executable file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
// Verificar permiso
|
||||
if (!hasPermission('manage_recipients', 'discord')) {
|
||||
jsonResponse(['success' => false, 'error' => 'No tienes permiso para crear destinatarios de Discord.'], 403);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$nombre = trim($input['nombre'] ?? '');
|
||||
$discord_id = trim($input['discord_id'] ?? '');
|
||||
$tipo = trim($input['tipo'] ?? 'canal');
|
||||
|
||||
if (empty($nombre) || empty($discord_id)) {
|
||||
jsonResponse(['success' => false, 'error' => 'Faltan datos'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO destinatarios_discord (nombre, discord_id, tipo, activo)
|
||||
VALUES (?, ?, ?, 1)
|
||||
");
|
||||
$stmt->execute([$nombre, $discord_id, $tipo]);
|
||||
|
||||
jsonResponse(['success' => true]);
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == 23000) {
|
||||
jsonResponse(['success' => false, 'error' => 'Este ID de Discord ya está registrado'], 409);
|
||||
}
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
33
discord/api/recipients/delete.php
Executable file
33
discord/api/recipients/delete.php
Executable file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
// Verificar permiso
|
||||
if (!hasPermission('manage_recipients', 'discord')) {
|
||||
jsonResponse(['success' => false, 'error' => 'No tienes permiso para eliminar destinatarios de Discord.'], 403);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
jsonResponse(['success' => false, 'error' => 'ID requerido'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("DELETE FROM destinatarios_discord WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
jsonResponse(['success' => true]);
|
||||
} catch (PDOException $e) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
85
discord/api/recipients/edit.php
Executable file
85
discord/api/recipients/edit.php
Executable file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* API - Editar Destinatario Discord
|
||||
*/
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Habilitar errores para debug (quitar en producción estricta)
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
// Verificar permiso
|
||||
if (!hasPermission('manage_recipients', 'discord')) {
|
||||
jsonResponse(['success' => false, 'error' => 'No tienes permiso para editar destinatarios de Discord.'], 403);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? null;
|
||||
$nombre = trim($input['nombre'] ?? '');
|
||||
$discord_id = trim($input['discord_id'] ?? '');
|
||||
$tipo = trim($input['tipo'] ?? '');
|
||||
|
||||
// Validaciones
|
||||
if (empty($id) || !is_numeric($id)) {
|
||||
jsonResponse(['success' => false, 'error' => 'ID de destinatario inválido'], 400);
|
||||
}
|
||||
if (empty($nombre) || empty($discord_id) || empty($tipo)) {
|
||||
jsonResponse(['success' => false, 'error' => 'Faltan datos requeridos (nombre, ID de Discord, tipo)'], 400);
|
||||
}
|
||||
if (!in_array($tipo, ['canal', 'usuario', 'grupo'])) { // Permitir 'grupo' aunque no esté en UI aún
|
||||
jsonResponse(['success' => false, 'error' => 'Tipo de destinatario inválido'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
|
||||
// 1. Verificar si el destinatario existe y si el usuario tiene permisos
|
||||
$stmt = $db->prepare("SELECT usuario_id FROM destinatarios_discord WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$recipient = $stmt->fetch();
|
||||
|
||||
if (!$recipient) {
|
||||
jsonResponse(['success' => false, 'error' => 'Destinatario no encontrado'], 404);
|
||||
}
|
||||
|
||||
// Opcional: Si se implementara la propiedad del destinatario, se verificaría aquí
|
||||
// if ($userData->rol !== 'Admin' && $recipient['usuario_id'] != $userData->userId) {
|
||||
// jsonResponse(['success' => false, 'error' => 'No tiene permisos para editar este destinatario'], 403);
|
||||
// }
|
||||
|
||||
// 2. Verificar duplicidad de discord_id (excluyendo el propio destinatario)
|
||||
$stmt = $db->prepare("SELECT id FROM destinatarios_discord WHERE discord_id = ? AND id != ?");
|
||||
$stmt->execute([$discord_id, $id]);
|
||||
if ($stmt->fetch()) {
|
||||
jsonResponse(['success' => false, 'error' => 'Ya existe un destinatario con este ID de Discord'], 409);
|
||||
}
|
||||
|
||||
// 3. Actualizar el destinatario
|
||||
$stmt = $db->prepare("
|
||||
UPDATE destinatarios_discord
|
||||
SET nombre = ?, discord_id = ?, tipo = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$nombre, $discord_id, $tipo, $id]);
|
||||
|
||||
logToFile('discord/recipients.log', "Destinatario editado: ID={$id}, Nombre={$nombre}, Usuario={$userData->username}");
|
||||
jsonResponse(['success' => true, 'message' => 'Destinatario actualizado correctamente']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
logToFile('discord/errors.log', "Error editando destinatario: " . $e->getMessage(), 'ERROR');
|
||||
jsonResponse(['success' => false, 'error' => 'Ocurrió un error en el servidor.'], 500);
|
||||
}
|
||||
125
discord/api/recipients/kick.php
Executable file
125
discord/api/recipients/kick.php
Executable file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
/**
|
||||
* API - Expulsar/Remover Destinatario Discord
|
||||
* Expulsa a un usuario de un guild o remueve el bot de un canal/grupo (según el tipo)
|
||||
*/
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Habilitar errores para debug (quitar en producción estricta)
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
// Verificar permiso
|
||||
if (!hasPermission('manage_recipients', 'discord')) {
|
||||
jsonResponse(['success' => false, 'error' => 'No tienes permiso para expulsar destinatarios de Discord.'], 403);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$recipientDbId = $input['id'] ?? null; // ID de nuestra base de datos
|
||||
|
||||
if (empty($recipientDbId) || !is_numeric($recipientDbId)) {
|
||||
jsonResponse(['success' => false, 'error' => 'ID de destinatario inválido o no proporcionado'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
$botToken = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||
$guildId = $_ENV['DISCORD_GUILD_ID'] ?? getenv('DISCORD_GUILD_ID');
|
||||
|
||||
if (!$botToken) {
|
||||
throw new Exception("Token de bot de Discord no configurado.");
|
||||
}
|
||||
if (!$guildId) {
|
||||
throw new Exception("ID de Guild de Discord no configurado.");
|
||||
}
|
||||
|
||||
// 1. Obtener detalles del destinatario de nuestra DB
|
||||
$stmt = $db->prepare("SELECT discord_id, tipo FROM destinatarios_discord WHERE id = ?");
|
||||
$stmt->execute([$recipientDbId]);
|
||||
$recipient = $stmt->fetch();
|
||||
|
||||
if (!$recipient) {
|
||||
jsonResponse(['success' => false, 'error' => 'Destinatario no encontrado en la base de datos local.'], 404);
|
||||
}
|
||||
|
||||
$discordId = $recipient['discord_id'];
|
||||
$tipo = $recipient['tipo'];
|
||||
|
||||
$actionSuccess = false;
|
||||
$actionMessage = '';
|
||||
|
||||
// Lógica para expulsar/remover según el tipo
|
||||
switch ($tipo) {
|
||||
case 'usuario':
|
||||
// Expulsar usuario de un guild (servidor)
|
||||
// Endpoint: DELETE /guilds/{guild.id}/members/{user.id}
|
||||
$url = "https://discord.com/api/v10/guilds/{$guildId}/members/{$discordId}";
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $botToken,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError) {
|
||||
throw new Exception("Error cURL al intentar expulsar al usuario de Discord: " . $curlError);
|
||||
}
|
||||
if ($httpCode === 204) { // 204 No Content es éxito para DELETE
|
||||
$actionSuccess = true;
|
||||
$actionMessage = "Usuario {$discordId} expulsado del guild {$guildId} en Discord.";
|
||||
} else {
|
||||
$responseJson = json_decode($response, true);
|
||||
$actionMessage = "Error de Discord al expulsar usuario ({$httpCode}): " . ($responseJson['message'] ?? 'Desconocido');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'canal':
|
||||
case 'grupo':
|
||||
// Para canales/grupos, "expulsar" podría significar que el bot abandone el canal/grupo
|
||||
// Esto es más complejo y no hay un endpoint directo "abandonar canal" para un bot en v10 sin conocer el webhook.
|
||||
// Para simplificar, por ahora solo marcaremos como removido en nuestra DB.
|
||||
// Una implementación real necesitaría una lógica para que el bot salga del canal/thread.
|
||||
$actionSuccess = true; // Por ahora, se asume éxito en la acción "local"
|
||||
$actionMessage = "No hay una API directa para que el bot 'expulse' de un canal/grupo Discord. Eliminado de la base de datos local.";
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['success' => false, 'error' => 'Tipo de destinatario no soportado para expulsión.'], 400);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($actionSuccess) {
|
||||
// Eliminar el destinatario de nuestra base de datos
|
||||
$deleteStmt = $db->prepare("DELETE FROM destinatarios_discord WHERE id = ?");
|
||||
$deleteStmt->execute([$recipientDbId]);
|
||||
|
||||
logToFile('discord/recipients.log', "Destinatario '{$recipient['discord_id']}' ({$tipo}) expulsado/eliminado de Discord y de la DB local por Usuario: {$userData->username}.");
|
||||
jsonResponse(['success' => true, 'message' => 'Destinatario expulsado/eliminado correctamente.', 'discord_action' => $actionMessage]);
|
||||
} else {
|
||||
logToFile('discord/errors.log', "Fallo al expulsar/eliminar destinatario '{$recipient['discord_id']}' ({$tipo}): {$actionMessage}", 'ERROR');
|
||||
jsonResponse(['success' => false, 'error' => 'Fallo al realizar la acción en Discord: ' . $actionMessage], 500);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
logToFile('discord/errors.log', "Error general en la API de expulsión de destinatarios: " . $e->getMessage(), 'ERROR');
|
||||
jsonResponse(['success' => false, 'error' => 'Ocurrió un error en el servidor: ' . $e->getMessage()], 500);
|
||||
}
|
||||
100
discord/api/templates/create.php
Executable file
100
discord/api/templates/create.php
Executable file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* API de Plantillas de Discord - Crear
|
||||
* REFRACTORIZADO para usar la tabla `comandos_discord`
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$userData = JWTAuth::requireAuth();
|
||||
} catch (Exception $e) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'No autenticado: ' . $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!hasPermission('manage_templates', 'discord')) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'No tienes permiso para crear plantillas de Discord.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Método no permitido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$nombre = trim($data['nombre'] ?? '');
|
||||
$comando = ltrim(trim($data['comando'] ?? ''), '#/');
|
||||
$contenido = $data['contenido'] ?? '';
|
||||
|
||||
if (empty($nombre) || empty($contenido)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'El nombre y el contenido de la plantilla son obligatorios.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// 1. Verificar si el comando ya existe en la tabla `comandos_discord`
|
||||
if (!empty($comando)) {
|
||||
$stmt = $db->prepare("SELECT id FROM comandos_discord WHERE comando = ?");
|
||||
$stmt->execute([$comando]);
|
||||
if ($stmt->fetch()) {
|
||||
$db->rollBack();
|
||||
http_response_code(409); // Conflict
|
||||
echo json_encode(['success' => false, 'error' => 'Ya existe un comando con ese nombre.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Insertar la plantilla (sin la columna `comando`)
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO plantillas_discord (nombre, contenido, usuario_id, fecha_creacion, fecha_modificacion)
|
||||
VALUES (?, ?, ?, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([$nombre, $contenido, $userData->userId]);
|
||||
$newTemplateId = $db->lastInsertId();
|
||||
|
||||
// 3. Si hay un comando, insertarlo en la tabla `comandos_discord`
|
||||
if (!empty($comando)) {
|
||||
logToFile('discord/templates.log', "Intentando insertar comando en comandos_discord. Comando: {$comando}, Plantilla ID: {$newTemplateId}", 'INFO');
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO comandos_discord (comando, descripcion, plantilla_id)
|
||||
VALUES (?, ?, ?)
|
||||
");
|
||||
// Usamos el nombre de la plantilla como descripción por defecto
|
||||
$stmt->execute([$comando, $nombre, $newTemplateId]);
|
||||
logToFile('discord/templates.log', "Comando insertado en comandos_discord. Comando: {$comando}, Plantilla ID: {$newTemplateId}", 'INFO');
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
logToFile('discord/templates.log', "Plantilla creada: {$nombre} (ID: {$newTemplateId}), por Usuario: {$userData->username}");
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Plantilla creada correctamente.',
|
||||
'templateId' => $newTemplateId
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
http_response_code(500);
|
||||
logToFile('discord/errors.log', 'Error creando plantilla: ' . $e->getMessage(), 'ERROR');
|
||||
echo json_encode(['success' => false, 'error' => 'Ocurrió un error en el servidor al crear la plantilla.']);
|
||||
}
|
||||
139
discord/api/templates/delete.php
Executable file
139
discord/api/templates/delete.php
Executable file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* API de Plantillas de Discord - Eliminar
|
||||
* Este script es llamado por la función deleteTemplate() en list.php
|
||||
*/
|
||||
|
||||
// Habilitar logging de errores
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// Función de logging mejorada
|
||||
function logError($message, $data = null) {
|
||||
$logDir = '/var/www/html/bot/logs/discord/';
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0777, true);
|
||||
}
|
||||
$logFile = $logDir . 'error.log';
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$logMessage = "[$timestamp] [ERROR] $message";
|
||||
if ($data) {
|
||||
$logMessage .= "\n" . (is_string($data) ? $data : json_encode($data, JSON_PRETTY_PRINT));
|
||||
}
|
||||
error_log($logMessage . "\n", 3, $logFile);
|
||||
}
|
||||
|
||||
// Iniciar buffer para capturar cualquier salida no deseada
|
||||
ob_start();
|
||||
|
||||
try {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Registrar inicio
|
||||
logError("Inicio de eliminación de plantilla");
|
||||
|
||||
// Incluir dependencias
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Limpiar buffer por si hay salida no deseada
|
||||
ob_clean();
|
||||
|
||||
// Autenticación JWT para API (no redirigir)
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Acceso no autorizado.']);
|
||||
logError("Error de autenticación");
|
||||
exit;
|
||||
}
|
||||
|
||||
logError("Usuario autenticado: " . json_encode(['id' => $userData->userId, 'username' => $userData->username]));
|
||||
|
||||
// Verificar método
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Método no permitido.']);
|
||||
logError("Método no permitido: " . $_SERVER['REQUEST_METHOD']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Obtener datos del cuerpo de la petición
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
$template_id = $data['id'] ?? null;
|
||||
|
||||
logError("Datos recibidos", ['input' => $input, 'data' => $data, 'template_id' => $template_id]);
|
||||
|
||||
// Validar ID
|
||||
if (!$template_id || !is_numeric($template_id)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'ID de plantilla inválido o no proporcionado.']);
|
||||
logError("ID de plantilla inválido", ['template_id' => $template_id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Conectar a la base de datos
|
||||
try {
|
||||
$db = getDB();
|
||||
logError("Conexión a BD exitosa");
|
||||
} catch (Exception $e) {
|
||||
logError("Error al conectar a la base de datos", $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Iniciar transacción
|
||||
$db->beginTransaction();
|
||||
logError("Transacción iniciada");
|
||||
|
||||
try {
|
||||
// 1. Eliminar comandos asociados (si existen)
|
||||
$stmt = $db->prepare("DELETE FROM comandos_discord WHERE plantilla_id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
$deletedCommands = $stmt->rowCount();
|
||||
logError("Comandos eliminados", ['count' => $deletedCommands]);
|
||||
|
||||
// 2. Intentar eliminar la plantilla
|
||||
$stmt = $db->prepare("DELETE FROM plantillas_discord WHERE id = ?");
|
||||
$stmt->execute([$template_id]);
|
||||
$deleted = $stmt->rowCount();
|
||||
|
||||
if ($deleted > 0) {
|
||||
$db->commit();
|
||||
logError("Plantilla eliminada exitosamente", ['id' => $template_id]);
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
$db->rollBack();
|
||||
http_response_code(404);
|
||||
logError("No se encontró la plantilla para eliminar", ['id' => $template_id]);
|
||||
echo json_encode(['success' => false, 'error' => 'No se encontró la plantilla para eliminar.']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
logError("Error en la transacción", [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Ocurrió un error en el servidor.',
|
||||
'detail' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
logError("Error no manejado", [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Error interno del servidor.',
|
||||
'detail' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
// Limpiar cualquier salida no deseada
|
||||
ob_end_flush();
|
||||
138
discord/api/templates/edit.php
Executable file
138
discord/api/templates/edit.php
Executable file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
/**
|
||||
* API de Plantillas de Discord - Editar
|
||||
* REFRACTORIZADO para usar la tabla `comandos_discord`
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
try {
|
||||
$userData = JWTAuth::requireAuth();
|
||||
} catch (Exception $e) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Acceso no autorizado.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!hasPermission('manage_templates', 'discord')) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'No tienes permiso para editar plantillas de Discord.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Método no permitido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$plantilla_id = $data['id'] ?? null;
|
||||
$nombre = trim($data['nombre'] ?? '');
|
||||
$comando = ltrim(trim($data['comando'] ?? ''), '#/');
|
||||
$contenido = $data['contenido'] ?? '';
|
||||
|
||||
if (!$plantilla_id || !is_numeric($plantilla_id)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'ID de plantilla inválido.']);
|
||||
exit;
|
||||
}
|
||||
if (empty($nombre) || empty($contenido)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'El nombre y el contenido son obligatorios.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
try {
|
||||
logToFile('discord/templates.log', "Iniciando edición de plantilla (ID: {$plantilla_id}). Comando recibido: {$comando}", 'INFO');
|
||||
$db->beginTransaction();
|
||||
|
||||
// 1. Verificar que la plantilla existe y el usuario tiene permiso
|
||||
$stmt = $db->prepare("SELECT usuario_id FROM plantillas_discord WHERE id = ?");
|
||||
$stmt->execute([$plantilla_id]);
|
||||
$plantillaExistente = $stmt->fetch();
|
||||
|
||||
if (!$plantillaExistente) {
|
||||
throw new Exception('Plantilla no encontrada.', 404);
|
||||
}
|
||||
if ($userData->rol !== 'Admin' && $plantillaExistente['usuario_id'] != $userData->userId) {
|
||||
throw new Exception('No tiene permisos para editar esta plantilla.', 403);
|
||||
}
|
||||
|
||||
// 2. Actualizar la plantilla en sí (nombre y contenido)
|
||||
$stmt = $db->prepare("
|
||||
UPDATE plantillas_discord
|
||||
SET nombre = ?, contenido = ?, fecha_modificacion = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$nombre, $contenido, $plantilla_id]);
|
||||
|
||||
// 3. Gestionar el comando en la tabla `comandos_discord`
|
||||
// Primero, obtener el comando actual si existe
|
||||
$stmt = $db->prepare("SELECT id, comando FROM comandos_discord WHERE plantilla_id = ?");
|
||||
$stmt->execute([$plantilla_id]);
|
||||
$comandoExistente = $stmt->fetch();
|
||||
|
||||
logToFile('discord/templates.log', "Comando existente para plantilla {$plantilla_id}: " . ($comandoExistente ? json_encode($comandoExistente) : 'Ninguno'), 'INFO');
|
||||
|
||||
|
||||
// Antes de insertar/actualizar, verificar si el nuevo nombre de comando ya está en uso por OTRA plantilla
|
||||
if (!empty($comando)) {
|
||||
$stmt = $db->prepare("SELECT id FROM comandos_discord WHERE comando = ? AND plantilla_id != ?");
|
||||
$stmt->execute([$comando, $plantilla_id]);
|
||||
if ($stmt->fetch()) {
|
||||
throw new Exception('Ya existe otro comando con ese nombre.', 409);
|
||||
}
|
||||
}
|
||||
|
||||
// Lógica de casos
|
||||
if (!empty($comando) && $comandoExistente) {
|
||||
// Caso: El comando se está modificando
|
||||
if ($comando !== $comandoExistente['comando']) {
|
||||
logToFile('discord/templates.log', "Modificando comando existente para plantilla {$plantilla_id}. De: {$comandoExistente['comando']} a: {$comando}", 'INFO');
|
||||
$stmt = $db->prepare("UPDATE comandos_discord SET comando = ?, descripcion = ? WHERE id = ?");
|
||||
$stmt->execute([$comando, $nombre, $comandoExistente['id']]);
|
||||
} else {
|
||||
logToFile('discord/templates.log', "Comando existente no modificado para plantilla {$plantilla_id}. Valor: {$comando}", 'INFO');
|
||||
}
|
||||
} elseif (!empty($comando) && !$comandoExistente) {
|
||||
// Caso: Se está añadiendo un comando nuevo
|
||||
logToFile('discord/templates.log', "Añadiendo nuevo comando para plantilla {$plantilla_id}. Comando: {$comando}", 'INFO');
|
||||
$stmt = $db->prepare("INSERT INTO comandos_discord (comando, descripcion, plantilla_id) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$comando, $nombre, $plantilla_id]);
|
||||
} elseif (empty($comando) && $comandoExistente) {
|
||||
// Caso: Se está eliminando el comando
|
||||
logToFile('discord/templates.log', "Eliminando comando existente para plantilla {$plantilla_id}. Comando: {$comandoExistente['comando']}", 'INFO');
|
||||
$stmt = $db->prepare("DELETE FROM comandos_discord WHERE id = ?");
|
||||
$stmt->execute([$comandoExistente['id']]);
|
||||
} else {
|
||||
logToFile('discord/templates.log', "No se gestionó comando para plantilla {$plantilla_id}. Comando recibido: {$comando}, Existente: " . ($comandoExistente ? $comandoExistente['comando'] : 'Ninguno'), 'INFO');
|
||||
}
|
||||
// Si no hay comando nuevo y no había uno existente, no se hace nada.
|
||||
|
||||
$db->commit();
|
||||
|
||||
logToFile('discord/templates.log', "Edición de plantilla completada (ID: {$plantilla_id}).", 'INFO');
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Plantilla actualizada correctamente.'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
$code = $e->getCode() >= 400 ? $e->getCode() : 500;
|
||||
http_response_code($code);
|
||||
logToFile('discord/templates.log', 'ERROR: Error editando plantilla (ID: ' . ($plantilla_id ?? 'N/A') . '): ' . $e->getMessage(), 'ERROR');
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
64
discord/api/templates/list.php
Executable file
64
discord/api/templates/list.php
Executable file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* API de Plantillas de Discord - Listar
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Para depuración
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// Verificar autenticación
|
||||
if (!isAuthenticated()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'No autenticado']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verificar permiso
|
||||
if (!hasPermission('view_templates', 'discord')) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'No tienes permiso para ver las plantillas de Discord.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
|
||||
// Búsqueda (opcional, para futuras mejoras)
|
||||
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||
|
||||
$sql = "
|
||||
SELECT p.id, p.nombre, p.comando, p.fecha_modificacion, u.username
|
||||
FROM plantillas_discord p
|
||||
LEFT JOIN usuarios u ON p.usuario_id = u.id
|
||||
";
|
||||
|
||||
$params = [];
|
||||
if (!empty($search)) {
|
||||
$sql .= " WHERE p.nombre LIKE ? OR p.comando LIKE ?";
|
||||
$params[] = "%{$search}%";
|
||||
$params[] = "%{$search}%";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY p.fecha_modificacion DESC";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$templates = $stmt->fetchAll();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'templates' => $templates
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
error_log('Error en /discord/api/templates/list.php: ' . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'error' => 'Error del servidor al obtener las plantillas.']);
|
||||
}
|
||||
36
discord/api/templates/php_errors.log
Executable file
36
discord/api/templates/php_errors.log
Executable file
@@ -0,0 +1,36 @@
|
||||
[04-Dec-2025 14:59:12 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 14:59:12 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 14:59:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:57:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:57:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:57:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:57:13 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:58:35 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:58:35 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:58:35 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:58:35 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:58:49 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:58:49 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:58:49 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:58:49 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 15:59:17 America/Mexico_City] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/discord/api/templates/delete.php:19
|
||||
Stack trace:
|
||||
#0 {main}
|
||||
thrown in /var/www/html/bot/discord/api/templates/delete.php on line 19
|
||||
[04-Dec-2025 15:59:57 America/Mexico_City] PHP Fatal error: Uncaught Error: Call to undefined function isAuthenticated() in /var/www/html/bot/discord/api/templates/delete.php:19
|
||||
Stack trace:
|
||||
#0 {main}
|
||||
thrown in /var/www/html/bot/discord/api/templates/delete.php on line 19
|
||||
[04-Dec-2025 22:04:20 UTC] PHP Parse error: syntax error, unexpected token "=" in /var/www/html/bot/discord/api/templates/delete.php on line 20
|
||||
[04-Dec-2025 22:05:28 UTC] PHP Parse error: syntax error, unexpected token "exit", expecting "]" in /var/www/html/bot/discord/api/templates/delete.php on line 24
|
||||
[04-Dec-2025 22:05:56 UTC] PHP Parse error: syntax error, unexpected token "exit", expecting "]" in /var/www/html/bot/discord/api/templates/delete.php on line 24
|
||||
[04-Dec-2025 22:06:02 UTC] PHP Parse error: syntax error, unexpected token "exit", expecting "]" in /var/www/html/bot/discord/api/templates/delete.php on line 24
|
||||
[04-Dec-2025 22:08:10 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 26
|
||||
[04-Dec-2025 22:11:10 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 26
|
||||
[04-Dec-2025 22:13:26 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||
[04-Dec-2025 22:28:45 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||
[04-Dec-2025 22:31:33 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||
[04-Dec-2025 22:32:17 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||
[04-Dec-2025 22:32:21 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||
[04-Dec-2025 22:32:47 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 18
|
||||
[04-Dec-2025 22:34:50 UTC] PHP Parse error: syntax error, unexpected token "\" in /var/www/html/bot/discord/api/templates/delete.php on line 19
|
||||
143
discord/api/welcome/send_test.php
Executable file
143
discord/api/welcome/send_test.php
Executable file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
/**
|
||||
* API - Enviar Mensaje de Prueba de Bienvenida
|
||||
*/
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::authenticate();
|
||||
if (!$userData) {
|
||||
jsonResponse(['success' => false, 'error' => 'No autenticado'], 401);
|
||||
}
|
||||
|
||||
// Verificar permiso
|
||||
if (!hasPermission('manage_welcome', 'discord')) {
|
||||
jsonResponse(['success' => false, 'error' => 'No tienes permiso para gestionar el mensaje de bienvenida de Discord.'], 403);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['success' => false, 'error' => 'Método no permitido'], 405);
|
||||
}
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
|
||||
// 1. Obtener configuración de bienvenida
|
||||
$stmt = $db->query("
|
||||
SELECT b.*, g.ruta as imagen_ruta
|
||||
FROM bienvenida_discord b
|
||||
LEFT JOIN gallery g ON b.imagen_id = g.id
|
||||
LIMIT 1
|
||||
");
|
||||
$config = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$config) {
|
||||
throw new Exception("No hay configuración de bienvenida guardada");
|
||||
}
|
||||
|
||||
if (empty($config['canal_id'])) {
|
||||
throw new Exception("No se ha configurado un canal de bienvenida");
|
||||
}
|
||||
|
||||
// 2. Obtener idiomas activos para los botones
|
||||
$stmt = $db->query("SELECT codigo, nombre, nombre_nativo, bandera FROM idiomas WHERE activo = 1 ORDER BY nombre ASC");
|
||||
$idiomas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Preparar contenido del mensaje
|
||||
$contenido = $config['texto'] ?? "Bienvenido al servidor!";
|
||||
|
||||
// Reemplazar variables
|
||||
$dummyUser = "<@{$userData->id}> (Usuario de Prueba)";
|
||||
$contenido = str_replace('{usuario}', $dummyUser, $contenido);
|
||||
|
||||
// Limpiar HTML a texto plano (similar a send.php)
|
||||
$cleanContent = str_replace(['<br>', '<br/>', '<p>'], ["\n", "\n", "\n"], $contenido);
|
||||
$cleanContent = strip_tags($cleanContent);
|
||||
$cleanContent = html_entity_decode($cleanContent);
|
||||
$cleanContent = trim($cleanContent);
|
||||
|
||||
$data = [
|
||||
'content' => $cleanContent
|
||||
];
|
||||
|
||||
// 4. (Imagen eliminada por solicitud)
|
||||
// if (!empty($config['imagen_ruta'])) { ... }
|
||||
|
||||
// 5. Agregar botones de idioma (Components)
|
||||
if (!empty($idiomas)) {
|
||||
$components = [];
|
||||
$currentRow = ['type' => 1, 'components' => []];
|
||||
|
||||
foreach ($idiomas as $index => $lang) {
|
||||
// Discord permite max 5 botones por fila, max 5 filas
|
||||
if (count($currentRow['components']) >= 5) {
|
||||
$components[] = $currentRow;
|
||||
$currentRow = ['type' => 1, 'components' => []];
|
||||
}
|
||||
|
||||
// Usar bandera si existe, sino nombre nativo, sino nombre
|
||||
$label = $lang['bandera'] ?: ($lang['nombre_nativo'] ?: $lang['nombre']);
|
||||
|
||||
$currentRow['components'][] = [
|
||||
'type' => 2, // Button
|
||||
'style' => 1, // Primary (Blurple)
|
||||
'label' => $label,
|
||||
'custom_id' => "lang_select_" . $lang['codigo']
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($currentRow['components'])) {
|
||||
$components[] = $currentRow;
|
||||
}
|
||||
|
||||
$data['components'] = $components;
|
||||
}
|
||||
|
||||
// 6. Enviar a Discord
|
||||
$botToken = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||
$url = "https://discord.com/api/v10/channels/{$config['canal_id']}/messages";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $botToken,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError) {
|
||||
throw new Exception("Error cURL: " . $curlError);
|
||||
}
|
||||
|
||||
$responseJson = json_decode($response, true);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
$errorMsg = $responseJson['message'] ?? 'Error desconocido de Discord';
|
||||
if (isset($responseJson['errors'])) {
|
||||
$errorMsg .= ' - ' . json_encode($responseJson['errors']);
|
||||
}
|
||||
throw new Exception("Discord API Error ({$httpCode}): {$errorMsg}");
|
||||
}
|
||||
|
||||
jsonResponse([
|
||||
'success' => true,
|
||||
'message' => 'Mensaje de prueba enviado correctamente',
|
||||
'debug_url' => $imageUrl ?? 'No image'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
211
discord/dashboard_discord.php
Executable file
211
discord/dashboard_discord.php
Executable file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../shared/bootstrap.php';
|
||||
|
||||
// El bootstrap.php ya maneja la autenticación y carga $userData
|
||||
// $userData está disponible globalmente a través de JWTAuth::getUserData() si se necesita de nuevo
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo __('discord_dashboard_title'); ?></title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--discord-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.modules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.module-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.module-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.module-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 15px;
|
||||
color: var(--discord-color);
|
||||
}
|
||||
|
||||
.module-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.module-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>💬 <?php echo __('discord_dashboard_header'); ?></h1>
|
||||
<a href="/index.php" class="btn-back">← <?php echo __('back_to_main_dashboard'); ?></a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="modules-grid">
|
||||
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||
<a href="/discord/views/templates/list.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-file-alt"></i></div>
|
||||
<div class="module-title"><?php echo __('templates_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('templates_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('send_messages', 'discord')): ?>
|
||||
<a href="/discord/views/messages/create.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-paper-plane"></i></div>
|
||||
<div class="module-title"><?php echo __('create_message_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('create_message_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('view_sent_messages', 'discord')): ?>
|
||||
<a href="/discord/views/messages/sent.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-history"></i></div>
|
||||
<div class="module-title"><?php echo __('sent_messages_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('sent_messages_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('manage_recipients', 'discord')): ?>
|
||||
<a href="/discord/views/recipients/list.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-users"></i></div>
|
||||
<div class="module-title"><?php echo __('recipients_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('recipients_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('manage_commands', 'discord')): ?>
|
||||
<a href="/discord/views/commands/list.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-terminal"></i></div>
|
||||
<div class="module-title"><?php echo __('commands_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('commands_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('manage_welcome', 'discord')): ?>
|
||||
<a href="/discord/views/welcome/config.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-handshake"></i></div>
|
||||
<div class="module-title"><?php echo __('welcome_message_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('welcome_message_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('view_logs', 'discord')): ?>
|
||||
<a href="/discord/views/logs/list.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-list-alt"></i></div>
|
||||
<div class="module-title"><?php echo __('system_logs_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('system_logs_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('view_gallery')): ?>
|
||||
<a href="/gallery/index.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-images"></i></div>
|
||||
<div class="module-title"><?php echo __('gallery_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('gallery_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('manage_languages')): ?>
|
||||
<a href="/shared/languages/manager.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-language"></i></div>
|
||||
<div class="module-title"><?php echo __('languages_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('languages_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('test_connection', 'discord')): ?>
|
||||
<a href="/discord/test_connection.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-plug"></i></div>
|
||||
<div class="module-title"><?php echo __('connection_test_module_title'); ?></div>
|
||||
<div class="module-description"><?php echo __('connection_test_module_description'); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('view_logs', 'discord')): ?>
|
||||
<a href="/discord/views/features.php" class="module-card">
|
||||
<div class="module-icon"><i class="fas fa-robot"></i></div>
|
||||
<div class="module-title">Funciones del Bot</div>
|
||||
<div class="module-description">Documentación de las funciones automáticas y comandos del bot.</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
55
discord/register_commands.php
Executable file
55
discord/register_commands.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Script para registrar los Slash Commands en Discord.
|
||||
* Ejecutar manualmente una sola vez o cuando los comandos cambien.
|
||||
* php discord/register_commands.php
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Discord\Discord;
|
||||
use Discord\Builders\Components\ActionRow;
|
||||
use Discord\Builders\Components\Button;
|
||||
use Discord\Builders\MessageBuilder;
|
||||
use Discord\Builders\CommandBuilder;
|
||||
use Discord\Parts\Interactions\Command\Command;
|
||||
|
||||
// Cargar variables de entorno
|
||||
if (file_exists(__DIR__ . '/../.env')) {
|
||||
$lines = file(__DIR__ . '/../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos(trim($line), '#') === 0) continue;
|
||||
if (strpos($line, '=') === false) continue;
|
||||
list($key, $value) = explode('=', $line, 2);
|
||||
$_ENV[trim($key)] = trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
$discord = new Discord([
|
||||
'token' => $_ENV['DISCORD_BOT_TOKEN'],
|
||||
]);
|
||||
|
||||
$discord->on('ready', function (Discord $discord) {
|
||||
echo "Bot de registro iniciado..." . PHP_EOL;
|
||||
|
||||
// Crear el comando
|
||||
$command = new Command($discord, [
|
||||
'name' => 'start',
|
||||
'description' => 'Muestra el mensaje de bienvenida y las opciones de idioma.',
|
||||
]);
|
||||
|
||||
// Registrar el comando globalmente
|
||||
$discord->application->commands->save($command)->then(
|
||||
function (Command $command) {
|
||||
echo "Comando '/{$command->name}' registrado exitosamente!" . PHP_EOL;
|
||||
},
|
||||
function (\Exception $e) {
|
||||
echo "Error al registrar el comando: " . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
)->done(function() use ($discord) {
|
||||
echo "Registro de comandos completado. Cerrando." . PHP_EOL;
|
||||
$discord->close();
|
||||
});
|
||||
});
|
||||
|
||||
$discord->run();
|
||||
95
discord/src/CommandLocker.php
Executable file
95
discord/src/CommandLocker.php
Executable file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
class CommandLocker {
|
||||
private $pdo;
|
||||
private $lockTimeout = 300; // 5 minutos de tiempo de espera para el bloqueo
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
}
|
||||
|
||||
private function log($message, $data = []) {
|
||||
// This will be handled by the main bot's logger
|
||||
// For now, do nothing.
|
||||
}
|
||||
|
||||
public function acquireLock($command, $chatId, $type = 'command', $data = []) {
|
||||
$this->log("Intentando adquirir bloqueo", ['command' => $command, 'chatId' => $chatId, 'type' => $type]);
|
||||
$this->cleanupExpiredLocks();
|
||||
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
$query = "SELECT id, status, created_at FROM command_locks WHERE command = ? AND chat_id = ? ORDER BY created_at DESC LIMIT 1 FOR UPDATE";
|
||||
$stmt = $this->pdo->prepare($query);
|
||||
$stmt->execute([$command, $chatId]);
|
||||
$existingLock = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existingLock && $existingLock['status'] === 'processing' && strtotime($existingLock['created_at']) > strtotime('-5 minutes')) {
|
||||
$this->log("Bloqueo ya en proceso, rechazando nueva solicitud");
|
||||
$this->pdo->rollBack();
|
||||
return false;
|
||||
}
|
||||
|
||||
$expiresAt = (new DateTime('+5 minutes'))->format('Y-m-d H:i:s');
|
||||
$dataJson = !empty($data) ? json_encode($data) : null;
|
||||
|
||||
if ($existingLock) {
|
||||
$upd = $this->pdo->prepare("UPDATE command_locks SET type = ?, status='processing', data = ?, expires_at = ?, updated_at = NOW() WHERE id = ?");
|
||||
$upd->execute([$type, $dataJson, $expiresAt, $existingLock['id']]);
|
||||
$this->pdo->commit();
|
||||
return (int)$existingLock['id'];
|
||||
} else {
|
||||
$query = "INSERT INTO command_locks (chat_id, command, type, data, status, expires_at, created_at, updated_at) VALUES (?, ?, ?, ?, 'processing', ?, NOW(), NOW())";
|
||||
$stmt = $this->pdo->prepare($query);
|
||||
$stmt->execute([$chatId, $command, $type, $dataJson, $expiresAt]);
|
||||
$lockId = $this->pdo->lastInsertId();
|
||||
$this->pdo->commit();
|
||||
return (int)$lockId;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->log("Error al adquirir bloqueo", ['error' => $e->getMessage()]);
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function updateLockStatus($lockId, $status, $messageId = null) {
|
||||
$this->log("Actualizando estado de bloqueo", ['lockId' => $lockId, 'status' => $status]);
|
||||
try {
|
||||
$query = "UPDATE command_locks SET status = ?, message_id = COALESCE(?, message_id), updated_at = NOW() WHERE id = ?";
|
||||
$stmt = $this->pdo->prepare($query);
|
||||
return $stmt->execute([$status, $messageId, $lockId]);
|
||||
} catch (Exception $e) {
|
||||
$this->log("Error al actualizar estado de bloqueo", ['error' => $e->getMessage()]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function releaseLock($lockId, $messageId = null) {
|
||||
return $this->updateLockStatus($lockId, 'completed', $messageId);
|
||||
}
|
||||
|
||||
public function failLock($lockId, $errorMessage = '') {
|
||||
$this->log("Marcando bloqueo como fallido", ['lockId' => $lockId, 'errorMessage' => $errorMessage]);
|
||||
try {
|
||||
$query = "UPDATE command_locks SET status = 'failed', data = JSON_SET(COALESCE(data, '{}'), '$.error', ?) WHERE id = ?";
|
||||
$stmt = $this->pdo->prepare($query);
|
||||
return $stmt->execute([$errorMessage, $lockId]);
|
||||
} catch (Exception $e) {
|
||||
$this->log("Error al marcar bloqueo como fallido", ['error' => $e->getMessage()]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupExpiredLocks() {
|
||||
try {
|
||||
$this->pdo->exec("DELETE FROM command_locks WHERE expires_at <= NOW()");
|
||||
} catch (Exception $e) {
|
||||
$this->log("Error al limpiar bloqueos expirados", ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
discord/src/DiscordSender.php
Executable file
76
discord/src/DiscordSender.php
Executable file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
class DiscordSender
|
||||
{
|
||||
private const API_BASE_URL = 'https://discord.com/api/v10';
|
||||
private string $token;
|
||||
|
||||
public function __construct(string $token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function sendMessage(string $channelId, string $htmlContent) {
|
||||
$converter = new HtmlToDiscordMarkdownConverter();
|
||||
$markdown = $converter->convert($htmlContent);
|
||||
|
||||
$sentMessageIds = [];
|
||||
|
||||
// Simple implementation: send the whole markdown.
|
||||
// The original had complex logic to split text and images.
|
||||
// This is a simplification to get it working first.
|
||||
try {
|
||||
$response = $this->sendApiRequest("/channels/{$channelId}/messages", ['content' => $markdown]);
|
||||
if (isset($response['id'])) {
|
||||
$sentMessageIds[] = $response['id'];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("DiscordSender Error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return $sentMessageIds;
|
||||
}
|
||||
|
||||
public function sendRawMessage(string $channelId, string $content): ?array
|
||||
{
|
||||
try {
|
||||
return $this->sendApiRequest("/channels/{$channelId}/messages", ['content' => $content]);
|
||||
} catch (Exception $e) {
|
||||
error_log("DiscordSender Error: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function sendApiRequest(string $endpoint, array $payload, string $method = 'POST') {
|
||||
$url = self::API_BASE_URL . $endpoint;
|
||||
|
||||
$ch = curl_init($url);
|
||||
$headers = [
|
||||
'Authorization: Bot ' . $this->token,
|
||||
'User-Agent: BotDiscord (https://github.com/nickpons/bot, 1.0)'
|
||||
];
|
||||
|
||||
if ($method === 'POST') {
|
||||
$headers[] = 'Content-Type: application/json';
|
||||
$postData = json_encode($payload);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$responseData = json_decode($response, true);
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
throw new Exception("Discord API error ({$httpCode}): " . ($responseData['message'] ?? 'Unknown error'));
|
||||
}
|
||||
|
||||
return $responseData;
|
||||
}
|
||||
}
|
||||
18
discord/src/HtmlToDiscordMarkdownConverter.php
Executable file
18
discord/src/HtmlToDiscordMarkdownConverter.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
class HtmlToDiscordMarkdownConverter
|
||||
{
|
||||
public function convert(string $html): string
|
||||
{
|
||||
return "test";
|
||||
}
|
||||
|
||||
public function convertToArray(string $html): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function absoluteUrl($url) {
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
60
discord/src/Translate.php
Executable file
60
discord/src/Translate.php
Executable file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
class Translate
|
||||
{
|
||||
private $apiUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiUrl = rtrim($_ENV['LIBRETRANSLATE_URL'], '/');
|
||||
}
|
||||
|
||||
public function detectLanguage($text)
|
||||
{
|
||||
if (empty(trim($text))) {
|
||||
return null;
|
||||
}
|
||||
$response = $this->request('/detect', ['q' => $text]);
|
||||
return $response[0]['language'] ?? null;
|
||||
}
|
||||
|
||||
public function translateText($text, $source, $target)
|
||||
{
|
||||
if (empty(trim($text))) {
|
||||
return null;
|
||||
}
|
||||
$response = $this->request('/translate', [
|
||||
'q' => $text,
|
||||
'source' => $source,
|
||||
'target' => $target,
|
||||
'format' => 'text'
|
||||
]);
|
||||
return $response['translatedText'] ?? null;
|
||||
}
|
||||
|
||||
private function request($endpoint, $data, $method = 'POST')
|
||||
{
|
||||
$url = $this->apiUrl . $endpoint;
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
|
||||
if ($method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
}
|
||||
|
||||
$response_body = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($http_code >= 400) {
|
||||
// In case of an error, log it or handle it
|
||||
error_log("LibreTranslate API error. HTTP Code: {$http_code}, Response: {$response_body}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode($response_body, true);
|
||||
}
|
||||
}
|
||||
5
discord/test_access.php
Executable file
5
discord/test_access.php
Executable file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
echo "<h1>Test Discord Folder</h1>";
|
||||
echo "<p>Si ves esto, la carpeta es accesible.</p>";
|
||||
echo "<p>Ruta actual: " . __DIR__ . "</p>";
|
||||
?>
|
||||
242
discord/test_connection.php
Executable file
242
discord/test_connection.php
Executable file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
/**
|
||||
* Test de Conexión con Discord
|
||||
*/
|
||||
|
||||
// Cargar variables de entorno
|
||||
if (file_exists(__DIR__ . '/../.env')) {
|
||||
$lines = file(__DIR__ . '/../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos(trim($line), '#') === 0) continue;
|
||||
if (strpos($line, '=') === false) continue;
|
||||
list($key, $value) = explode('=', $line, 2);
|
||||
$_ENV[trim($key)] = trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../shared/auth/jwt.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::requireAuth();
|
||||
|
||||
$bot_token = $_ENV['DISCORD_BOT_TOKEN'] ?? getenv('DISCORD_BOT_TOKEN');
|
||||
$guild_id = $_ENV['DISCORD_GUILD_ID'] ?? getenv('DISCORD_GUILD_ID');
|
||||
|
||||
$testResults = [];
|
||||
$allSuccess = true;
|
||||
|
||||
// Test 1: Verificar token
|
||||
$testResults[] = [
|
||||
'test' => 'Verificar token configurado',
|
||||
'success' => !empty($bot_token),
|
||||
'message' => !empty($bot_token) ? 'Token configurado correctamente' : 'Token no configurado'
|
||||
];
|
||||
|
||||
// Test 2: Obtener información del bot
|
||||
if (!empty($bot_token)) {
|
||||
$ch = curl_init('https://discord.com/api/v10/users/@me');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $bot_token,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$success = $httpCode === 200;
|
||||
$allSuccess = $allSuccess && $success;
|
||||
|
||||
if ($success) {
|
||||
$botInfo = json_decode($response, true);
|
||||
$testResults[] = [
|
||||
'test' => 'Conectar con Discord API',
|
||||
'success' => true,
|
||||
'message' => 'Conectado como: ' . $botInfo['username'] . '#' . $botInfo['discriminator'],
|
||||
'data' => $botInfo
|
||||
];
|
||||
} else {
|
||||
$testResults[] = [
|
||||
'test' => 'Conectar con Discord API',
|
||||
'success' => false,
|
||||
'message' => 'Error HTTP ' . $httpCode . ': ' . $response
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Obtener información del servidor/guild
|
||||
if (!empty($bot_token) && !empty($guild_id)) {
|
||||
$ch = curl_init("https://discord.com/api/v10/guilds/{$guild_id}");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $bot_token,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$success = $httpCode === 200;
|
||||
$allSuccess = $allSuccess && $success;
|
||||
|
||||
if ($success) {
|
||||
$guildInfo = json_decode($response, true);
|
||||
$testResults[] = [
|
||||
'test' => 'Acceder al servidor Discord',
|
||||
'success' => true,
|
||||
'message' => 'Servidor: ' . $guildInfo['name'] . ' (Miembros: ' . ($guildInfo['approximate_member_count'] ?? 'N/A') . ')',
|
||||
'data' => $guildInfo
|
||||
];
|
||||
} else {
|
||||
$testResults[] = [
|
||||
'test' => 'Acceder al servidor Discord',
|
||||
'success' => false,
|
||||
'message' => 'Error HTTP ' . $httpCode . ': ' . $response
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Test 4: Listar canales
|
||||
if (!empty($bot_token) && !empty($guild_id)) {
|
||||
$ch = curl_init("https://discord.com/api/v10/guilds/{$guild_id}/channels");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bot ' . $bot_token,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$success = $httpCode === 200;
|
||||
$allSuccess = $allSuccess && $success;
|
||||
|
||||
if ($success) {
|
||||
$channels = json_decode($response, true);
|
||||
$textChannels = array_filter($channels, fn($c) => $c['type'] === 0);
|
||||
$testResults[] = [
|
||||
'test' => 'Listar canales',
|
||||
'success' => true,
|
||||
'message' => 'Se encontraron ' . count($textChannels) . ' canales de texto',
|
||||
'data' => array_slice($textChannels, 0, 5)
|
||||
];
|
||||
} else {
|
||||
$testResults[] = [
|
||||
'test' => 'Listar canales',
|
||||
'success' => false,
|
||||
'message' => 'Error HTTP ' . $httpCode
|
||||
];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Discord - Sistema de Bots</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #5865F2 0%, #4752C4 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
h1 {
|
||||
color: #5865F2;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.test-result {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 5px solid #ddd;
|
||||
}
|
||||
.test-result.success {
|
||||
border-left-color: #28a745;
|
||||
}
|
||||
.test-result.error {
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
.test-name {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.test-message {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.badge-success { background: #d4edda; color: #155724; }
|
||||
.badge-error { background: #f8d7da; color: #721c24; }
|
||||
.btn-back {
|
||||
display: inline-block;
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.summary {
|
||||
background: <?php echo $allSuccess ? '#d4edda' : '#f8d7da'; ?>;
|
||||
color: <?php echo $allSuccess ? '#155724' : '#721c24'; ?>;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧪 Test de Conexión con Discord</h1>
|
||||
|
||||
<div class="summary">
|
||||
<?php if ($allSuccess): ?>
|
||||
✅ Todos los tests pasaron correctamente
|
||||
<?php else: ?>
|
||||
❌ Algunos tests fallaron
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php foreach ($testResults as $result): ?>
|
||||
<div class="test-result <?php echo $result['success'] ? 'success' : 'error'; ?>">
|
||||
<div class="test-name">
|
||||
<?php echo htmlspecialchars($result['test']); ?>
|
||||
<span class="badge <?php echo $result['success'] ? 'badge-success' : 'badge-error'; ?>">
|
||||
<?php echo $result['success'] ? 'OK' : 'ERROR'; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="test-message"><?php echo htmlspecialchars($result['message']); ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<a href="/discord/dashboard_discord.php" class="btn-back">← Volver al Dashboard</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1
discord/test_hello.php
Executable file
1
discord/test_hello.php
Executable file
@@ -0,0 +1 @@
|
||||
<?php echo 'Hello from discord dir'; ?>
|
||||
304
discord/views/commands/list.php
Executable file
304
discord/views/commands/list.php
Executable file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Habilitar logging para depuración
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
$userData = JWTAuth::requireAuth();
|
||||
|
||||
// Verificar permiso para ver la página de comandos
|
||||
if (!hasPermission('view_commands', 'discord')) {
|
||||
die('No tienes permiso para ver los comandos de Discord.');
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// Obtener comandos con información de la plantilla asociada
|
||||
$stmt = $db->query("
|
||||
SELECT c.id, p.nombre, c.comando, c.fecha_creacion, c.descripcion
|
||||
FROM comandos_discord c
|
||||
LEFT JOIN plantillas_discord p ON c.plantilla_id = p.id
|
||||
ORDER BY c.comando ASC
|
||||
");
|
||||
$comandos = $stmt->fetchAll();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Comandos Discord - Sistema de Bots</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--discord-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.command-tag {
|
||||
background: #2b2d31;
|
||||
color: #dbdee1;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--discord-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--discord-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
margin: 10% auto;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #eee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-terminal"></i> Comandos Discord</h1>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||
<a href="/discord/views/templates/create.php" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Nuevo Comando (Plantilla)
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<?php if (empty($comandos)): ?>
|
||||
<div style="text-align: center; padding: 40px; color: #666;">
|
||||
<i class="fas fa-terminal" style="font-size: 48px; margin-bottom: 20px; color: #ddd;"></i>
|
||||
<h3>No hay comandos configurados</h3>
|
||||
<p>Los comandos se definen al crear o editar una plantilla.</p>
|
||||
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||
<a href="/discord/views/templates/create.php" class="btn btn-primary" style="margin-top: 10px;">
|
||||
Ir a Plantillas
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Comando</th>
|
||||
<th>Plantilla Asociada</th>
|
||||
<th>Fecha Creación</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($comandos as $cmd): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="command-tag"><?php echo htmlspecialchars($cmd['comando']); ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fas fa-file-alt"></i> <?php echo htmlspecialchars($cmd['nombre']); ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo date('d/m/Y', strtotime($cmd['fecha_creacion'])); ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||
<a href="/discord/views/templates/edit.php?id=<?php echo $cmd['id']; ?>" class="btn btn-edit">
|
||||
<i class="fas fa-edit"></i> Editar
|
||||
</a>
|
||||
<button onclick="deleteCommand(<?php echo $cmd['id']; ?>, '<?php echo htmlspecialchars($cmd['comando'], ENT_QUOTES); ?>')" class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> Eliminar
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts eliminados ya que no se necesita modal local -->
|
||||
|
||||
<script>
|
||||
async function deleteCommand(templateId, commandName) {
|
||||
if (!confirm(`¿Estás seguro de eliminar el comando "${commandName}"? Esto eliminará también la plantilla asociada.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/templates/delete.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: templateId })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Comando y plantilla eliminados correctamente.');
|
||||
location.reload(); // Recargar la página para actualizar la lista
|
||||
} else {
|
||||
alert('Error al eliminar: ' + (result.error || 'Error desconocido.'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al enviar la solicitud de eliminación:', error);
|
||||
alert('Error de conexión al intentar eliminar el comando.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
5
discord/views/commands/php_errors.log
Executable file
5
discord/views/commands/php_errors.log
Executable file
@@ -0,0 +1,5 @@
|
||||
[04-Dec-2025 16:44:49 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'comando' in 'field list' in /var/www/html/bot/discord/views/commands/list.php:23
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/discord/views/commands/list.php(23): PDO->query()
|
||||
#1 {main}
|
||||
thrown in /var/www/html/bot/discord/views/commands/list.php on line 23
|
||||
113
discord/views/features.php
Executable file
113
discord/views/features.php
Executable file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../shared/bootstrap.php';
|
||||
|
||||
// El bootstrap.php ya maneja la autenticación
|
||||
if (!hasPermission('view_logs', 'discord')) {
|
||||
die('No tienes permiso para ver esta página.');
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Funciones del Bot de Discord</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background: white; border-radius: 15px; padding: 20px 30px;
|
||||
margin-bottom: 30px; display: flex; justify-content: space-between;
|
||||
align-items: center; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header h1 { color: var(--discord-color); font-size: 24px; }
|
||||
.btn-back {
|
||||
background: #6c757d; color: white; padding: 10px 20px;
|
||||
border-radius: 8px; text-decoration: none; transition: transform 0.2s;
|
||||
}
|
||||
.btn-back:hover { transform: translateY(-2px); }
|
||||
.container { max-width: 1000px; margin: 0 auto; }
|
||||
.card {
|
||||
background: white; border-radius: 15px; padding: 30px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); margin-bottom: 20px;
|
||||
}
|
||||
.feature {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.feature:last-child { border-bottom: none; }
|
||||
.feature-title { font-size: 18px; font-weight: 700; color: #333; margin-bottom: 8px; }
|
||||
.feature-event { font-size: 14px; color: var(--discord-color); font-family: monospace; margin-bottom: 8px; }
|
||||
.feature-description { font-size: 15px; color: #666; line-height: 1.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-robot"></i> Funciones del Bot de Discord</h1>
|
||||
<a href="/discord/dashboard_discord.php" class="btn-back">← Volver al Dashboard</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
|
||||
<div class="feature">
|
||||
<div class="feature-title">Mensajes de Bienvenida a Nuevos Miembros</div>
|
||||
<div class="feature-event">Evento: Un nuevo usuario se une al servidor</div>
|
||||
<p class="feature-description">
|
||||
Cuando un nuevo miembro se une, el bot le da la bienvenida en un canal específico. Este mensaje es totalmente configurable desde el panel de administración, permitiendo cambiar el texto, añadir una imagen y habilitar botones para que el usuario seleccione su idioma preferido.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<div class="feature-title">Sistema de Plantillas y Comandos</div>
|
||||
<div class="feature-event">Evento: Un usuario escribe un comando como #ayuda o /comandos</div>
|
||||
<p class="feature-description">
|
||||
El bot puede responder a comandos que empiezan con <code>#</code>. Cada comando está asociado a una plantilla de texto predefinida.
|
||||
<br>- <strong>Comandos de Plantilla:</strong> Al usar un comando como <code>#reglas</code>, el bot envía el contenido de esa plantilla al canal.
|
||||
<br>- <strong>Traducción de Plantillas:</strong> Junto con la plantilla, el bot añade botones para que los usuarios puedan traducirla instantáneamente a los idiomas activos.
|
||||
<br>- <strong>Listar Comandos:</strong> El comando <code>/comandos</code> muestra una lista de todos los comandos de plantilla disponibles.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<div class="feature-title">Traducción Automática de Mensajes</div>
|
||||
<div class="feature-event">Evento: Al recibir un mensaje que no es un comando</div>
|
||||
<p class="feature-description">
|
||||
Para romper las barreras del idioma, el bot añade un botón <strong>"Traducir / Translate"</strong> debajo de los mensajes que no son comandos. Al hacer clic, el bot traduce el mensaje original al idioma que el usuario haya configurado y se lo envía como un mensaje privado (efímero) que solo él puede ver.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<div class="feature-title">Gestión de Preferencias de Idioma</div>
|
||||
<div class="feature-event">Evento: Clic en un botón de selección de idioma</div>
|
||||
<p class="feature-description">
|
||||
El bot permite a los usuarios establecer su idioma preferido.
|
||||
<br>- <strong>Botones de Bienvenida:</strong> Al hacer clic en los botones de idioma del mensaje de bienvenida, el bot guarda la preferencia del usuario.
|
||||
<br>- <strong>Traducciones Personalizadas:</strong> Esta preferencia se usa para saber a qué idioma traducir cuando el usuario utiliza una función de traducción.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<div class="feature-title">Registro de Usuarios para Mejor Interacción</div>
|
||||
<div class="feature-event">Evento: Al unirse un miembro o interactuar por primera vez</div>
|
||||
<p class="feature-description">
|
||||
Para personalizar la experiencia, el bot guarda información básica de los usuarios.
|
||||
<br>- <strong>Al unirse al servidor:</strong> Si está activado en la configuración de bienvenida, el bot registra al nuevo miembro.
|
||||
<br>- <strong>Al seleccionar un idioma:</strong> El bot registra o actualiza al usuario que interactúa con los botones de idioma para guardar su preferencia.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
322
discord/views/logs/list.php
Executable file
322
discord/views/logs/list.php
Executable file
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Habilitar logging para depuración
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
$userData = JWTAuth::requireAuth();
|
||||
$db = getDB();
|
||||
|
||||
// Paginación
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$perPage = 20;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
// Filtros
|
||||
$nivel = isset($_GET['nivel']) ? $_GET['nivel'] : '';
|
||||
$origen = isset($_GET['origen']) ? $_GET['origen'] : '';
|
||||
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||
|
||||
// Construir Query
|
||||
$where = ["1=1"];
|
||||
$params = [];
|
||||
|
||||
if ($nivel) {
|
||||
$where[] = "nivel = ?";
|
||||
$params[] = $nivel;
|
||||
}
|
||||
|
||||
if ($origen) {
|
||||
$where[] = "origen = ?";
|
||||
$params[] = $origen;
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$where[] = "descripcion LIKE ?";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$whereClause = implode(" AND ", $where);
|
||||
|
||||
// Total para paginación
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM logs_discord WHERE $whereClause");
|
||||
$stmt->execute($params);
|
||||
$totalLogs = $stmt->fetchColumn();
|
||||
$totalPages = ceil($totalLogs / $perPage);
|
||||
|
||||
// Obtener logs
|
||||
$stmt = $db->prepare("
|
||||
SELECT l.*, u.username
|
||||
FROM logs_discord l
|
||||
LEFT JOIN usuarios u ON l.usuario_id = u.id
|
||||
WHERE $whereClause
|
||||
ORDER BY l.fecha DESC
|
||||
LIMIT $perPage OFFSET $offset
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$logs = $stmt->fetchAll();
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Logs Discord - Sistema de Bots</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
--bg-color: #f0f2f5;
|
||||
--text-color: #333;
|
||||
--success: #28a745;
|
||||
--warning: #ffc107;
|
||||
--danger: #dc3545;
|
||||
--info: #17a2b8;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg-color);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 { color: var(--discord-color); font-size: 24px; }
|
||||
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 10px;
|
||||
border: 2px solid #eee;
|
||||
border-radius: 8px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn-primary { background: var(--discord-color); color: white; }
|
||||
.btn-secondary { background: #6c757d; color: white; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 15px; text-align: left; border-bottom: 1px solid #eee; }
|
||||
th { background: #f8f9fa; color: #555; font-weight: 600; }
|
||||
|
||||
.badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
.badge-info { background: var(--info); }
|
||||
.badge-warning { background: var(--warning); color: #333; }
|
||||
.badge-error { background: var(--danger); }
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination a {
|
||||
padding: 8px 12px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
color: var(--discord-color);
|
||||
}
|
||||
|
||||
.pagination a.active {
|
||||
background: var(--discord-color);
|
||||
color: white;
|
||||
border-color: var(--discord-color);
|
||||
}
|
||||
|
||||
/* Modal JSON */
|
||||
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
|
||||
.modal-content { background: white; margin: 10% auto; padding: 25px; width: 80%; max-width: 800px; border-radius: 15px; position: relative; max-height: 80vh; overflow-y: auto; }
|
||||
.close-modal { position: absolute; top: 15px; right: 20px; font-size: 24px; cursor: pointer; color: #aaa; }
|
||||
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-list-alt"></i> Logs del Sistema</h1>
|
||||
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<form class="filters" method="GET">
|
||||
<input type="text" name="search" class="form-control" placeholder="Buscar en descripción..." value="<?php echo htmlspecialchars($search); ?>">
|
||||
|
||||
<select name="nivel" class="form-control">
|
||||
<option value="">-- Todos los Niveles --</option>
|
||||
<option value="info" <?php echo $nivel === 'info' ? 'selected' : ''; ?>>Info</option>
|
||||
<option value="warning" <?php echo $nivel === 'warning' ? 'selected' : ''; ?>>Warning</option>
|
||||
<option value="error" <?php echo $nivel === 'error' ? 'selected' : ''; ?>>Error</option>
|
||||
</select>
|
||||
|
||||
<select name="origen" class="form-control">
|
||||
<option value="">-- Todos los Orígenes --</option>
|
||||
<option value="sistema" <?php echo $origen === 'sistema' ? 'selected' : ''; ?>>Sistema</option>
|
||||
<option value="usuario" <?php echo $origen === 'usuario' ? 'selected' : ''; ?>>Usuario</option>
|
||||
<option value="bot" <?php echo $origen === 'bot' ? 'selected' : ''; ?>>Bot</option>
|
||||
<option value="webhook" <?php echo $origen === 'webhook' ? 'selected' : ''; ?>>Webhook</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-filter"></i> Filtrar
|
||||
</button>
|
||||
|
||||
<?php if ($nivel || $origen || $search): ?>
|
||||
<a href="list.php" class="btn btn-secondary">Limpiar</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Nivel</th>
|
||||
<th>Origen</th>
|
||||
<th>Descripción</th>
|
||||
<th>Usuario</th>
|
||||
<th>Detalles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($logs)): ?>
|
||||
<tr><td colspan="6" style="text-align:center; color:#666;">No hay registros encontrados.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($logs as $log): ?>
|
||||
<tr>
|
||||
<td style="font-size: 14px; color: #666;"><?php echo $log['fecha']; ?></td>
|
||||
<td>
|
||||
<span class="badge badge-<?php echo $log['nivel']; ?>">
|
||||
<?php echo strtoupper($log['nivel']); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo ucfirst($log['origen']); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr($log['descripcion'], 0, 100)) . (strlen($log['descripcion']) > 100 ? '...' : ''); ?></td>
|
||||
<td><?php echo $log['username'] ? htmlspecialchars($log['username']) : '-'; ?></td>
|
||||
<td>
|
||||
<?php if ($log['datos_json']): ?>
|
||||
<button class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px;"
|
||||
onclick='showDetails(<?php echo json_encode($log['datos_json']); ?>)'>
|
||||
<i class="fas fa-code"></i> JSON
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<span style="color:#ccc;">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Paginación -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||
<a href="?page=<?php echo $i; ?>&nivel=<?php echo $nivel; ?>&origen=<?php echo $origen; ?>&search=<?php echo $search; ?>"
|
||||
class="<?php echo $i === $page ? 'active' : ''; ?>">
|
||||
<?php echo $i; ?>
|
||||
</a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Detalles -->
|
||||
<div id="detailsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-modal" onclick="closeModal()">×</span>
|
||||
<h2>Detalles del Log</h2>
|
||||
<pre id="jsonContent"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showDetails(jsonString) {
|
||||
try {
|
||||
// Si ya es objeto, usarlo, si es string, parsearlo
|
||||
const obj = typeof jsonString === 'string' ? JSON.parse(jsonString) : jsonString;
|
||||
document.getElementById('jsonContent').textContent = JSON.stringify(obj, null, 2);
|
||||
document.getElementById('detailsModal').style.display = 'block';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error al parsear JSON');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('detailsModal').style.display = 'none';
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target == document.getElementById('detailsModal')) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
736
discord/views/messages/create.php
Executable file
736
discord/views/messages/create.php
Executable file
@@ -0,0 +1,736 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Habilitar logging para depuración
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
$userData = JWTAuth::requireAuth();
|
||||
|
||||
// Verificar permiso para enviar mensajes
|
||||
if (!hasPermission('send_messages', 'discord')) {
|
||||
die('No tienes permiso para crear y enviar mensajes de Discord.');
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// Obtener plantillas para el selector
|
||||
$stmt = $db->query("SELECT id, nombre, contenido FROM plantillas_discord ORDER BY nombre ASC");
|
||||
$plantillas = $stmt->fetchAll();
|
||||
|
||||
// Obtener destinatarios guardados (si existen)
|
||||
$stmt = $db->query("SELECT id, nombre, discord_id as identificador, tipo FROM destinatarios_discord ORDER BY nombre ASC");
|
||||
$destinatarios = $stmt->fetchAll();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crear Mensaje Discord - Sistema de Bots</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/4.1.5/css/flag-icons.min.css" />
|
||||
<style>
|
||||
/* Estilos para los botones de traducción */
|
||||
.translation-buttons {
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.translation-buttons .btn-translate {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: white;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.translation-buttons .btn-translate:hover {
|
||||
background-color: #e9ecef;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.translation-buttons .btn-translate:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.translation-buttons .flag-icon {
|
||||
width: 16px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 1px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.translation-buttons .loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--discord-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--discord-color);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--discord-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--discord-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
/* Select2 Customization */
|
||||
.select2-container--default .select2-selection--single {
|
||||
height: 45px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
line-height: 45px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 43px;
|
||||
}
|
||||
|
||||
/* Modal Galería */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
margin: 50px auto;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
cursor: pointer;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.gallery-item:hover {
|
||||
border-color: var(--discord-color);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-paper-plane"></i> Crear Mensaje Discord</h1>
|
||||
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="form-container">
|
||||
<form id="messageForm">
|
||||
<div class="form-group">
|
||||
<label for="destinatario_id">Destinatarios (Canal ID o Usuario ID) *</label>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<div style="flex-grow: 1;">
|
||||
<select id="destinatario_select" class="form-control" style="width: 100%;" multiple="multiple">
|
||||
<?php foreach ($destinatarios as $dest): ?>
|
||||
<option value="<?php echo htmlspecialchars($dest['identificador']); ?>">
|
||||
<?php echo htmlspecialchars($dest['nombre']); ?> (<?php echo $dest['tipo']; ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex-grow: 1;">
|
||||
<input type="text" id="destinatario_manual" name="destinatario_manual" class="form-control" placeholder="O pega IDs manualmente aquí, separados por comas">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-help">Selecciona uno o varios destinatarios guardados, o ingresa IDs manualmente separados por comas.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="plantilla_id">Cargar Plantilla (Opcional)</label>
|
||||
<select id="plantilla_id" class="form-control" onchange="loadTemplate(this.value)">
|
||||
<option value="">-- Seleccionar Plantilla --</option>
|
||||
<?php foreach ($plantillas as $plantilla): ?>
|
||||
<option value="<?php echo htmlspecialchars($plantilla['id']); ?>">
|
||||
<?php echo htmlspecialchars($plantilla['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contenido">Contenido del Mensaje *</label>
|
||||
<div id="translation-buttons" class="translation-buttons" style="margin-bottom: 10px; display: flex; gap: 5px;">
|
||||
<!-- Los botones se cargarán dinámicamente con JavaScript -->
|
||||
</div>
|
||||
<button type="button" onclick="openGallery()" class="btn btn-success" style="margin-bottom: 10px; padding: 5px 10px; font-size: 12px;">
|
||||
<i class="fas fa-images"></i> Insertar Imagen
|
||||
</button>
|
||||
<textarea id="contenido" name="contenido" class="form-control" rows="10" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Tipo de Envío *</label>
|
||||
<div>
|
||||
<input type="radio" id="send_immediate" name="send_type" value="inmediato" checked>
|
||||
<label for="send_immediate">Inmediato</label>
|
||||
|
||||
<input type="radio" id="send_scheduled" name="send_type" value="programado" class="ml-3">
|
||||
<label for="send_scheduled">Programado</label>
|
||||
|
||||
<input type="radio" id="send_recurring" name="send_type" value="recurrente" class="ml-3">
|
||||
<label for="send_recurring">Recurrente</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="scheduled_options" class="form-group" style="display: none;">
|
||||
<label for="schedule_datetime">Fecha y Hora de Envío *</label>
|
||||
<input type="datetime-local" id="schedule_datetime" name="schedule_datetime" class="form-control">
|
||||
</div>
|
||||
|
||||
<div id="recurring_options" class="form-group" style="display: none;">
|
||||
<label for="recurrence_frequency">Frecuencia de Recurrencia *</label>
|
||||
<select id="recurrence_frequency" name="recurrence_frequency" class="form-control">
|
||||
<option value="diario">Diario</option>
|
||||
<option value="semanal">Semanal</option>
|
||||
<option value="mensual">Mensual</option>
|
||||
</select>
|
||||
|
||||
<div id="recurring_details" style="margin-top: 15px;">
|
||||
<!-- Detalles específicos de recurrencia (día de la semana, día del mes) -->
|
||||
<div id="weekly_options" style="display: none;">
|
||||
<label for="recurrence_day_of_week">Día de la Semana</label>
|
||||
<select id="recurrence_day_of_week" name="recurrence_day_of_week" class="form-control">
|
||||
<option value="1">Lunes</option>
|
||||
<option value="2">Martes</option>
|
||||
<option value="3">Miércoles</option>
|
||||
<option value="4">Jueves</option>
|
||||
<option value="5">Viernes</option>
|
||||
<option value="6">Sábado</option>
|
||||
<option value="7">Domingo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="monthly_options" style="display: none;">
|
||||
<label for="recurrence_day_of_month">Día del Mes</label>
|
||||
<input type="number" id="recurrence_day_of_month" name="recurrence_day_of_month" class="form-control" min="1" max="31">
|
||||
</div>
|
||||
<label for="recurrence_time">Hora de Envío</label>
|
||||
<input type="time" id="recurrence_time" name="recurrence_time" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="btnEnviar">
|
||||
<i class="fas fa-paper-plane"></i> Enviar Ahora
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="previewMessage()">
|
||||
<i class="fas fa-eye"></i> Vista Previa
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Galería -->
|
||||
<div id="galleryModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeGallery()">×</span>
|
||||
<h2>Galería de Imágenes</h2>
|
||||
<div class="gallery-grid" id="galleryGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Templates Data Hidden -->
|
||||
<script>
|
||||
const templates = <?php echo json_encode($plantillas); ?>;
|
||||
</script>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Función para cargar los idiomas activos
|
||||
function loadActiveLanguages() {
|
||||
return fetch('/shared/languages/get_active.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.languages) {
|
||||
return data.languages;
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error cargando idiomas:', error);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Función para crear botones de traducción
|
||||
function createTranslationButtons(languages) {
|
||||
const container = $('#translation-buttons');
|
||||
container.empty();
|
||||
|
||||
languages.forEach(lang => {
|
||||
if (lang.codigo !== 'es') { // No mostramos el botón para español (idioma original)
|
||||
const button = $(`
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary translate-btn"
|
||||
data-lang="${lang.codigo}"
|
||||
title="Traducir a ${lang.nombre}">
|
||||
<span class="flag-icon flag-icon-${lang.bandera || 'globe'}"></span>
|
||||
</button>
|
||||
`);
|
||||
container.append(button);
|
||||
}
|
||||
});
|
||||
|
||||
// Manejador de eventos para los botones de traducción
|
||||
$('.translate-btn').on('click', async function() {
|
||||
const targetLang = $(this).data('lang');
|
||||
const content = $('#contenido').val();
|
||||
|
||||
if (!content) {
|
||||
alert('Por favor, ingrese un mensaje para traducir.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/shared/translations/translate.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: content,
|
||||
target: targetLang,
|
||||
source: 'es' // Asumimos que el texto original está en español
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Agregar la traducción al final del contenido
|
||||
const translatedText = `\n\n--- Traducción a ${targetLang.toUpperCase()} ---\n${data.translatedText}`;
|
||||
$('#contenido').val(content + translatedText);
|
||||
|
||||
// Eliminar el botón de traducción
|
||||
$(this).remove();
|
||||
} else {
|
||||
alert('Error al traducir: ' + (data.error || 'Error desconocido'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al traducir:', error);
|
||||
alert('Error al conectar con el servicio de traducción.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar los botones de traducción al iniciar
|
||||
$(document).ready(function() {
|
||||
loadActiveLanguages().then(languages => {
|
||||
createTranslationButtons(languages);
|
||||
});
|
||||
$('#contenido').summernote({
|
||||
height: 300,
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'underline', 'clear']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['insert', ['link']],
|
||||
['view', ['fullscreen', 'codeview', 'help']]
|
||||
]
|
||||
});
|
||||
|
||||
// Inicializar Select2 para selección múltiple
|
||||
$('#destinatario_select').select2({
|
||||
placeholder: "-- Seleccionar Destinatarios Guardados --",
|
||||
allowClear: true // Permite deseleccionar
|
||||
});
|
||||
|
||||
// Sincronizar select con input manual
|
||||
$('#destinatario_select').on('change', function() {
|
||||
const selectedIds = $(this).val(); // Array de IDs del select2
|
||||
const manualIds = $('#destinatario_manual').val().split(',').map(id => id.trim()).filter(id => id !== '');
|
||||
|
||||
// Combinar y eliminar duplicados
|
||||
const combinedIds = [...new Set([...selectedIds, ...manualIds])];
|
||||
|
||||
// Actualizar el campo manual para reflejar todas las selecciones y entradas manuales
|
||||
$('#destinatario_manual').val(combinedIds.join(', '));
|
||||
});
|
||||
|
||||
$('#destinatario_manual').on('input', function() {
|
||||
const manualIds = $(this).val().split(',').map(id => id.trim()).filter(id => id !== '');
|
||||
const selectedIds = $('#destinatario_select').val(); // IDs del select2
|
||||
|
||||
// Asegurarse de que el select2 no se deseleccione si se añade manualmente
|
||||
// Esto es complejo si se quiere mantener el estado exacto en ambos sentidos.
|
||||
// Para simplificar, solo aseguramos que el input manual tenga todos los IDs.
|
||||
const combinedIds = [...new Set([...manualIds, ...selectedIds])];
|
||||
// Intentar seleccionar en select2 lo que está en manual si existe
|
||||
$('#destinatario_select').val(manualIds).trigger('change');
|
||||
});
|
||||
});
|
||||
|
||||
function loadTemplate(id) {
|
||||
if (!id) return;
|
||||
const template = templates.find(t => t.id == id);
|
||||
if (template) {
|
||||
if (confirm('¿Reemplazar el contenido actual con la plantilla?')) {
|
||||
$('#contenido').summernote('code', template.contenido);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openGallery() {
|
||||
$('#galleryModal').show();
|
||||
loadGalleryImages();
|
||||
}
|
||||
|
||||
function closeGallery() {
|
||||
$('#galleryModal').hide();
|
||||
}
|
||||
|
||||
async function loadGalleryImages() {
|
||||
try {
|
||||
const response = await fetch('/gallery/api/list.php');
|
||||
const data = await response.json();
|
||||
|
||||
const grid = document.getElementById('galleryGrid');
|
||||
if (data.success && data.images.length > 0) {
|
||||
grid.innerHTML = data.images.map(img => `
|
||||
<div class="gallery-item" onclick="insertImage('${img.url}')">
|
||||
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
grid.innerHTML = '<p>No hay imágenes.</p>';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function insertImage(url) {
|
||||
const fullUrl = window.location.origin + url;
|
||||
$('#contenido').summernote('insertImage', fullUrl);
|
||||
closeGallery();
|
||||
}
|
||||
|
||||
function previewMessage() {
|
||||
const content = $('#contenido').summernote('code');
|
||||
const win = window.open('', 'Preview', 'width=800,height=600');
|
||||
win.document.write('<div style="padding:20px;font-family:sans-serif;">' + content + '</div>');
|
||||
}
|
||||
|
||||
// Cerrar modal click fuera
|
||||
window.onclick = function(event) {
|
||||
if (event.target == document.getElementById('galleryModal')) {
|
||||
closeGallery();
|
||||
}
|
||||
}
|
||||
|
||||
// Lógica para mostrar/ocultar campos de programación/recurrencia
|
||||
const sendTypeRadios = document.querySelectorAll('input[name="send_type"]');
|
||||
const scheduledOptions = document.getElementById('scheduled_options');
|
||||
const recurringOptions = document.getElementById('recurring_options');
|
||||
const recurrenceFrequency = document.getElementById('recurrence_frequency');
|
||||
const weeklyOptions = document.getElementById('weekly_options');
|
||||
const monthlyOptions = document.getElementById('monthly_options');
|
||||
|
||||
function toggleSendTypeOptions() {
|
||||
const selectedSendType = document.querySelector('input[name="send_type"]:checked').value;
|
||||
scheduledOptions.style.display = 'none';
|
||||
recurringOptions.style.display = 'none';
|
||||
|
||||
if (selectedSendType === 'programado') {
|
||||
scheduledOptions.style.display = 'block';
|
||||
} else if (selectedSendType === 'recurrente') {
|
||||
recurringOptions.style.display = 'block';
|
||||
toggleRecurringDetails(); // Mostrar detalles específicos al cargar
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRecurringDetails() {
|
||||
const selectedFrequency = recurrenceFrequency.value;
|
||||
weeklyOptions.style.display = 'none';
|
||||
monthlyOptions.style.display = 'none';
|
||||
|
||||
if (selectedFrequency === 'semanal') {
|
||||
weeklyOptions.style.display = 'block';
|
||||
} else if (selectedFrequency === 'mensual') {
|
||||
monthlyOptions.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Inicializar al cargar la página
|
||||
toggleSendTypeOptions();
|
||||
|
||||
// Escuchar cambios en los tipos de envío
|
||||
sendTypeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', toggleSendTypeOptions);
|
||||
});
|
||||
|
||||
// Escuchar cambios en la frecuencia de recurrencia
|
||||
recurrenceFrequency.addEventListener('change', toggleRecurringDetails);
|
||||
|
||||
$('#messageForm').on('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const selectedDestinatarios = $('#destinatario_select').val() || []; // Array de IDs del select2
|
||||
const manualDestinatarios = $('#destinatario_manual').val()
|
||||
.split(',')
|
||||
.map(id => id.trim())
|
||||
.filter(id => id !== '');
|
||||
|
||||
// Combinar y eliminar duplicados de ambos orígenes
|
||||
const destinatarios = [...new Set([...selectedDestinatarios, ...manualDestinatarios])];
|
||||
|
||||
const contenido = $('#contenido').summernote('code');
|
||||
const sendType = document.querySelector('input[name="send_type"]:checked').value;
|
||||
let scheduleData = {};
|
||||
|
||||
if (destinatarios.length === 0 || !contenido) {
|
||||
alert('Por favor selecciona al menos un destinatario y escribe un mensaje.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendType === 'programado') {
|
||||
const scheduleDatetime = $('#schedule_datetime').val();
|
||||
if (!scheduleDatetime) {
|
||||
alert('Por favor, selecciona la fecha y hora de envío programado.');
|
||||
return;
|
||||
}
|
||||
scheduleData = {
|
||||
fecha_envio: scheduleDatetime
|
||||
};
|
||||
} else if (sendType === 'recurrente') {
|
||||
const recurrenceFrequencyVal = $('#recurrence_frequency').val();
|
||||
const recurrenceTime = $('#recurrence_time').val();
|
||||
|
||||
if (!recurrenceFrequencyVal || !recurrenceTime) {
|
||||
alert('Por favor, completa la frecuencia y hora de envío recurrente.');
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleData = {
|
||||
frecuencia: recurrenceFrequencyVal,
|
||||
hora_envio: recurrenceTime
|
||||
};
|
||||
|
||||
if (recurrenceFrequencyVal === 'semanal') {
|
||||
const dayOfWeek = $('#recurrence_day_of_week').val();
|
||||
if (!dayOfWeek) {
|
||||
alert('Por favor, selecciona el día de la semana para el envío recurrente.');
|
||||
return;
|
||||
}
|
||||
scheduleData.dia_semana = dayOfWeek;
|
||||
} else if (recurrenceFrequencyVal === 'mensual') {
|
||||
const dayOfMonth = $('#recurrence_day_of_month').val();
|
||||
if (!dayOfMonth || dayOfMonth < 1 || dayOfMonth > 31) {
|
||||
alert('Por favor, ingresa un día válido del mes (1-31) para el envío recurrente.');
|
||||
return;
|
||||
}
|
||||
scheduleData.dia_mes = dayOfMonth;
|
||||
}
|
||||
}
|
||||
|
||||
if (!confirm(`¿Enviar mensaje (${sendType}) a ${destinatarios.length} destinatario(s)?`)) return;
|
||||
|
||||
$('#btnEnviar').prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Enviando...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/messages/send.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
destinatario_id: destinatarios, // Ahora puede ser un array
|
||||
contenido: contenido,
|
||||
tipo_envio: sendType,
|
||||
...scheduleData // Añadir datos de programación/recurrencia
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('¡Mensaje enviado correctamente!');
|
||||
window.location.href = '/discord/views/messages/sent.php';
|
||||
} else {
|
||||
alert('Error: ' + (result.error || 'Error desconocido'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Error de conexión');
|
||||
} finally {
|
||||
$('#btnEnviar').prop('disabled', false).html('<i class="fas fa-paper-plane"></i> Enviar Ahora');
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
39
discord/views/messages/php_errors.log
Executable file
39
discord/views/messages/php_errors.log
Executable file
@@ -0,0 +1,39 @@
|
||||
[29-Nov-2025 04:52:45 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
|
||||
#1 {main}
|
||||
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
|
||||
[29-Nov-2025 04:53:24 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
|
||||
#1 {main}
|
||||
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
|
||||
[29-Nov-2025 04:53:28 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
|
||||
#1 {main}
|
||||
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
|
||||
[29-Nov-2025 04:54:04 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
|
||||
#1 {main}
|
||||
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
|
||||
[30-Nov-2025 15:23:38 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in /var/www/html/bot/discord/views/messages/sent.php:51
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/discord/views/messages/sent.php(51): PDOStatement->execute()
|
||||
#1 {main}
|
||||
thrown in /var/www/html/bot/discord/views/messages/sent.php on line 51
|
||||
[30-Nov-2025 15:24:23 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in /var/www/html/bot/discord/views/messages/sent.php:51
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/discord/views/messages/sent.php(51): PDOStatement->execute()
|
||||
#1 {main}
|
||||
thrown in /var/www/html/bot/discord/views/messages/sent.php on line 51
|
||||
[30-Nov-2025 15:25:34 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in /var/www/html/bot/discord/views/messages/sent.php:51
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/discord/views/messages/sent.php(51): PDOStatement->execute()
|
||||
#1 {main}
|
||||
thrown in /var/www/html/bot/discord/views/messages/sent.php on line 51
|
||||
[30-Nov-2025 16:17:08 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
|
||||
[30-Nov-2025 16:19:47 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
|
||||
[30-Nov-2025 16:20:43 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
|
||||
[30-Nov-2025 16:22:34 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
|
||||
377
discord/views/messages/sent.php
Executable file
377
discord/views/messages/sent.php
Executable file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Habilitar logging para depuración
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
$userData = JWTAuth::requireAuth();
|
||||
$db = getDB();
|
||||
|
||||
// Paginación
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// Obtener estado de filtro si existe
|
||||
$filterStatus = $_GET['status'] ?? 'todos'; // Default a 'todos'
|
||||
|
||||
// Construir la cláusula WHERE para filtrar por estado
|
||||
$whereClause = "WHERE m.estado != 'deshabilitado'"; // No mostrar deshabilitados por defecto en la lista principal
|
||||
$params = [];
|
||||
|
||||
if ($filterStatus !== 'todos') {
|
||||
$whereClause = "WHERE m.estado = ?";
|
||||
$params[] = $filterStatus;
|
||||
}
|
||||
|
||||
|
||||
// Obtener total de mensajes (según filtro)
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM mensajes_discord m {$whereClause}");
|
||||
$stmt->execute($params);
|
||||
$totalMessages = $stmt->fetchColumn();
|
||||
$totalPages = ceil($totalMessages / $limit);
|
||||
|
||||
// Obtener mensajes (según filtro)
|
||||
$stmt = $db->prepare("
|
||||
SELECT m.*, u.username
|
||||
FROM mensajes_discord m
|
||||
LEFT JOIN usuarios u ON m.usuario_id = u.id
|
||||
{$whereClause}
|
||||
ORDER BY m.fecha_envio DESC
|
||||
LIMIT ? OFFSET ?
|
||||
");
|
||||
$stmt->bindValue(count($params) + 1, $limit, PDO::PARAM_INT);
|
||||
$stmt->bindValue(count($params) + 2, $offset, PDO::PARAM_INT);
|
||||
|
||||
// Bind WHERE parameters manually
|
||||
foreach ($params as $k => $v) {
|
||||
$stmt->bindValue($k + 1, $v);
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
$mensajes = $stmt->fetchAll();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mensajes - Discord</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--discord-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.message-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.message-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.message-dest {
|
||||
font-weight: 600;
|
||||
color: var(--discord-color);
|
||||
background: #eef0ff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
text-transform: capitalize;
|
||||
color: white;
|
||||
}
|
||||
.status-enviado { background-color: #28a745; }
|
||||
.status-pendiente { background-color: #ffc107; }
|
||||
.status-fallido { background-color: #dc3545; }
|
||||
.status-deshabilitado { background-color: #6c757d; }
|
||||
|
||||
.message-content {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--discord-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
background: white;
|
||||
color: var(--discord-color);
|
||||
padding: 8px 12px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-link.active {
|
||||
background: var(--discord-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
.filter-buttons .btn {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.filter-buttons .btn.active {
|
||||
background-color: var(--discord-color);
|
||||
color: white;
|
||||
border-color: var(--discord-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-history"></i> Historial de Mensajes Discord</h1>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
<a href="create.php" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Nuevo Mensaje
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="filter-buttons">
|
||||
<a href="?status=todos" class="btn <?php echo $filterStatus === 'todos' ? 'active' : ''; ?>">Todos</a>
|
||||
<a href="?status=enviado" class="btn <?php echo $filterStatus === 'enviado' ? 'active' : ''; ?>">Enviados</a>
|
||||
<a href="?status=pendiente" class="btn <?php echo $filterStatus === 'pendiente' ? 'active' : ''; ?>">Pendientes</a>
|
||||
<a href="?status=fallido" class="btn <?php echo $filterStatus === 'fallido' ? 'active' : ''; ?>">Fallidos</a>
|
||||
<a href="?status=deshabilitado" class="btn <?php echo $filterStatus === 'deshabilitado' ? 'active' : ''; ?>">Deshabilitados</a>
|
||||
</div>
|
||||
|
||||
<?php if (empty($mensajes)): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox" style="font-size: 48px; margin-bottom: 20px; color: #ddd;"></i>
|
||||
<h2>No hay mensajes en este estado</h2>
|
||||
<p>Los mensajes que cumplan este criterio aparecerán aquí.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="messages-list">
|
||||
<?php foreach ($mensajes as $msg): ?>
|
||||
<div class="message-card">
|
||||
<div class="message-header">
|
||||
<div>
|
||||
Enviado a: <span class="message-dest"><?php echo htmlspecialchars($msg['canal_id']); ?></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="message-status status-<?php echo htmlspecialchars($msg['estado']); ?>">
|
||||
<?php echo htmlspecialchars($msg['estado']); ?>
|
||||
</span>
|
||||
<i class="fas fa-clock" style="margin-left: 10px;"></i> <?php echo date('d/m/Y H:i', strtotime($msg['fecha_envio'] ?? 'now')); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-content"><?php echo strip_tags($msg['contenido']); ?></div>
|
||||
|
||||
<div class="message-footer">
|
||||
<div>
|
||||
<i class="fas fa-user"></i> Por: <?php echo htmlspecialchars($msg['username'] ?? 'Sistema'); ?>
|
||||
</div>
|
||||
<div>
|
||||
ID Discord: <?php echo htmlspecialchars($msg['mensaje_discord_id'] ?? 'N/A'); ?>
|
||||
<button class="btn btn-warning btn-sm" onclick="retryMessage(<?php echo $msg['id']; ?>)" style="margin-left: 15px;">
|
||||
<i class="fas fa-sync-alt"></i> Reintentar
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteMessage(<?php echo $msg['id']; ?>)" style="margin-left: 5px;">
|
||||
<i class="fas fa-trash"></i> Eliminar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||
<a href="?page=<?php echo $i; ?>&status=<?php echo htmlspecialchars($filterStatus); ?>" class="page-link <?php echo $i === $page ? 'active' : ''; ?>">
|
||||
<?php echo $i; ?>
|
||||
</a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function retryMessage(messageId) {
|
||||
if (!confirm('¿Estás seguro de que quieres volver a poner este mensaje en la cola como "pendiente"?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/messages/retry.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: messageId })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Mensaje marcado como pendiente.');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error al reintentar el mensaje: ' + (result.error || 'Error desconocido.'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al enviar la solicitud de reintento:', error);
|
||||
alert('Error de conexión al intentar reintentar el mensaje.');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMessage(messageId) {
|
||||
if (!confirm('¿Estás seguro de que quieres eliminar/deshabilitar este mensaje?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/messages/delete.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: messageId })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Mensaje deshabilitado correctamente.');
|
||||
location.reload(); // Recargar la página para actualizar la lista
|
||||
} else {
|
||||
alert('Error al deshabilitar el mensaje: ' + (result.error || 'Error desconocido.'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al enviar la solicitud de eliminación:', error);
|
||||
alert('Error de conexión al intentar deshabilitar el mensaje.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
461
discord/views/recipients/list.php
Executable file
461
discord/views/recipients/list.php
Executable file
@@ -0,0 +1,461 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Habilitar logging para depuración
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
$userData = JWTAuth::requireAuth();
|
||||
|
||||
// Verificar permiso para ver la página de destinatarios
|
||||
if (!hasPermission('view_recipients', 'discord')) {
|
||||
die('No tienes permiso para ver los destinatarios de Discord.');
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// Obtener destinatarios
|
||||
$stmt = $db->query("SELECT * FROM destinatarios_discord ORDER BY nombre ASC");
|
||||
$destinatarios = $stmt->fetchAll();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Destinatarios Discord - Sistema de Bots</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--discord-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-channel {
|
||||
background: #e3e7ff;
|
||||
color: var(--discord-color);
|
||||
}
|
||||
|
||||
.badge-user {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.badge-grupo {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--discord-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--discord-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
margin: 10% auto;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #eee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-users"></i> Destinatarios Discord</h1>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
<?php if (hasPermission('manage_recipients', 'discord')): ?>
|
||||
<button onclick="openCreateModal()" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Nuevo Destinatario
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<?php if (empty($destinatarios)): ?>
|
||||
<div style="text-align: center; padding: 40px; color: #666;">
|
||||
<i class="fas fa-users-slash" style="font-size: 48px; margin-bottom: 20px; color: #ddd;"></i>
|
||||
<h3>No hay destinatarios guardados</h3>
|
||||
<p>Agrega canales o usuarios frecuentes para enviar mensajes más rápido.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Tipo</th>
|
||||
<th>ID Discord</th>
|
||||
<th>Fecha Registro</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($destinatarios as $dest): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($dest['nombre']); ?></strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?php
|
||||
if ($dest['tipo'] == 'canal') echo 'badge-channel';
|
||||
else if ($dest['tipo'] == 'usuario') echo 'badge-user';
|
||||
else if ($dest['tipo'] == 'grupo') echo 'badge-grupo';
|
||||
?>">
|
||||
<?php echo ucfirst($dest['tipo']); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><code><?php echo htmlspecialchars($dest['discord_id']); ?></code></td>
|
||||
<td><?php echo date('d/m/Y', strtotime($dest['fecha_registro'])); ?></td>
|
||||
<td>
|
||||
<?php if (hasPermission('manage_recipients', 'discord')): ?>
|
||||
<button onclick="editRecipient(<?php echo $dest['id']; ?>, '<?php echo htmlspecialchars($dest['nombre'], ENT_QUOTES); ?>', '<?php echo htmlspecialchars($dest['discord_id'], ENT_QUOTES); ?>', '<?php echo htmlspecialchars($dest['tipo'], ENT_QUOTES); ?>')" class="btn btn-primary" style="padding: 5px 10px; font-size: 12px;">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php if ($dest['tipo'] === 'usuario'): ?>
|
||||
<button onclick="kickRecipient(<?php echo $dest['id']; ?>)" class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;">
|
||||
<i class="fas fa-user-slash"></i> Expulsar
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button onclick="deleteRecipient(<?php echo $dest['id']; ?>)" class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Crear -->
|
||||
<div id="createModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeCreateModal()">×</span>
|
||||
<h2 style="margin-bottom: 20px;">Nuevo Destinatario</h2>
|
||||
<form id="createForm">
|
||||
<div class="form-group">
|
||||
<label>Nombre (Alias)</label>
|
||||
<input type="text" name="nombre" class="form-control" required placeholder="Ej: Canal General">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>ID de Discord</label>
|
||||
<input type="text" name="discord_id" class="form-control" required placeholder="Ej: 123456789012345678">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tipo</label>
|
||||
<select name="tipo" class="form-control">
|
||||
<option value="canal">Canal</option>
|
||||
<option value="usuario">Usuario (DM)</option>
|
||||
<option value="grupo">Grupo</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Guardar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Editar -->
|
||||
<div id="editModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditModal()">×</span>
|
||||
<h2 style="margin-bottom: 20px;">Editar Destinatario</h2>
|
||||
<form id="editForm">
|
||||
<input type="hidden" id="edit_id" name="id">
|
||||
<div class="form-group">
|
||||
<label>Nombre (Alias)</label>
|
||||
<input type="text" id="edit_nombre" name="nombre" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>ID de Discord</label>
|
||||
<input type="text" id="edit_discord_id" name="discord_id" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tipo</label>
|
||||
<select id="edit_tipo" name="tipo" class="form-control">
|
||||
<option value="canal">Canal</option>
|
||||
<option value="usuario">Usuario (DM)</option>
|
||||
<option value="grupo">Grupo</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Guardar Cambios</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openCreateModal() {
|
||||
document.getElementById('createModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeCreateModal() {
|
||||
document.getElementById('createModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function openEditModal() {
|
||||
document.getElementById('editModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
document.getElementById('editModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function editRecipient(id, nombre, discord_id, tipo) {
|
||||
document.getElementById('edit_id').value = id;
|
||||
document.getElementById('edit_nombre').value = nombre;
|
||||
document.getElementById('edit_discord_id').value = discord_id;
|
||||
document.getElementById('edit_tipo').value = tipo;
|
||||
openEditModal();
|
||||
}
|
||||
|
||||
document.getElementById('createForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/recipients/create.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Error de conexión');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('editForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/recipients/edit.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Error de conexión');
|
||||
}
|
||||
};
|
||||
|
||||
async function deleteRecipient(id) {
|
||||
if (!confirm('¿Eliminar este destinatario? Esta acción es solo local.')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/recipients/delete.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({id})
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Error de conexión');
|
||||
}
|
||||
}
|
||||
|
||||
async function kickRecipient(id) {
|
||||
if (!confirm('¿Estás seguro de expulsar este usuario de Discord? Esta acción es irreversible en Discord.')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/recipients/kick.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({id})
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Usuario expulsado de Discord y eliminado de la lista local.');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error al expulsar: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Error de conexión');
|
||||
}
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target == document.getElementById('createModal')) {
|
||||
closeCreateModal();
|
||||
}
|
||||
if (event.target == document.getElementById('editModal')) {
|
||||
closeEditModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
discord/views/recipients/php_errors.log
Executable file
1
discord/views/recipients/php_errors.log
Executable file
@@ -0,0 +1 @@
|
||||
[29-Nov-2025 21:47:58 America/Mexico_City] PHP Fatal error: Cannot redeclare hasPermission() (previously declared in /var/www/html/bot/shared/utils/helpers.php:97) in /var/www/html/bot/shared/auth/jwt.php on line 216
|
||||
529
discord/views/templates/create.php
Executable file
529
discord/views/templates/create.php
Executable file
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Cargar variables de entorno
|
||||
if (file_exists(__DIR__ . '/../../../.env')) {
|
||||
$lines = file(__DIR__ . '/../../../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos(trim($line), '#') === 0) continue;
|
||||
if (strpos($line, '=') === false) continue;
|
||||
list($key, $value) = explode('=', $line, 2);
|
||||
$_ENV[trim($key)] = trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
|
||||
$userData = JWTAuth::requireAuth();
|
||||
|
||||
// Verificar permiso para ver y crear plantillas
|
||||
if (!hasPermission('editar_plantillas')) {
|
||||
die('No tienes permiso para crear plantillas de Discord.');
|
||||
}
|
||||
|
||||
// PHP logic for initial display, not for form processing
|
||||
$error = $_GET['error'] ?? '';
|
||||
$success = $_GET['success'] ?? '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crear Plantilla - Discord</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--discord-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--discord-color);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
border-left: 4px solid #c33;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #efe;
|
||||
color: #3c3;
|
||||
border-left: 4px solid #3c3;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--discord-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--discord-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.note-editor {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.note-editor.note-frame {
|
||||
border-color: var(--discord-color);
|
||||
}
|
||||
|
||||
/* Modal de galería */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
margin: 50px auto;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
color: var(--discord-color);
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
cursor: pointer;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.gallery-item:hover {
|
||||
border-color: var(--discord-color);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 10px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: var(--discord-color);
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.upload-area input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-plus"></i> Crear Plantilla Discord</h1>
|
||||
<a href="list.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="form-container">
|
||||
<div id="alert-messages">
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form id="createTemplateForm">
|
||||
<div class="form-group">
|
||||
<label for="nombre">Nombre de la Plantilla *</label>
|
||||
<input type="text" id="nombre" name="nombre" required value="">
|
||||
<div class="form-help">Nombre descriptivo para identificar la plantilla</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="comando">Comando (opcional)</label>
|
||||
<input type="text" id="comando" name="comando" placeholder="Ej: /comandos, #asedio" value="">
|
||||
<div class="form-help">Comando para invocar esta plantilla en Discord. Debe ser único.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contenido">Contenido *</label>
|
||||
<button type="button" onclick="openGallery()" class="btn btn-success" style="margin-bottom: 10px;">
|
||||
<i class="fas fa-images"></i> Insertar Imagen
|
||||
</button>
|
||||
<textarea id="contenido" name="contenido"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Guardar Plantilla
|
||||
</button>
|
||||
<button type="button" onclick="previewContent()" class="btn btn-secondary">
|
||||
<i class="fas fa-eye"></i> Vista Previa
|
||||
</button>
|
||||
<a href="list.php" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Cancelar
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Galería -->
|
||||
<div id="galleryModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-images"></i> Galería de Imágenes</h2>
|
||||
<span class="close" onclick="closeGallery()">×</span>
|
||||
</div>
|
||||
|
||||
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
|
||||
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: #ddd;"></i>
|
||||
<p style="margin-top: 10px; color: #666;">Haz clic para subir una imagen o arrastra aquí</p>
|
||||
<input type="file" id="fileInput" accept="image/*" onchange="uploadImage(this)">
|
||||
</div>
|
||||
|
||||
<div class="gallery-grid" id="galleryGrid">
|
||||
<p style="text-align: center; color: #999;">Cargando imágenes...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||
<script>
|
||||
console.log('Scripts loaded');
|
||||
console.log('jQuery:', typeof $);
|
||||
console.log('Summernote:', typeof $.fn.summernote);
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log('Document ready');
|
||||
console.log('Textarea exists:', $('#contenido').length);
|
||||
|
||||
try {
|
||||
$('#contenido').summernote({
|
||||
height: 300,
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'underline', 'clear']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', ['link']],
|
||||
['view', ['fullscreen', 'codeview', 'help']]
|
||||
],
|
||||
placeholder: 'Escribe el contenido de tu plantilla aquí...'
|
||||
});
|
||||
console.log('Summernote initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Error initializing Summernote:', error);
|
||||
}
|
||||
|
||||
// Handle form submission via Fetch API
|
||||
$('#createTemplateForm').on('submit', async function(e) {
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
|
||||
const nombre = $('#nombre').val();
|
||||
const comando = $('#comando').val();
|
||||
const contenido = $('#contenido').summernote('code'); // Get content from Summernote
|
||||
|
||||
// Clear previous alerts
|
||||
$('#alert-messages').empty();
|
||||
|
||||
if (!nombre || !contenido) {
|
||||
showAlert('El nombre y el contenido son obligatorios.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/templates/create.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ nombre, comando, contenido })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showAlert('Plantilla creada correctamente.', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'list.php';
|
||||
}, 1500); // Redirect after a short delay
|
||||
} else {
|
||||
showAlert(result.error || 'Error desconocido al crear la plantilla.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al enviar la solicitud:', error);
|
||||
showAlert('Error de conexión al guardar la plantilla.', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function openGallery() {
|
||||
document.getElementById('galleryModal').style.display = 'block';
|
||||
loadGallery();
|
||||
}
|
||||
|
||||
function closeGallery() {
|
||||
document.getElementById('galleryModal').style.display = 'none';
|
||||
}
|
||||
|
||||
async function loadGallery() {
|
||||
try {
|
||||
const response = await fetch('/gallery/api/list.php');
|
||||
const data = await response.json();
|
||||
|
||||
const grid = document.getElementById('galleryGrid');
|
||||
|
||||
if (data.success && data.images.length > 0) {
|
||||
grid.innerHTML = data.images.map(img => `
|
||||
<div class="gallery-item" onclick="insertImage('${img.url}')">
|
||||
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
grid.innerHTML = '<p style="text-align: center; color: #999;">No hay imágenes disponibles</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cargando galería:', error);
|
||||
document.getElementById('galleryGrid').innerHTML = '<p style="text-align: center; color: #c33;">Error cargando imágenes</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function insertImage(url) {
|
||||
const fullUrl = window.location.origin + url;
|
||||
$('#contenido').summernote('insertImage', fullUrl);
|
||||
closeGallery();
|
||||
}
|
||||
|
||||
async function uploadImage(input) {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', input.files[0]);
|
||||
|
||||
try {
|
||||
const response = await fetch('/gallery/api/upload.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('Imagen subida correctamente');
|
||||
loadGallery();
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error al subir la imagen');
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function previewContent() {
|
||||
const content = $('#contenido').summernote('code');
|
||||
const win = window.open('', 'Preview', 'width=800,height=600');
|
||||
const html = '<!DOCTYPE html>' +
|
||||
'<html>' +
|
||||
'<head>' +
|
||||
'<title>Vista Previa</title>' +
|
||||
'<style>' +
|
||||
'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 20px; background: #f8f9fa; }' +
|
||||
'.preview-container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }' +
|
||||
'</style>' +
|
||||
'</head>' +
|
||||
'<body>' +
|
||||
'<div class="preview-container">' +
|
||||
content +
|
||||
'</div>' +
|
||||
'</body>' +
|
||||
'</html>';
|
||||
win.document.write(html);
|
||||
}
|
||||
|
||||
// Cerrar modal al hacer clic fuera
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('galleryModal');
|
||||
if (event.target == modal) {
|
||||
closeGallery();
|
||||
}
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alertDiv = `<div class="alert alert-${type}">${message}</div>`;
|
||||
$('#alert-messages').html(alertDiv);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
544
discord/views/templates/edit.php
Executable file
544
discord/views/templates/edit.php
Executable file
@@ -0,0 +1,544 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
$userData = JWTAuth::requireAuth();
|
||||
|
||||
// Verificar permiso para ver y editar plantillas
|
||||
if (!hasPermission('manage_templates', 'discord')) {
|
||||
die('No tienes permiso para editar plantillas de Discord.'); // Mensaje de error más general para evitar leaks
|
||||
}
|
||||
|
||||
// Obtener ID de la plantilla
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
header('Location: list.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("
|
||||
SELECT p.*, c.comando
|
||||
FROM plantillas_discord p
|
||||
LEFT JOIN comandos_discord c ON p.id = c.plantilla_id
|
||||
WHERE p.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$plantilla = $stmt->fetch();
|
||||
|
||||
if (!$plantilla) {
|
||||
header('Location: list.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verificar propiedad de la plantilla si no es Admin
|
||||
if ($userData->rol !== 'Admin' && $plantilla['usuario_id'] != $userData->userId) {
|
||||
die('No tienes permiso para editar esta plantilla, ya que no te pertenece.');
|
||||
}
|
||||
|
||||
$error = $_GET['error'] ?? '';
|
||||
$success = $_GET['success'] ?? '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Editar Plantilla - Discord</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--discord-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--discord-color);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
border-left: 4px solid #c33;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #efe;
|
||||
color: #3c3;
|
||||
border-left: 4px solid #3c3;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--discord-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--discord-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.note-editor {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.note-editor.note-frame {
|
||||
border-color: var(--discord-color);
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
margin: 50px auto;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
color: var(--discord-color);
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
cursor: pointer;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.gallery-item:hover {
|
||||
border-color: var(--discord-color);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 10px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: var(--discord-color);
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.upload-area input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-edit"></i> Editar Plantilla</h1>
|
||||
<a href="list.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="form-container">
|
||||
<div id="alert-messages">
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form id="editTemplateForm">
|
||||
<input type="hidden" id="templateId" value="<?php echo htmlspecialchars($plantilla['id']); ?>">
|
||||
<div class="form-group">
|
||||
<label for="nombre">Nombre de la Plantilla *</label>
|
||||
<input type="text" id="nombre" name="nombre" required value="<?php echo htmlspecialchars($plantilla['nombre']); ?>">
|
||||
<div class="form-help">Nombre descriptivo para identificar la plantilla</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="comando">Comando (opcional)</label>
|
||||
<input type="text" id="comando" name="comando" placeholder="Ej: /comandos, #asedio" value="<?php echo htmlspecialchars($plantilla['comando'] ?? ''); ?>">
|
||||
<div class="form-help">Comando para invocar esta plantilla en Discord. Debe ser único.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contenido">Contenido *</label>
|
||||
<button type="button" onclick="openGallery()" class="btn btn-success" style="margin-bottom: 10px;">
|
||||
<i class="fas fa-images"></i> Insertar Imagen
|
||||
</button>
|
||||
<textarea id="contenido" name="contenido"><?php echo htmlspecialchars($plantilla['contenido']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Guardar Cambios
|
||||
</button>
|
||||
<button type="button" onclick="previewContent()" class="btn btn-secondary">
|
||||
<i class="fas fa-eye"></i> Vista Previa
|
||||
</button>
|
||||
<a href="list.php" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Cancelar
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Galería -->
|
||||
<div id="galleryModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-images"></i> Galería de Imágenes</h2>
|
||||
<span class="close" onclick="closeGallery()">×</span>
|
||||
</div>
|
||||
|
||||
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
|
||||
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: #ddd;"></i>
|
||||
<p style="margin-top: 10px; color: #666;">Haz clic para subir una imagen o arrastra aquí</p>
|
||||
<input type="file" id="fileInput" accept="image/*" onchange="uploadImage(this)">
|
||||
</div>
|
||||
|
||||
<div class="gallery-grid" id="galleryGrid">
|
||||
<p style="text-align: center; color: #999;">Cargando imágenes...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
console.log('edit.php script loaded and ready');
|
||||
$('#contenido').summernote({
|
||||
height: 300,
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'underline', 'clear']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', ['link']],
|
||||
['view', ['fullscreen', 'codeview', 'help']]
|
||||
]
|
||||
});
|
||||
|
||||
// Handle form submission via Fetch API
|
||||
$('#editTemplateForm').on('submit', async function(e) {
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
|
||||
const id = $('#templateId').val();
|
||||
const nombre = $('#nombre').val();
|
||||
const comando = $('#comando').val();
|
||||
const contenido = $('#contenido').summernote('code'); // Get content from Summernote
|
||||
|
||||
// Clear previous alerts
|
||||
$('#alert-messages').empty();
|
||||
|
||||
if (!nombre || !contenido) {
|
||||
showAlert('El nombre y el contenido son obligatorios.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/templates/edit.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id, nombre, comando, contenido })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showAlert('Plantilla actualizada correctamente.', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'list.php';
|
||||
}, 1500); // Redirect after a short delay
|
||||
} else {
|
||||
showAlert(result.error || 'Error desconocido al actualizar la plantilla.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al enviar la solicitud:', error);
|
||||
showAlert('Error de conexión al guardar la plantilla.', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function openGallery() {
|
||||
document.getElementById('galleryModal').style.display = 'block';
|
||||
loadGallery();
|
||||
}
|
||||
|
||||
function closeGallery() {
|
||||
document.getElementById('galleryModal').style.display = 'none';
|
||||
}
|
||||
|
||||
async function loadGallery() {
|
||||
try {
|
||||
const response = await fetch('/gallery/api/list.php');
|
||||
const data = await response.json();
|
||||
|
||||
const grid = document.getElementById('galleryGrid');
|
||||
|
||||
if (data.success && data.images.length > 0) {
|
||||
grid.innerHTML = data.images.map(img => `
|
||||
<div class="gallery-item" onclick="insertImage('${img.url}')">
|
||||
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
grid.innerHTML = '<p style="text-align: center; color: #999;">No hay imágenes disponibles</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cargando galería:', error);
|
||||
document.getElementById('galleryGrid').innerHTML = '<p style="text-align: center; color: #c33;">Error cargando imágenes</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function insertImage(url) {
|
||||
const fullUrl = window.location.origin + url;
|
||||
$('#contenido').summernote('insertImage', fullUrl);
|
||||
closeGallery();
|
||||
}
|
||||
|
||||
async function uploadImage(input) {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', input.files[0]);
|
||||
|
||||
try {
|
||||
const response = await fetch('/gallery/api/upload.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('Imagen subida correctamente');
|
||||
loadGallery();
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error al subir la imagen');
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function previewContent() {
|
||||
const content = $('#contenido').summernote('code');
|
||||
const win = window.open('', 'Preview', 'width=800,height=600');
|
||||
win.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Vista Previa</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.preview-container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="preview-container">
|
||||
${content}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('galleryModal');
|
||||
if (event.target == modal) {
|
||||
closeGallery();
|
||||
}
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alertDiv = `<div class="alert alert-${type}">${message}</div>`;
|
||||
$('#alert-messages').html(alertDiv);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
336
discord/views/templates/list.php
Executable file
336
discord/views/templates/list.php
Executable file
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Habilitar logging para depuración
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// Cargar configuración
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::requireAuth();
|
||||
|
||||
// Verificar permiso para ver la página
|
||||
if (!hasPermission('view_templates', 'discord')) {
|
||||
die('No tienes permiso para ver las plantillas de Discord.');
|
||||
}
|
||||
|
||||
// Obtener plantillas
|
||||
$db = getDB();
|
||||
$stmt = $db->query("
|
||||
SELECT p.*, c.comando, u.username
|
||||
FROM plantillas_discord p
|
||||
LEFT JOIN comandos_discord c ON p.id = c.plantilla_id
|
||||
LEFT JOIN usuarios u ON p.usuario_id = u.id
|
||||
ORDER BY p.fecha_creacion DESC
|
||||
");
|
||||
$plantillas = $stmt->fetchAll();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Plantillas Discord - Sistema de Bots</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--discord-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--discord-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--discord-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.templates-grid {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.template-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.template-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.template-command {
|
||||
display: inline-block;
|
||||
background: #e3e7ff;
|
||||
color: var(--discord-color);
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.template-meta {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.template-content {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 15px;
|
||||
max-height: 150px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.template-content::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background: linear-gradient(transparent, #f8f9fa);
|
||||
}
|
||||
|
||||
.template-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 64px;
|
||||
color: #ddd;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: #999;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-file-alt"></i> Plantillas Discord</h1>
|
||||
<div class="header-actions">
|
||||
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||
<a href="create.php" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Nueva Plantilla
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<?php if (empty($plantillas)): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<h2>No hay plantillas creadas</h2>
|
||||
<p>Crea tu primera plantilla para empezar a enviar mensajes en Discord</p>
|
||||
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||
<a href="create.php" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Crear Primera Plantilla
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="templates-grid">
|
||||
<?php foreach ($plantillas as $plantilla): ?>
|
||||
<div class="template-card">
|
||||
<div class="template-header">
|
||||
<div>
|
||||
<div class="template-title"><?php echo htmlspecialchars($plantilla['nombre']); ?></div>
|
||||
<?php if ($plantilla['comando']): ?>
|
||||
<span class="template-command"><?php echo htmlspecialchars($plantilla['comando']); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="template-meta">
|
||||
<i class="fas fa-user"></i> <?php echo htmlspecialchars($plantilla['username'] ?? 'Desconocido'); ?>
|
||||
|
|
||||
<i class="fas fa-clock"></i> <?php echo date('d/m/Y H:i', strtotime($plantilla['fecha_creacion'])); ?>
|
||||
</div>
|
||||
|
||||
<div class="template-content">
|
||||
<?php echo $plantilla['contenido']; ?>
|
||||
</div>
|
||||
|
||||
<div class="template-actions">
|
||||
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||
<a href="edit.php?id=<?php echo $plantilla['id']; ?>" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-edit"></i> Editar
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (hasPermission('view_templates', 'discord')): ?>
|
||||
<button onclick="previewTemplate(<?php echo $plantilla['id']; ?>)" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-eye"></i> Vista Previa
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (hasPermission('manage_templates', 'discord')): ?>
|
||||
<button onclick="deleteTemplate(<?php echo $plantilla['id']; ?>, '<?php echo htmlspecialchars($plantilla['nombre'], ENT_QUOTES); ?>')" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-trash"></i> Eliminar
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function previewTemplate(id) {
|
||||
window.open('preview.php?id=' + id, 'preview', 'width=800,height=600');
|
||||
}
|
||||
|
||||
async function deleteTemplate(id, nombre) {
|
||||
if (!confirm(`¿Estás seguro de eliminar la plantilla "${nombre}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/templates/delete.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('Plantilla eliminada correctamente');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error al eliminar: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error de conexión');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
40
discord/views/templates/php_errors.log
Executable file
40
discord/views/templates/php_errors.log
Executable file
@@ -0,0 +1,40 @@
|
||||
[29-Nov-2025 04:28:49 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
|
||||
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
|
||||
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
|
||||
#3 /var/www/html/bot/discord/views/templates/list.php(10): JWTAuth::requireAuth()
|
||||
#4 {main}
|
||||
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
|
||||
[29-Nov-2025 04:37:46 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
|
||||
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
|
||||
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
|
||||
#3 /var/www/html/bot/discord/views/templates/list.php(15): JWTAuth::requireAuth()
|
||||
#4 {main}
|
||||
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
|
||||
[29-Nov-2025 04:38:17 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
|
||||
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
|
||||
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
|
||||
#3 /var/www/html/bot/discord/views/templates/list.php(15): JWTAuth::requireAuth()
|
||||
#4 {main}
|
||||
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
|
||||
[29-Nov-2025 04:40:46 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
|
||||
Stack trace:
|
||||
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
|
||||
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
|
||||
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
|
||||
#3 /var/www/html/bot/discord/views/templates/list.php(15): JWTAuth::requireAuth()
|
||||
#4 {main}
|
||||
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
|
||||
[29-Nov-2025 04:43:07 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[29-Nov-2025 04:44:44 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[29-Nov-2025 05:03:31 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[29-Nov-2025 16:24:11 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[29-Nov-2025 16:37:32 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
|
||||
[04-Dec-2025 14:44:31 America/Mexico_City] PHP Warning: Undefined array key "comando" in /var/www/html/bot/discord/views/templates/list.php on line 262
|
||||
[04-Dec-2025 14:45:03 America/Mexico_City] PHP Warning: Undefined array key "comando" in /var/www/html/bot/discord/views/templates/list.php on line 262
|
||||
[04-Dec-2025 14:45:33 America/Mexico_City] PHP Warning: Undefined array key "comando" in /var/www/html/bot/discord/views/templates/list.php on line 262
|
||||
130
discord/views/templates/preview.php
Executable file
130
discord/views/templates/preview.php
Executable file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
// Verificar autenticación
|
||||
$userData = JWTAuth::requireAuth();
|
||||
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
die('ID no proporcionado');
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT * FROM plantillas_discord WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$plantilla = $stmt->fetch();
|
||||
|
||||
if (!$plantilla) {
|
||||
die('Plantilla no encontrada');
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vista Previa: <?php echo htmlspecialchars($plantilla['nombre']); ?></title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background-color: #313338;
|
||||
color: #dbdee1;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
line-height: 1.375rem;
|
||||
}
|
||||
|
||||
.discord-message {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: #313338;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 1rem;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Estilos básicos para simular Discord */
|
||||
strong { font-weight: 700; }
|
||||
em { font-style: italic; }
|
||||
u { text-decoration: underline; }
|
||||
|
||||
a {
|
||||
color: #00a8fc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #2b2d31;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
font-family: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #2b2d31;
|
||||
border: 1px solid #1e1f22;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin: 6px 0;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0;
|
||||
padding: 0 0 0 4px;
|
||||
border-left: 4px solid #4e5058;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin: 8px 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h1 { font-size: 1.5rem; }
|
||||
h2 { font-size: 1.25rem; }
|
||||
h3 { font-size: 1rem; }
|
||||
|
||||
ul, ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="discord-message">
|
||||
<div class="message-content">
|
||||
<?php echo $plantilla['contenido']; ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
475
discord/views/welcome/config.php
Executable file
475
discord/views/welcome/config.php
Executable file
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Habilitar logging para depuración
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../../../shared/utils/helpers.php';
|
||||
require_once __DIR__ . '/../../../shared/auth/jwt.php';
|
||||
require_once __DIR__ . '/../../../shared/database/connection.php';
|
||||
|
||||
$userData = JWTAuth::requireAuth();
|
||||
|
||||
// Verificar permiso para gestionar el mensaje de bienvenida
|
||||
if (!hasPermission('manage_welcome', 'discord')) {
|
||||
die('No tienes permiso para gestionar la configuración del mensaje de bienvenida de Discord.');
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// Manejar guardado
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Verificar permiso para la acción de guardar
|
||||
if (!hasPermission('manage_welcome', 'discord')) {
|
||||
jsonResponse(['success' => false, 'error' => 'No tienes permiso para guardar la configuración del mensaje de bienvenida.'], 403);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
try {
|
||||
// Verificar si ya existe configuración (solo debe haber una por ahora, o una por servidor si escalamos)
|
||||
// Por simplicidad asumimos una configuración global única (id=1) o la creamos
|
||||
|
||||
$canal_id = $input['canal_id'] ?? '';
|
||||
$texto = $input['texto'] ?? '';
|
||||
$imagen_id = !empty($input['imagen_id']) ? $input['imagen_id'] : null;
|
||||
$idiomas_habilitados = json_encode($input['idiomas_habilitados'] ?? []);
|
||||
$registrar = isset($input['registrar_usuario']) ? (int)$input['registrar_usuario'] : 1;
|
||||
$activo = isset($input['activo']) ? (int)$input['activo'] : 1;
|
||||
|
||||
// Intentar actualizar primero
|
||||
$stmt = $db->prepare("SELECT id FROM bienvenida_discord LIMIT 1");
|
||||
$stmt->execute();
|
||||
$exists = $stmt->fetchColumn();
|
||||
|
||||
if ($exists) {
|
||||
$stmt = $db->prepare("
|
||||
UPDATE bienvenida_discord
|
||||
SET canal_id = ?, texto = ?, imagen_id = ?, idiomas_habilitados = ?, registrar_usuario = ?, activo = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$canal_id, $texto, $imagen_id, $idiomas_habilitados, $registrar, $activo, $exists]);
|
||||
} else {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO bienvenida_discord (canal_id, texto, imagen_id, idiomas_habilitados, registrar_usuario, activo)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$canal_id, $texto, $imagen_id, $idiomas_habilitados, $registrar, $activo]);
|
||||
}
|
||||
|
||||
jsonResponse(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// Obtener configuración actual
|
||||
$stmt = $db->query("
|
||||
SELECT b.*, g.ruta as imagen_ruta
|
||||
FROM bienvenida_discord b
|
||||
LEFT JOIN gallery g ON b.imagen_id = g.id
|
||||
LIMIT 1
|
||||
");
|
||||
$config = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Obtener canales destinatarios
|
||||
$stmt = $db->query("SELECT discord_id, nombre FROM destinatarios_discord WHERE tipo = 'canal' ORDER BY nombre ASC");
|
||||
$canales = $stmt->fetchAll();
|
||||
|
||||
// Obtener idiomas activos
|
||||
$stmt = $db->query("SELECT id, codigo, nombre FROM idiomas WHERE activo = 1 ORDER BY nombre ASC");
|
||||
$idiomas = $stmt->fetchAll();
|
||||
|
||||
// Decodificar idiomas seleccionados
|
||||
$idiomasSeleccionados = [];
|
||||
if ($config && $config['idiomas_habilitados']) {
|
||||
$idiomasSeleccionados = json_decode($config['idiomas_habilitados'], true) ?? [];
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bienvenida Discord - Sistema de Bots</title>
|
||||
|
||||
<!-- FontAwesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Summernote CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Select2 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--discord-color: #5865F2;
|
||||
--discord-dark: #4752C4;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 { color: var(--discord-color); font-size: 24px; }
|
||||
|
||||
.container { max-width: 1000px; margin: 0 auto; }
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group { margin-bottom: 20px; }
|
||||
.form-group label { display: block; margin-bottom: 8px; font-weight: 600; }
|
||||
.form-control { width: 100%; padding: 10px; border: 2px solid #eee; border-radius: 8px; }
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn-primary { background: var(--discord-color); color: white; }
|
||||
.btn-secondary { background: #6c757d; color: white; }
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
.switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider {
|
||||
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: #ccc; transition: .4s; border-radius: 34px;
|
||||
}
|
||||
.slider:before {
|
||||
position: absolute; content: ""; height: 16px; width: 16px; left: 4px; bottom: 4px;
|
||||
background-color: white; transition: .4s; border-radius: 50%;
|
||||
}
|
||||
input:checked + .slider { background-color: var(--discord-color); }
|
||||
input:checked + .slider:before { transform: translateX(26px); }
|
||||
|
||||
/* Modal Galería */
|
||||
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
|
||||
.modal-content { background: white; margin: 5% auto; padding: 20px; width: 80%; max-width: 900px; border-radius: 15px; max-height: 80vh; overflow-y: auto; }
|
||||
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 20px; }
|
||||
.gallery-item { border: 2px solid #eee; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.2s; position: relative; }
|
||||
.gallery-item:hover { border-color: var(--discord-color); transform: translateY(-2px); }
|
||||
.gallery-item img { width: 100%; height: 120px; object-fit: cover; }
|
||||
.gallery-item.selected { border-color: var(--discord-color); box-shadow: 0 0 0 3px rgba(88, 101, 242, 0.3); }
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.image-preview img { width: 100%; height: 100%; object-fit: contain; }
|
||||
.remove-image {
|
||||
position: absolute; top: 10px; right: 10px;
|
||||
background: rgba(255,0,0,0.8); color: white;
|
||||
border: none; border-radius: 50%; width: 30px; height: 30px;
|
||||
cursor: pointer; display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-handshake"></i> Configuración de Bienvenida</h1>
|
||||
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<form id="welcomeForm">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label>Activar Bienvenida</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="activo" <?php echo ($config['activo'] ?? 1) ? 'checked' : ''; ?>>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label>Registrar Usuario en BD</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="registrar_usuario" <?php echo ($config['registrar_usuario'] ?? 1) ? 'checked' : ''; ?>>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Canal de Bienvenida</label>
|
||||
<select name="canal_id" class="form-control select2" required>
|
||||
<option value="">-- Seleccionar Canal --</option>
|
||||
<?php foreach ($canales as $canal): ?>
|
||||
<option value="<?php echo $canal['discord_id']; ?>"
|
||||
<?php echo ($config['canal_id'] ?? '') == $canal['discord_id'] ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($canal['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small style="color: #666;">Si no aparece, agrégalo en "Destinatarios".</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Idiomas Disponibles (Botones Automáticos)</label>
|
||||
<select name="idiomas_habilitados[]" id="idiomas_habilitados_select" class="form-control" multiple="multiple">
|
||||
<?php foreach ($idiomas as $lang): ?>
|
||||
<option value="<?php echo $lang['codigo']; ?>"
|
||||
<?php echo in_array($lang['codigo'], $idiomasSeleccionados) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($lang['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small style="color: #666;">Selecciona los idiomas que se mostrarán como botones en el mensaje de bienvenida.</small>
|
||||
<div style="margin-top: 10px;">
|
||||
<a href="/shared/languages/manager.php" style="font-size: 12px; color: var(--discord-color); text-decoration: none;">
|
||||
<i class="fas fa-cog"></i> Gestionar idiomas
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Mensaje de Bienvenida</label>
|
||||
<textarea id="summernote" name="texto"><?php echo htmlspecialchars($config['texto'] ?? ''); ?></textarea>
|
||||
<small style="color: #666;">Puedes usar {usuario} para mencionar al nuevo miembro.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Imagen Opcional</label>
|
||||
<input type="hidden" id="imagen_id" name="imagen_id" value="<?php echo htmlspecialchars($config['imagen_id'] ?? ''); ?>">
|
||||
<button type="button" onclick="openGallery()" class="btn btn-secondary" style="margin-bottom: 10px;">
|
||||
<i class="fas fa-image"></i> Seleccionar Imagen
|
||||
</button>
|
||||
<div id="imagePreview" class="image-preview">
|
||||
<?php if (!empty($config['imagen_id']) && !empty($config['imagen_ruta'])): ?>
|
||||
<img src="/gallery/uploads/<?php echo basename($config['imagen_ruta']); ?>" alt="Imagen de bienvenida">
|
||||
<button type="button" class="remove-image" style="display:block" onclick="removeImage()">×</button>
|
||||
<?php else: ?>
|
||||
<span style="color: #ccc;">Sin imagen seleccionada</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<?php if (hasPermission('manage_welcome', 'discord')): ?>
|
||||
<button type="submit" class="btn btn-primary" style="flex: 1; justify-content: center;">
|
||||
<i class="fas fa-save"></i> Guardar Configuración
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (hasPermission('manage_welcome', 'discord')): ?>
|
||||
<button type="button" onclick="sendTestMessage()" class="btn btn-success" style="flex: 1; justify-content: center; background: #28a745;">
|
||||
<i class="fas fa-paper-plane"></i> Probar Mensaje
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Galería -->
|
||||
<div id="galleryModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span style="float:right; cursor:pointer; font-size:24px;" onclick="closeGallery()">×</span>
|
||||
<h2>Seleccionar Imagen</h2>
|
||||
<div id="galleryContainer" class="gallery-grid">
|
||||
<!-- Se carga vía AJAX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Librerías JS -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#summernote').summernote({
|
||||
height: 200,
|
||||
toolbar: [
|
||||
['style', ['bold', 'italic', 'underline', 'clear']],
|
||||
['para', ['ul', 'ol']],
|
||||
['insert', ['link']],
|
||||
['view', ['codeview']]
|
||||
]
|
||||
});
|
||||
|
||||
$('.select2').select2();
|
||||
$('#idiomas_habilitados_select').select2(); // Inicializar el select2 para idiomas
|
||||
|
||||
// Galería
|
||||
function openGallery() {
|
||||
document.getElementById('galleryModal').style.display = 'block';
|
||||
loadGalleryImages();
|
||||
}
|
||||
|
||||
function closeGallery() {
|
||||
document.getElementById('galleryModal').style.display = 'none';
|
||||
}
|
||||
|
||||
async function loadGalleryImages() {
|
||||
const container = document.getElementById('galleryContainer');
|
||||
container.innerHTML = '<p>Cargando...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/gallery/api/list.php');
|
||||
const data = await response.json();
|
||||
|
||||
container.innerHTML = '';
|
||||
if (data.images && data.images.length > 0) {
|
||||
data.images.forEach(img => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'gallery-item';
|
||||
// La API devuelve url_thumbnail (ruta completa) y nombre_original
|
||||
div.innerHTML = `<img src="${img.url_thumbnail}" alt="${img.nombre_original}">`;
|
||||
// Pasamos img.nombre que es el nombre del archivo físico
|
||||
div.onclick = () => selectImage(img.id, img.nombre, img.ruta); // Pasar también la ruta completa para preview
|
||||
container.appendChild(div);
|
||||
});
|
||||
} else {
|
||||
container.innerHTML = '<p>No hay imágenes en la galería.</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
container.innerHTML = '<p>Error cargando imágenes.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function selectImage(id, filename, ruta_completa) {
|
||||
document.getElementById('imagen_id').value = id;
|
||||
const preview = document.getElementById('imagePreview');
|
||||
preview.innerHTML = `
|
||||
<img src="${ruta_completa}" alt="Imagen de bienvenida">
|
||||
<button type="button" class="remove-image" style="display:block" onclick="removeImage()">×</button>
|
||||
`;
|
||||
closeGallery();
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
document.getElementById('imagen_id').value = '';
|
||||
document.getElementById('imagePreview').innerHTML = `
|
||||
<span style="color: #ccc;">Sin imagen seleccionada</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Guardar
|
||||
document.getElementById('welcomeForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
// Recolectar datos del formulario
|
||||
const activo = $('input[name="activo"]').is(':checked') ? 1 : 0;
|
||||
const registrar_usuario = $('input[name="registrar_usuario"]').is(':checked') ? 1 : 0;
|
||||
const canal_id = $('select[name="canal_id"]').val();
|
||||
const texto = $('#summernote').summernote('code');
|
||||
const imagen_id = $('#imagen_id').val();
|
||||
const idiomas_habilitados = $('#idiomas_habilitados_select').val(); // Array de códigos de idioma
|
||||
|
||||
const data = {
|
||||
activo: activo,
|
||||
registrar_usuario: registrar_usuario,
|
||||
canal_id: canal_id,
|
||||
texto: texto,
|
||||
imagen_id: imagen_id === '' ? null : imagen_id,
|
||||
idiomas_habilitados: idiomas_habilitados
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Configuración guardada correctamente');
|
||||
} else {
|
||||
alert('Error: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Error de conexión');
|
||||
}
|
||||
};
|
||||
|
||||
async function sendTestMessage() {
|
||||
if (!confirm('¿Enviar mensaje de prueba al canal configurado?')) return;
|
||||
|
||||
const btn = document.querySelector('.btn-success');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Enviando...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/discord/api/welcome/send_test.php', {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Mensaje de prueba enviado con éxito a Discord!');
|
||||
} else {
|
||||
alert('Error: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Error de conexión');
|
||||
} finally {
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target == document.getElementById('galleryModal')) {
|
||||
closeGallery();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
91
discord/webhook/index.php
Executable file
91
discord/webhook/index.php
Executable file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/**
|
||||
* Webhook Principal de Discord
|
||||
* Punto de entrada para interacciones de Discord que redirige al daemon
|
||||
*/
|
||||
|
||||
// Habilitar logging de errores en archivo, no en salida
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
ini_set('error_log', __DIR__ . '/../../logs/webhook_errors.log');
|
||||
|
||||
require_once __DIR__ . '/../../shared/utils/helpers.php';
|
||||
|
||||
// Cargar variables de entorno
|
||||
if (file_exists(__DIR__ . '/../../.env')) {
|
||||
$lines = file(__DIR__ . '/../../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos(trim($line), '#') === 0) continue;
|
||||
if (strpos($line, '=') === false) continue;
|
||||
list($key, $value) = explode('=', $line, 2);
|
||||
$_ENV[trim($key)] = trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
// Constantes de Discord
|
||||
define('DISCORD_PUBLIC_KEY', $_ENV['DISCORD_PUBLIC_KEY'] ?? getenv('DISCORD_PUBLIC_KEY'));
|
||||
|
||||
// Verificar firma de seguridad (Ed25519)
|
||||
function verifySignature($body, $headers) {
|
||||
$signature = $headers['x-signature-ed25519'] ?? '';
|
||||
$timestamp = $headers['x-signature-timestamp'] ?? '';
|
||||
$publicKey = DISCORD_PUBLIC_KEY;
|
||||
|
||||
if (\!$signature || \!$timestamp || \!$publicKey) {
|
||||
error_log("Faltan parámetros de firma");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (function_exists('sodium_crypto_sign_verify_detached')) {
|
||||
try {
|
||||
$sig = hex2bin($signature);
|
||||
$msg = $timestamp . $body;
|
||||
$key = hex2bin($publicKey);
|
||||
return sodium_crypto_sign_verify_detached($sig, $msg, $key);
|
||||
} catch (Exception $e) {
|
||||
error_log("Error verificando firma (sodium): " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
error_log("ADVERTENCIA: La extensión 'sodium' de PHP no está instalada. La verificación de firma de Discord es INSEGURA.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Obtener headers y body
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$body = file_get_contents('php://input');
|
||||
|
||||
// Registrar la solicitud para depuración
|
||||
error_log("Solicitud recibida: " . json_encode([
|
||||
'headers' => array_intersect_key($headers, ['x-signature-ed25519' => true, 'x-signature-timestamp' => true]),
|
||||
'body' => $body
|
||||
]));
|
||||
|
||||
// Verificar firma
|
||||
if (\!verifySignature($body, $headers)) {
|
||||
http_response_code(401);
|
||||
error_log("Firma no válida");
|
||||
echo 'Invalid Signature';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Decodificar JSON para verificar si es un PING
|
||||
$data = json_decode($body, true);
|
||||
$type = $data['type'] ?? 0;
|
||||
|
||||
// Manejar PING (requerido por Discord para validar la URL del webhook)
|
||||
if ($type === 1) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['type' => 1]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Para otras interacciones, redirigir al daemon
|
||||
$interactionId = $data['id'] ?? 'unknown';
|
||||
error_log("Interacción recibida (ID: $interactionId). El daemon debería manejar esto.");
|
||||
|
||||
// NO responder aquí. El daemon se encargará de la respuesta completa.
|
||||
// Simplemente terminamos el script del webhook con un 200 OK.
|
||||
http_response_code(200);
|
||||
exit;
|
||||
Reference in New Issue
Block a user