Commit inicial con archivos existentes

This commit is contained in:
2026-01-17 16:14:00 -06:00
parent 48671dc88e
commit 4c48c279de
2539 changed files with 2412708 additions and 0 deletions

84
admin/activity.php Executable file
View File

@@ -0,0 +1,84 @@
<?php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../includes/db.php';
// Only admins can access this page
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
header("Location: ../index.php");
exit;
}
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = 25;
$offset = ($page - 1) * $limit;
// Get total number of records
$total_stmt = $pdo->query("SELECT COUNT(*) FROM activity_log");
$total_records = $total_stmt->fetchColumn();
$total_pages = ceil($total_records / $limit);
// Fetch activity logs
$stmt = $pdo->prepare("SELECT * FROM activity_log ORDER BY timestamp DESC LIMIT :limit OFFSET :offset");
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
require_once __DIR__ . '/../templates/header.php';
?>
<div class="container-fluid">
<h1 class="mt-4">Registro de Actividad</h1>
<div class="card mb-4">
<div class="card-body">
<table class="table table-striped table-bordered">
<thead class="thead-dark">
<tr>
<th>Usuario</th>
<th>Acción</th>
<th>Detalles</th>
<th>Fecha y Hora</th>
</tr>
</thead>
<tbody>
<?php if (empty($logs)): ?>
<tr>
<td colspan="4" class="text-center">No hay registros de actividad.</td>
</tr>
<?php else: ?>
<?php foreach ($logs as $log): ?>
<tr>
<td><?php echo htmlspecialchars($log['username'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($log['action']); ?></td>
<td><?php echo htmlspecialchars($log['details'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars(date('d/m/Y H:i:s', strtotime($log['timestamp']))); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<?php if ($page > 1): ?>
<li class="page-item"><a class="page-link" href="?page=<?php echo $page - 1; ?>">Anterior</a></li>
<?php endif; ?>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<li class="page-item <?php echo ($i == $page) ? 'active' : ''; ?>">
<a class="page-link" href="?page=<?php echo $i; ?>"><?php echo $i; ?></a>
</li>
<?php endfor; ?>
<?php if ($page < $total_pages): ?>
<li class="page-item"><a class="page-link" href="?page=<?php echo $page + 1; ?>">Siguiente</a></li>
<?php endif; ?>
</ul>
</nav>
</div>
<?php require_once __DIR__ . '/../templates/footer.php'; ?>

54
admin/comandos.php Executable file
View File

@@ -0,0 +1,54 @@
<?php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../includes/db.php';
// Fetch recurrent messages with commands
try {
$stmt = $pdo->query("SELECT name, telegram_command FROM recurrent_messages WHERE telegram_command IS NOT NULL AND telegram_command != '' ORDER BY name ASC");
$commands = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Error: No se pudieron cargar los comandos.");
}
require_once __DIR__ . '/../templates/header.php';
?>
<div class="container-fluid">
<h1 class="mt-4" data-translate="true">Lista de Comandos de Telegram</h1>
<p class="text-muted" data-translate="true">Esta es una lista de todos los comandos de plantilla que has configurado. Los usuarios pueden usar estos comandos en Telegram para recibir el mensaje correspondiente.</p>
<div class="card card-body p-4">
<?php if (empty($commands)): ?>
<div class="alert alert-info text-center" role="alert">
<i class="bi bi-info-circle-fill me-2"></i>
<span data-translate="true">No has configurado ningún comando de Telegram todavía.</span>
<br>
<span data-translate="true">Puedes asignar comandos a tus plantillas desde la página de</span>
<a href="recurrentes.php" class="alert-link" data-translate="true">Mensajes Recurrentes</a>.
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th scope="col" data-translate="true">Comando de Telegram</th>
<th scope="col" data-translate="true">Nombre de la Plantilla</th>
</tr>
</thead>
<tbody>
<?php foreach ($commands as $command): ?>
<tr>
<td>
<code class="fs-5">#<?php echo htmlspecialchars($command['telegram_command']); ?></code>
</td>
<td><?php echo htmlspecialchars($command['name']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<?php require_once __DIR__ . '/../templates/footer.php'; ?>

45
admin/get_user_groups.php Executable file
View File

@@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../includes/db.php';
header('Content-Type: application/json');
// Admin-only access
if ($_SESSION['role'] !== 'admin') {
echo json_encode(['error' => 'Unauthorized']);
exit();
}
$recipientId = $_GET['recipient_id'] ?? null;
if (!$recipientId) {
echo json_encode(['error' => 'Recipient ID missing']);
exit();
}
try {
// Obtener el nombre del usuario
$stmt = $pdo->prepare("SELECT name FROM recipients WHERE id = ?");
$stmt->execute([$recipientId]);
$userName = $stmt->fetchColumn();
// Obtener los grupos a los que pertenece el usuario
$stmt = $pdo->prepare("
SELECT tgm.chat_id, r.name as group_name
FROM telegram_group_members tgm
JOIN recipients r ON tgm.chat_id = r.platform_id AND r.platform = 'telegram' AND r.type = 'channel'
WHERE tgm.recipient_id = ?
");
$stmt->execute([$recipientId]);
$groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'userName' => $userName, 'groups' => $groups]);
} catch (PDOException $e) {
error_log("Error fetching user groups: " . $e->getMessage());
echo json_encode(['error' => 'Database error']);
} catch (Exception $e) {
error_log("Error: " . $e->getMessage());
echo json_encode(['error' => 'Server error']);
}
?>

214
admin/languages.php Executable file
View File

@@ -0,0 +1,214 @@
<?php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/db.php';
// Solo para administradores
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
header('Location: /login.php');
exit;
}
$pageTitle = 'Gestionar Idiomas de Traducción';
require_once __DIR__ . '/../templates/header.php';
try {
$stmt = $pdo->query("SELECT * FROM supported_languages ORDER BY language_name ASC");
$languages = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$languages = [];
$errorMessage = "Error al cargar los idiomas: " . $e->getMessage();
}
?>
<div class="container-fluid">
<h1 class="mt-4">Gestionar Idiomas de Traducción</h1>
<p class="text-muted">Activa o desactiva los idiomas a los que el bot traducirá automáticamente los mensajes.</p>
<?php if (isset($errorMessage)): ?>
<div class="alert alert-danger"><?= htmlspecialchars($errorMessage) ?></div>
<?php endif; ?>
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Idiomas Soportados</h5>
<button id="sync-languages-btn" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-repeat me-1"></i> Sincronizar con LibreTranslate
</button>
</div>
<div class="card-body">
<div id="language-list" class="list-group">
<?php if (empty($languages)): ?>
<div class="list-group-item">No se encontraron idiomas.</div>
<?php else: ?>
<?php foreach ($languages as $lang): ?>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<div class="d-flex align-items-center">
<div class="flag-container me-2" style="min-width: 40px;">
<span class="fs-4 flag-emoji"><?= htmlspecialchars($lang['flag_emoji'] ?? '') ?></span>
<div class="edit-flag-form d-none">
<div class="input-group input-group-sm">
<input type="text" class="form-control flag-input" value="<?= htmlspecialchars($lang['flag_emoji'] ?? '') ?>" placeholder=" पेस्ट">
<button class="btn btn-success btn-sm save-flag-btn" data-lang-id="<?= $lang['id'] ?>">✓</button>
</div>
</div>
</div>
<i class="bi bi-pencil-square edit-flag-btn me-2" style="cursor: pointer;" data-lang-id="<?= $lang['id'] ?>"></i>
<div>
<span class="fw-bold"><?= htmlspecialchars($lang['language_name']) ?></span>
<small class="text-muted">(<?= htmlspecialchars($lang['language_code']) ?>)</small>
</div>
</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input language-toggle" type="checkbox" role="switch"
id="lang-toggle-<?= $lang['id'] ?>"
data-lang-id="<?= $lang['id'] ?>"
<?= $lang['is_active'] ? 'checked' : '' ?>>
<label class="form-check-label" for="lang-toggle-<?= $lang['id'] ?>"></label>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<div id="alert-container" class="position-fixed bottom-0 end-0 p-3" style="z-index: 11"></div>
</div>
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
const languageToggles = document.querySelectorAll('.language-toggle');
const syncBtn = document.getElementById('sync-languages-btn');
if (syncBtn) {
syncBtn.addEventListener('click', function () {
const originalHtml = this.innerHTML;
this.disabled = true;
this.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Sincronizando...';
fetch('sync_languages.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(`Sincronización completada. Se añadieron ${data.new_languages} nuevos idiomas.`, 'success');
setTimeout(() => window.location.reload(), 2000);
} else {
showAlert('Error en la sincronización: ' + data.error, 'danger');
this.disabled = false;
this.innerHTML = originalHtml;
}
})
.catch(error => {
showAlert('Error de red durante la sincronización.', 'danger');
this.disabled = false;
this.innerHTML = originalHtml;
});
});
}
languageToggles.forEach(toggle => {
toggle.addEventListener('change', function () {
const langId = this.dataset.langId;
const isActive = this.checked;
fetch('update_language_status.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ id: langId, is_active: isActive })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('Estado del idioma actualizado con éxito.', 'success');
} else {
showAlert('Error: ' + data.error, 'danger');
// Revertir el cambio visual si falla la actualización
this.checked = !isActive;
}
})
.catch(error => {
showAlert('Error de red al actualizar el idioma.', 'danger');
this.checked = !isActive;
});
});
});
// Lógica para editar la bandera
const languageList = document.getElementById('language-list');
if (languageList) {
languageList.addEventListener('click', function(e) {
// Botón de editar
if (e.target.classList.contains('edit-flag-btn')) {
const listItem = e.target.closest('.list-group-item');
listItem.querySelector('.flag-emoji').classList.add('d-none');
e.target.classList.add('d-none');
listItem.querySelector('.edit-flag-form').classList.remove('d-none');
listItem.querySelector('.flag-input').focus();
}
// Botón de guardar
if (e.target.classList.contains('save-flag-btn')) {
const langId = e.target.dataset.langId;
const listItem = e.target.closest('.list-group-item');
const input = listItem.querySelector('.flag-input');
const newEmoji = input.value;
fetch('update_language_flag.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ id: langId, flag_emoji: newEmoji })
})
.then(response => response.json())
.then(data => {
if (data.success) {
const flagEmojiSpan = listItem.querySelector('.flag-emoji');
flagEmojiSpan.textContent = newEmoji;
flagEmojiSpan.classList.remove('d-none');
listItem.querySelector('.edit-flag-form').classList.add('d-none');
listItem.querySelector('.edit-flag-btn').classList.remove('d-none');
showAlert('Bandera actualizada.', 'success');
} else {
showAlert('Error: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('Error de red al actualizar la bandera.', 'danger');
});
}
});
}
function showAlert(message, type = 'success') {
const alertContainer = document.getElementById('alert-container');
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show`;
alert.role = 'alert';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
alertContainer.appendChild(alert);
setTimeout(() => {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}, 3000);
}
});
</script>

538
admin/recipients.php Executable file
View File

@@ -0,0 +1,538 @@
<?php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/activity_logger.php';
// Admin-only access
if ($_SESSION['role'] !== 'admin') {
header('Location: ../index.php?error=unauthorized');
exit();
}
// Initialize variables for edit mode
$edit_mode = false;
$edit_recipient = [
'id' => null,
'name' => '',
'platform_id' => '',
'type' => 'channel',
'platform' => 'discord',
'language_code' => 'es'
];
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$current_user_id = $_SESSION['user_id'];
$current_username = $_SESSION['username'];
$action = $_POST['action'] ?? null;
// Action: Add or Update Recipient
if (isset($_POST['add_recipient']) || isset($_POST['update_recipient'])) {
$platform = $_POST['platform'] ?? 'discord';
$type = $_POST['type'] ?? 'channel';
$name = $_POST['name'] ?? '';
$platform_id = $_POST['platform_id'] ?? '';
$language_code = $_POST['language_code'] ?? 'es';
$id = $_POST['id'] ?? null;
if (empty($name) || empty($platform_id) || empty($type) || empty($platform)) {
$error = "Todos los campos son obligatorios.";
} elseif (!is_numeric($platform_id)) {
$error = "El ID de Plataforma debe ser un número.";
} else {
try {
if (isset($_POST['update_recipient'])) { // UPDATE
$stmt = $pdo->prepare("UPDATE recipients SET name = ?, platform_id = ?, type = ?, platform = ?, language_code = ? WHERE id = ?");
$stmt->execute([$name, $platform_id, $type, $platform, $language_code, $id]);
$details = 'Admin ' . $current_username . ' updated recipient: ' . $name . ' (' . $platform . ':' . $platform_id . ')';
log_activity($current_user_id, 'Recipient Updated', $details);
header('Location: recipients.php?success=updated');
exit();
} else { // ADD
$stmt = $pdo->prepare("INSERT INTO recipients (name, platform_id, type, platform, language_code) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$name, $platform_id, $type, $platform, $language_code]);
$new_recipient_id = $pdo->lastInsertId();
$details = 'Admin ' . $current_username . ' added new recipient: ' . $name . ' (' . $platform . ':' . $platform_id . ')';
log_activity($current_user_id, 'Recipient Added', $details);
$success = "Destinatario añadido con éxito.";
}
} catch (PDOException $e) {
if ($e->errorInfo[1] == 1062) {
$error = "El ID de Plataforma ('$platform_id') ya existe.";
} else {
$error = "Error en la base de datos: " . $e->getMessage();
}
// Keep form data on error
$edit_mode = isset($_POST['update_recipient']);
$edit_recipient = ['id' => $id, 'name' => $name, 'platform_id' => $platform_id, 'type' => $type, 'platform' => $platform, 'language_code' => $language_code];
}
}
}
// Action: Delete Single Recipient
elseif (isset($_POST['delete_recipient'])) {
$id_to_delete = $_POST['id_to_delete'];
try {
$stmt_recipient = $pdo->prepare("SELECT name, platform, platform_id FROM recipients WHERE id = ?");
$stmt_recipient->execute([$id_to_delete]);
$recipient_info = $stmt_recipient->fetch(PDO::FETCH_ASSOC);
$details = 'Admin ' . $current_username . ' deleted recipient: ' . ($recipient_info['name'] ?? 'Unknown') . ' (' . ($recipient_info['platform'] ?? 'N/A') . ':' . ($recipient_info['platform_id'] ?? 'N/A') . ')';
$stmt = $pdo->prepare("DELETE FROM recipients WHERE id = ?");
$stmt->execute([$id_to_delete]);
log_activity($current_user_id, 'Recipient Deleted', $details);
$success = "Destinatario eliminado con éxito.";
} catch (PDOException $e) {
$error = "Error al eliminar. Es posible que el destinatario esté en uso.";
}
}
// Action: Kick Telegram User
elseif ($action === 'kick_telegram_user') {
$recipient_id_to_kick = $_POST['recipient_id_to_kick'] ?? null;
$chat_id_to_kick_from = $_POST['chat_id_to_kick_from'] ?? null;
if ($recipient_id_to_kick && $chat_id_to_kick_from) {
try {
// Get recipient's platform_id (Telegram user ID)
$stmt = $pdo->prepare("SELECT platform_id, name FROM recipients WHERE id = ? AND platform = 'telegram' AND type = 'user'");
$stmt->execute([$recipient_id_to_kick]);
$recipient_info = $stmt->fetch(PDO::FETCH_ASSOC);
$telegram_user_id = $recipient_info['platform_id'] ?? null;
$recipient_name = $recipient_info['name'] ?? 'Unknown';
if ($telegram_user_id) {
// Get bot token
$botToken = $_ENV['TELEGRAM_BOT_TOKEN'] ?? '';
if (empty($botToken)) {
throw new Exception("Token de bot de Telegram no configurado.");
}
// Telegram API URL for banning a chat member
$telegramApiUrl = "https://api.telegram.org/bot{$botToken}/banChatMember";
$params = [
'chat_id' => $chat_id_to_kick_from,
'user_id' => $telegram_user_id,
'until_date' => time() + 30 // Ban for 30 seconds to ensure they are removed, then they can rejoin
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($params),
],
];
$context = stream_context_create($options);
$result = file_get_contents($telegramApiUrl, false, $context);
$response = json_decode($result, true);
if ($response && $response['ok']) {
// If successful, unban immediately to allow re-entry
$unbanTelegramApiUrl = "https://api.telegram.org/bot{$botToken}/unbanChatMember";
$unbanParams = [
'chat_id' => $chat_id_to_kick_from,
'user_id' => $telegram_user_id,
'only_if_banned' => true // Only try to unban if they are actually banned
];
$unbanOptions = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($unbanParams),
],
];
$unbanContext = stream_context_create($unbanOptions);
$unbanResult = file_get_contents($unbanTelegramApiUrl, false, $unbanContext);
$unbanResponse = json_decode($unbanResult, true);
if ($unbanResponse && $unbanResponse['ok']) {
$success = "Usuario de Telegram expulsado del grupo (permite reingreso) y eliminado de la base de datos.";
} else {
// Log unban error but still proceed with local deletion as kick was successful
error_log("Error al desbanear usuario de Telegram después de la expulsión: " . ($unbanResponse['description'] ?? 'Error desconocido'));
$success = "Usuario de Telegram expulsado del grupo (error al permitir reingreso) y eliminado de la base de datos.";
}
// Delete from local DB regardless of unban success, as the kick itself was successful
$stmt = $pdo->prepare("DELETE FROM recipients WHERE id = ?");
$stmt->execute([$recipient_id_to_kick]);
$details = 'Admin ' . $current_username . ' kicked Telegram user: ' . $recipient_name . ' (ID: ' . $telegram_user_id . ') from group ID: ' . $chat_id_to_kick_from;
log_activity($current_user_id, 'Telegram User Kicked', $details);
} else {
$error = "Error al expulsar usuario de Telegram: " . ($response['description'] ?? 'Error desconocido');
}
} else {
$error = "Usuario de Telegram no encontrado o no es un usuario válido para expulsar.";
}
} catch (Exception $e) {
$error = "Error al procesar la expulsión: " . $e->getMessage();
}
} else {
$error = "Faltan parámetros para expulsar al usuario de Telegram.";
}
}
// Action: Delete Multiple Recipients
elseif ($action === 'delete_selected' && !empty($_POST['selected_recipients'])) {
$deleted_count = 0;
$error_count = 0;
foreach ($_POST['selected_recipients'] as $recipient_id) {
try {
$stmt_recipient = $pdo->prepare("SELECT name, platform, platform_id FROM recipients WHERE id = ?");
$stmt_recipient->execute([$recipient_id]);
$recipient_info = $stmt_recipient->fetch(PDO::FETCH_ASSOC);
$details = 'Admin ' . $current_username . ' deleted recipient: ' . ($recipient_info['name'] ?? 'Unknown') . ' (' . ($recipient_info['platform'] ?? 'N/A') . ':' . ($recipient_info['platform_id'] ?? 'N/A') . ')';
$stmt = $pdo->prepare("DELETE FROM recipients WHERE id = ?");
$stmt->execute([$recipient_id]);
log_activity($current_user_id, 'Recipient Deleted', $details);
$deleted_count++;
} catch (PDOException $e) {
$error_count++;
error_log("Error al eliminar destinatario ID $recipient_id: " . $e->getMessage());
}
}
if ($deleted_count > 0) {
$success = "Se eliminaron $deleted_count destinatarios correctamente.";
if ($error_count > 0) {
$error = "Hubo errores al eliminar $error_count destinatarios.";
}
header('Location: recipients.php?success=deleted_multiple&deleted=' . $deleted_count . '&errors=' . $error_count);
exit();
} else if ($error_count > 0) {
$error = "No se pudo eliminar ningún destinatario. Por favor, inténtalo de nuevo.";
}
}
}
// Handle entering edit mode via GET request
if (isset($_GET['action']) && $_GET['action'] === 'edit' && isset($_GET['id'])) {
$edit_mode = true;
$stmt = $pdo->prepare("SELECT * FROM recipients WHERE id = ?");
$stmt->execute([$_GET['id']]);
$recipient_to_edit = $stmt->fetch();
if ($recipient_to_edit) {
$edit_recipient = $recipient_to_edit;
}
}
// Fetch all recipients to display
$recipients = $pdo->query("SELECT * FROM recipients ORDER BY platform, type, name ASC")->fetchAll();
$pageTitle = 'Gestionar Destinatarios';
require_once __DIR__ . '/../templates/header.php';
// Display feedback messages
if (isset($error)) echo "<div class='alert alert-danger'>$error</div>";
if (isset($success)) echo "<div class='alert alert-success'>$success</div>";
if (isset($_GET['success'])) {
if ($_GET['success'] === 'deleted_multiple') {
$deleted = isset($_GET['deleted']) ? (int)$_GET['deleted'] : 0;
$errors = isset($_GET['errors']) ? (int)$_GET['errors'] : 0;
if ($deleted > 0) {
echo '<div class="alert alert-success">Se eliminaron ' . $deleted . ' destinatarios correctamente.</div>';
}
if ($errors > 0) {
echo '<div class="alert alert-danger">Hubo errores al eliminar ' . $errors . ' destinatarios.</div>';
}
} else {
echo "<div class='alert alert-success'>Operación completada con éxito.</div>";
}
}
?>
<div class="container-fluid">
<h1 class="mt-4" data-translate="true">Gestionar Destinatarios</h1>
<!-- Add/Edit Recipient Form -->
<div class="card shadow-sm mb-4">
<div class="card-header">
<h5 class="mb-0" data-translate="true"><?= $edit_mode ? 'Editar Destinatario' : 'Añadir Nuevo Destinatario' ?></h5>
</div>
<div class="card-body">
<form action="recipients.php" method="POST">
<?php if ($edit_mode): ?><input type="hidden" name="id" value="<?= $edit_recipient['id'] ?>"><?php endif; ?>
<div class="row align-items-end">
<div class="col-md-2 mb-3">
<label for="platform" class="form-label" data-translate="true">Plataforma</label>
<select class="form-select" id="platform" name="platform" required>
<option value="discord" <?= ($edit_recipient['platform'] === 'discord') ? 'selected' : '' ?> data-translate="true">Discord</option>
<option value="telegram" <?= ($edit_recipient['platform'] === 'telegram') ? 'selected' : '' ?> data-translate="true">Telegram</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label for="name" class="form-label" data-translate="true">Nombre (identificador)</label>
<input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($edit_recipient['name']) ?>" required>
</div>
<div class="col-md-2 mb-3">
<label for="platform_id" class="form-label" data-translate="true">ID de Plataforma</label>
<input type="text" class="form-control" id="platform_id" name="platform_id" value="<?= htmlspecialchars($edit_recipient['platform_id']) ?>" required>
</div>
<div class="col-md-2 mb-3">
<label for="type" class="form-label" data-translate="true">Tipo</label>
<select class="form-select" id="type" name="type" required>
<option value="channel" <?= ($edit_recipient['type'] === 'channel') ? 'selected' : '' ?> data-translate="true">Canal/Grupo</option>
<option value="user" <?= ($edit_recipient['type'] === 'user') ? 'selected' : '' ?> data-translate="true">Usuario</option>
</select>
</div>
<div class="col-md-1 mb-3">
<label for="language_code" class="form-label" data-translate="true">Idioma</label>
<select class="form-select" id="language_code" name="language_code" required>
<option value="es" <?= ($edit_recipient['language_code'] === 'es') ? 'selected' : '' ?>>ES</option>
<option value="en" <?= ($edit_recipient['language_code'] === 'en') ? 'selected' : '' ?>>EN</option>
<option value="fr" <?= ($edit_recipient['language_code'] === 'fr') ? 'selected' : '' ?>>FR</option>
<option value="de" <?= ($edit_recipient['language_code'] === 'de') ? 'selected' : '' ?>>DE</option>
<option value="it" <?= ($edit_recipient['language_code'] === 'it') ? 'selected' : '' ?>>IT</option>
<option value="pt" <?= ($edit_recipient['language_code'] === 'pt') ? 'selected' : '' ?>>PT</option>
<option value="ru" <?= ($edit_recipient['language_code'] === 'ru') ? 'selected' : '' ?>>RU</option>
<option value="ja" <?= ($edit_recipient['language_code'] === 'ja') ? 'selected' : '' ?>>JA</option>
<option value="zh" <?= ($edit_recipient['language_code'] === 'zh') ? 'selected' : '' ?>>ZH</option>
</select>
</div>
<div class="col-md-2 mb-3">
<?php if ($edit_mode): ?>
<button type="submit" name="update_recipient" class="btn btn-success w-100">
<i class="bi bi-save"></i> <span data-translate="true">Guardar</span>
</button>
<?php else: ?>
<button type="submit" name="add_recipient" class="btn btn-primary w-100">
<i class="bi bi-plus-circle"></i> <span data-translate="true">Añadir</span>
</button>
<?php endif; ?>
</div>
<?php if ($edit_mode): ?>
<div class="col-md-12"><a href="recipients.php" class="btn btn-sm btn-outline-secondary" data-translate="true">Cancelar Edición</a></div>
<?php endif; ?>
</div>
</form>
</div>
</div>
<!-- Recipients List -->
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" data-translate="true">Lista de Destinatarios</h5>
<div>
<button type="button" id="selectAllBtn" class="btn btn-sm btn-outline-secondary me-2">
<i class="bi bi-check2-square"></i> <span data-translate="true">Seleccionar todo</span>
</button>
<button type="button" id="deleteSelectedBtn" class="btn btn-sm btn-danger" disabled>
<i class="bi bi-trash"></i> <span data-translate="true">Eliminar seleccionados</span>
</button>
</div>
</div>
<div class="card-body">
<form id="recipientsForm" action="" method="POST" onsubmit="return confirm('¿Estás seguro de que quieres eliminar los destinatarios seleccionados?');">
<input type="hidden" name="action" value="delete_selected">
<input type="hidden" name="confirm_message" value="¿Estás seguro de que quieres eliminar los destinatarios seleccionados?" data-translate-confirm="¿Estás seguro de que quieres eliminar los destinatarios seleccionados?">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th width="40"><input type="checkbox" id="selectAll"></th>
<th data-translate="true">Plataforma</th>
<th data-translate="true">Nombre</th>
<th data-translate="true">ID de Plataforma</th>
<th data-translate="true">Tipo</th>
<th data-translate="true">Idioma</th>
<th data-translate="true">Añadido en</th>
<th class="text-center" data-translate="true">Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($recipients as $recipient): ?>
<tr>
<td><input type="checkbox" name="selected_recipients[]" value="<?= $recipient['id'] ?>" class="recipient-checkbox"></td>
<td>
<span class="badge <?= $recipient['platform'] === 'discord' ? 'bg-primary' : 'bg-info' ?>">
<?= htmlspecialchars(ucfirst($recipient['platform'])) ?>
</span>
</td>
<td><?= htmlspecialchars($recipient['name']) ?></td>
<td><?= htmlspecialchars($recipient['platform_id']) ?></td>
<td><?= htmlspecialchars(ucfirst($recipient['type'])) ?></td>
<td><span class="badge bg-secondary"><?= strtoupper(htmlspecialchars($recipient['language_code'] ?? 'es')) ?></span></td>
<td><?= date('d/m/Y H:i', strtotime($recipient['created_at'])) ?></td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="?action=edit&id=<?= $recipient['id'] ?>" class="btn btn-sm btn-primary" title="Editar">
<i class="bi bi-pencil"></i>
</a>
<form action="recipients.php" method="POST" onsubmit="return confirm(this.querySelector('[data-translate-confirm]').getAttribute('data-translate-confirm'));" class="d-inline ms-1">
<input type="hidden" name="id_to_delete" value="<?= $recipient['id'] ?>">
<input type="hidden" name="confirm_message" value="¿Estás seguro de que quieres eliminar este destinatario?" data-translate-confirm="¿Estás seguro de que quieres eliminar este destinatario?">
<button type="submit" name="delete_recipient" class="btn btn-sm btn-danger" title="Eliminar" data-translate-title="true">
<i class="bi bi-trash"></i>
</button>
</form>
<?php if ($recipient['platform'] === 'telegram' && $recipient['type'] === 'user'): ?>
<button type="button" class="btn btn-sm btn-warning ms-1 kick-telegram-user-btn"
data-bs-toggle="modal" data-bs-target="#kickUserModal"
data-recipient-id="<?= $recipient['id'] ?>"
title="Expulsar de Telegram y Eliminar" data-translate-title="true">
<i class="bi bi-person-x"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</form>
</div>
</div>
</div>
<?php require_once __DIR__ . '/../templates/footer.php'; ?>
<!-- Modal para Expulsar Usuario de Telegram -->
<div class="modal fade" id="kickUserModal" tabindex="-1" aria-labelledby="kickUserModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="kickUserModalLabel">Expulsar Usuario de Telegram</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="kickUserForm" action="recipients.php" method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="kick_telegram_user">
<input type="hidden" name="recipient_id_to_kick" id="modalRecipientId">
<p>Selecciona el grupo del que deseas expulsar a <strong id="modalUserName"></strong>:</p>
<div id="groupListContainer">
<!-- Los grupos se cargarán aquí dinámicamente -->
<p class="text-muted">Cargando grupos...</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-danger" id="confirmKickBtn" disabled>Expulsar y Eliminar</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Selección múltiple
const selectAllCheckbox = document.getElementById('selectAll');
const recipientCheckboxes = document.querySelectorAll('.recipient-checkbox');
const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
const selectAllBtn = document.getElementById('selectAllBtn');
// Seleccionar/deseleccionar todos los checkboxes
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
const isChecked = this.checked;
recipientCheckboxes.forEach(checkbox => {
checkbox.checked = isChecked;
});
updateDeleteButton();
});
}
// Botón "Seleccionar todo"
if (selectAllBtn) {
selectAllBtn.addEventListener('click', function() {
const allChecked = Array.from(recipientCheckboxes).every(checkbox => checkbox.checked);
recipientCheckboxes.forEach(checkbox => {
checkbox.checked = !allChecked;
});
selectAllCheckbox.checked = !allChecked;
updateDeleteButton();
});
}
// Actualizar el estado del botón de eliminar seleccionados
function updateDeleteButton() {
const checkedBoxes = document.querySelectorAll('.recipient-checkbox:checked');
deleteSelectedBtn.disabled = checkedBoxes.length === 0;
}
// Actualizar el checkbox "Seleccionar todo" cuando se marcan/desmarcan checkboxes individuales
recipientCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateDeleteButton();
selectAllCheckbox.checked = Array.from(recipientCheckboxes).every(checkbox => checkbox.checked);
});
});
// Manejar el clic en el botón de eliminar seleccionados
if (deleteSelectedBtn) {
deleteSelectedBtn.addEventListener('click', function() {
if (confirm('¿Estás seguro de que quieres eliminar los destinatarios seleccionados?')) {
document.getElementById('recipientsForm').submit();
}
});
}
// Lógica para el modal de expulsar usuario de Telegram
const kickUserModal = document.getElementById('kickUserModal');
const modalRecipientId = document.getElementById('modalRecipientId');
const modalUserName = document.getElementById('modalUserName');
const groupListContainer = document.getElementById('groupListContainer');
const confirmKickBtn = document.getElementById('confirmKickBtn');
const kickUserForm = document.getElementById('kickUserForm');
kickUserModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget; // Botón que activó el modal
const recipientId = button.getAttribute('data-recipient-id');
modalRecipientId.value = recipientId;
modalUserName.textContent = 'Cargando...';
groupListContainer.innerHTML = '<p class="text-muted">Cargando grupos...</p>';
confirmKickBtn.disabled = true;
// Cargar los grupos del usuario vía AJAX
fetch(`get_user_groups.php?recipient_id=${recipientId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
modalUserName.textContent = data.userName;
if (data.groups.length > 0) {
let groupsHtml = '';
data.groups.forEach(group => {
groupsHtml += `
<div class="form-check">
<input class="form-check-input" type="radio" name="chat_id_to_kick_from" id="group_${group.chat_id}" value="${group.chat_id}">
<label class="form-check-label" for="group_${group.chat_id}">
${group.group_name} (ID: ${group.chat_id})
</label>
</div>
`;
});
groupListContainer.innerHTML = groupsHtml;
// Habilitar el botón de confirmar cuando se selecciona un radio
groupListContainer.querySelectorAll('input[type="radio"]').forEach(radio => {
radio.addEventListener('change', () => {
confirmKickBtn.disabled = false;
});
});
} else {
groupListContainer.innerHTML = '<p class="text-info">Este usuario no está registrado en ningún grupo de Telegram.</p>';
}
} else {
groupListContainer.innerHTML = `<p class="text-danger">Error al cargar grupos: ${data.error}</p>`;
}
})
.catch(error => {
console.error('Error fetching groups:', error);
groupListContainer.innerHTML = '<p class="text-danger">Error de red al cargar grupos.</p>';
});
});
// Confirmación final antes de enviar el formulario de expulsión
kickUserForm.addEventListener('submit', function(event) {
if (!confirm('¿Estás seguro de que quieres expulsar a este usuario del grupo seleccionado y eliminarlo de la base de datos?')) {
event.preventDefault();
}
});
});
</script>

65
admin/set_test_webhook.php Executable file
View File

@@ -0,0 +1,65 @@
<?php
// Cargar configuración del entorno de pruebas
$envFile = __DIR__ . '/.env.pruebas';
if (!file_exists($envFile)) {
die("Error: No se encontró el archivo de configuración de pruebas (.env.pruebas)\n");
}
// Cargar variables de entorno
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value, "'\" \t\n\r\0\x0B");
if (!empty($name)) $_ENV[$name] = $value;
}
// Configuración
$botToken = $_ENV['TELEGRAM_BOT_TOKEN'] ?? '';
$webhookToken = $_ENV['TELEGRAM_WEBHOOK_TOKEN'] ?? 'webhook_secure_token_12345';
$webhookUrl = 'https://pruebaspons.duckdns.org/telegram_bot_webhook.php?auth_token=' . urlencode($webhookToken);
if (empty($botToken)) die("Error: No se configuró TELEGRAM_BOT_TOKEN\n");
echo "Configurando webhook para pruebas...\nURL: $webhookUrl\n";
// Configurar webhook
$apiUrl = "https://api.telegram.org/bot{$botToken}/setWebhook";
$postData = ['url' => $webhookUrl, 'allowed_updates' => json_encode(['message', 'callback_query'])];
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// Mostrar resultados
echo "\nRespuesta de la API de Telegram (HTTP $httpCode):\n";
if ($error) {
echo "Error: $error\n";
} else {
$result = json_decode($response, true);
if (json_last_error() === JSON_ERROR_NONE) {
if ($result['ok'] ?? false) {
echo "✅ Webhook configurado correctamente.\n";
echo "URL: " . ($result['result']['url'] ?? 'N/A') . "\n";
echo "Tiene certificado: " . ($result['result']['has_custom_certificate'] ? 'Sí' : 'No') . "\n";
echo "Updates pendientes: " . ($result['result']['pending_update_count'] ?? '0') . "\n";
} else {
echo "❌ Error al configurar el webhook: " . ($result['description'] ?? 'Error desconocido') . "\n";
}
} else {
echo "Respuesta no válida: " . substr($response, 0, 200) . "...\n";
}
}
echo "\nPara verificar la configuración actual del webhook, ejecuta:\n";
echo "curl -s 'https://api.telegram.org/bot{$botToken}/getWebhookInfo' | jq\n\n";

66
admin/sync_languages.php Executable file
View File

@@ -0,0 +1,66 @@
<?php
// admin/sync_languages.php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/logger.php';
require_once __DIR__ . '/../src/Translate.php';
header('Content-Type: application/json');
// Envolver todo en un manejador de errores para capturar hasta los errores fatales
register_shutdown_function(function () {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
// Limpiar cualquier salida anterior
if (ob_get_length()) {
ob_end_clean();
}
echo json_encode(['success' => false, 'error' => 'Error fatal en el servidor: ' . $error['message'] . ' en ' . $error['file'] . ' línea ' . $error['line']]);
}
});
// Solo para administradores
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
echo json_encode(['success' => false, 'error' => 'Acceso denegado.']);
exit;
}
try {
// 1. Verificar que la URL de LibreTranslate esté configurada
if (empty($_ENV['LIBRETRANSLATE_URL'])) {
throw new Exception("La variable de entorno LIBRETRANSLATE_URL no está configurada en tu archivo .env");
}
$translator = new Translate();
$libreLanguages = $translator->getSupportedLanguages();
if ($libreLanguages === null) {
throw new Exception("No se pudo obtener la lista de idiomas de LibreTranslate. Revisa que la URL ('" . htmlspecialchars($_ENV['LIBRETRANSLATE_URL']) . "') sea correcta y que el servicio esté funcionando.");
}
if (empty($libreLanguages)) {
throw new Exception("LibreTranslate devolvió una lista de idiomas vacía.");
}
$newLanguagesCount = 0;
$sql = "INSERT IGNORE INTO supported_languages (language_code, language_name, is_active) VALUES (?, ?, 0)";
$stmt = $pdo->prepare($sql);
foreach ($libreLanguages as $lang) {
if (isset($lang['code']) && isset($lang['name'])) {
$stmt->execute([$lang['code'], $lang['name']]);
if ($stmt->rowCount() > 0) {
$newLanguagesCount++;
}
}
}
echo json_encode(['success' => true, 'new_languages' => $newLanguagesCount]);
} catch (Throwable $e) { // Captura Throwable para errores y excepciones
error_log("Error en sync_languages.php: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
?>

408
admin/test_discord_connection.php Executable file
View File

@@ -0,0 +1,408 @@
<?php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../src/DiscordSender.php';
require_once __DIR__ . '/../config/config.php';
// Verificar que el usuario sea administrador
if ($_SESSION['role'] !== 'admin') {
header('HTTP/1.0 403 Forbidden');
die('Acceso denegado. Solo los administradores pueden acceder a esta página.');
}
$message = '';
$error = '';
$success = '';
$discordResponse = null;
$permissionsInfo = [];
$telegramWebhookInfo = null;
// Manejar el envío del formulario de prueba
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Verificar si es una solicitud de verificación de permisos de Discord
if (isset($_POST['check_permissions']) && !empty($_POST['guild_id'])) {
$guildId = trim($_POST['guild_id']);
try {
$ch = curl_init("https://discord.com/api/v10/guilds/{$guildId}");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"Authorization: Bot " . DISCORD_BOT_TOKEN,
"Content-Type: application/json"
],
CURLOPT_RETURNTRANSFER => true
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode === 200) {
$guild = json_decode($response, true);
$ch = curl_init("https://discord.com/api/v10/users/@me");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bot " . DISCORD_BOT_TOKEN],
CURLOPT_RETURNTRANSFER => true
]);
$botInfo = json_decode(curl_exec($ch), true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode !== 200 || !isset($botInfo['id'])) {
throw new Exception("No se pudo obtener la información del bot de Discord. Verifica que el token sea correcto. Código HTTP: $httpCode");
}
$botId = $botInfo['id'];
$ch = curl_init("https://discord.com/api/v10/guilds/{$guildId}/roles");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bot " . DISCORD_BOT_TOKEN],
CURLOPT_RETURNTRANSFER => true
]);
$roles = json_decode(curl_exec($ch), true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode === 200 && is_array($roles)) {
$ch = curl_init("https://discord.com/api/v10/guilds/{$guildId}/members/{$botId}");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bot " . DISCORD_BOT_TOKEN],
CURLOPT_RETURNTRANSFER => true
]);
$member = json_decode(curl_exec($ch), true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode === 404) {
throw new Exception("El bot no está en el servidor de Discord especificado.");
} elseif ($httpCode !== 200) {
throw new Exception("Error al obtener información del bot en el servidor. Código HTTP: $httpCode");
}
if (isset($member['roles']) && is_array($member['roles'])) {
$botRoles = array_filter($roles, fn($role) => in_array($role['id'], $member['roles']));
$permissions = 0;
foreach ($botRoles as $role) {
$permissions |= intval($role['permissions']);
}
$permissionsInfo = [
'guild_name' => $guild['name'] ?? 'Desconocido',
'bot_has_admin' => ($permissions & 0x8) === 0x8,
'can_kick' => ($permissions & 0x2) === 0x2,
'can_ban' => ($permissions & 0x4) === 0x4,
'permissions_int' => $permissions,
'permissions_bin' => decbin($permissions),
'roles' => array_column($botRoles, 'name')
];
$success = "Permisos de Discord verificados para el servidor: " . htmlspecialchars($permissionsInfo['guild_name']);
} else {
$error = "No se pudo obtener la información de roles del bot en el servidor de Discord.";
}
} else {
$error = "No se pudieron obtener los roles del servidor de Discord. Código HTTP: $httpCode";
}
} else {
$error = "Error al obtener información del servidor de Discord. Código HTTP: $httpCode";
}
} catch (Exception $e) {
$error = "Error al verificar permisos de Discord: " . $e->getMessage();
}
}
// Verificar si es una solicitud de verificación de webhook de Telegram
elseif (isset($_POST['check_telegram_webhook'])) {
try {
if (!defined('TELEGRAM_BOT_TOKEN') || empty(TELEGRAM_BOT_TOKEN)) {
throw new Exception("La constante TELEGRAM_BOT_TOKEN no está definida o está vacía.");
}
$botToken = TELEGRAM_BOT_TOKEN;
$apiUrl = "https://api.telegram.org/bot{$botToken}/getWebhookInfo";
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10, // Segundos para esperar la conexión
CURLOPT_TIMEOUT => 20, // Segundos para la ejecución total de cURL
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
throw new Exception('Error en cURL al contactar la API de Telegram: ' . curl_error($ch));
}
if ($httpCode !== 200) {
throw new Exception("La API de Telegram devolvió un código HTTP {$httpCode}. Respuesta: " . $response);
}
$result = json_decode($response, true);
if (!$result['ok']) {
throw new Exception("Error de la API de Telegram: " . ($result['description'] ?? 'Error desconocido'));
}
$telegramWebhookInfo = $result['result'];
$success = "Información del webhook de Telegram obtenida correctamente.";
} catch (Exception $e) {
$error = "Error al verificar el webhook de Telegram: " . $e->getMessage();
}
}
// Manejar la eliminación del webhook de Telegram
elseif (isset($_POST['delete_telegram_webhook'])) {
try {
if (!defined('TELEGRAM_BOT_TOKEN') || empty(TELEGRAM_BOT_TOKEN)) {
throw new Exception("La constante TELEGRAM_BOT_TOKEN no está definida o está vacía.");
}
$botToken = TELEGRAM_BOT_TOKEN;
$apiUrl = "https://api.telegram.org/bot{$botToken}/deleteWebhook";
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$result = json_decode($response, true);
if ($httpCode === 200 && $result['ok']) {
$success = "Webhook de Telegram eliminado correctamente.";
} else {
throw new Exception("Error al eliminar el webhook: " . ($result['description'] ?? 'Respuesta inválida de la API.'));
}
} catch (Exception $e) {
$error = "Error al eliminar el webhook de Telegram: " . $e->getMessage();
}
}
// Manejar la configuración del webhook de Telegram
elseif (isset($_POST['set_telegram_webhook'])) {
try {
// Las constantes son definidas en config.php
if (!defined('TELEGRAM_BOT_TOKEN') || !defined('BOT_BASE_URL') || !defined('TELEGRAM_WEBHOOK_TOKEN')) {
throw new Exception("Una o más constantes requeridas (TELEGRAM_BOT_TOKEN, BOT_BASE_URL, TELEGRAM_WEBHOOK_TOKEN) no están definidas. Revisa tu archivo .env y la configuración.");
}
$botToken = TELEGRAM_BOT_TOKEN;
$webhookUrl = rtrim(BOT_BASE_URL, '/') . '/telegram/webhook/telegram_bot_webhook.php?auth_token=' . TELEGRAM_WEBHOOK_TOKEN;
$apiUrl = "https://api.telegram.org/bot{$botToken}/setWebhook?url=" . urlencode($webhookUrl);
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$result = json_decode($response, true);
if ($httpCode === 200 && $result['ok']) {
$success = "Webhook de Telegram configurado correctamente en: " . htmlspecialchars($webhookUrl);
} else {
throw new Exception("Error al configurar el webhook: " . ($result['description'] ?? 'Respuesta inválida de la API.'));
}
} catch (Exception $e) {
$error = "Error al configurar el webhook de Telegram: " . $e->getMessage();
}
}
// Si es un envío de mensaje de prueba de Discord
elseif (isset($_POST['discord_id']) && !empty($_POST['test_content'])) {
$recipientId = trim($_POST['discord_id']);
$testContent = trim($_POST['test_content']);
$testRecipientType = $_POST['test_recipient_type'] ?? 'channel';
try {
$logDir = __DIR__ . '/../logs';
if (!is_dir($logDir)) mkdir($logDir, 0755, true);
error_log("Test Discord: ID={$recipientId}, Type={$testRecipientType}, Token=" . substr(DISCORD_BOT_TOKEN, 0, 8) . "...", 3, $logDir . '/discord_api.log');
$discordSender = new DiscordSender(DISCORD_BOT_TOKEN);
$discordResponse = $discordSender->sendMessage($recipientId, $testContent, $testRecipientType);
$success = "Mensaje de prueba de Discord enviado con éxito.";
} catch (Exception $e) {
$error = "Error al enviar mensaje de prueba de Discord: " . $e->getMessage();
}
} else {
$error = "Por favor, completa todos los campos requeridos.";
}
}
// Incluir el encabezado que contiene el menú lateral
require_once __DIR__ . '/../templates/header.php';
?>
<!-- Contenido principal -->
<div id="page-content-wrapper">
<div class="container-fluid">
<h1 class="mt-4">Probar Conexiones de Bots</h1>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><?php echo $success; ?></div>
<?php endif; ?>
<div class="card shadow-sm mb-4">
<div class="card-header">
<h5 class="mb-0">Enviar Mensaje de Prueba a Discord</h5>
</div>
<div class="card-body">
<form action="" method="POST">
<div class="mb-3">
<label for="discord_id" class="form-label">ID de Canal o Usuario de Discord</label>
<input type="text" class="form-control" id="discord_id" name="discord_id" required
value="<?php echo htmlspecialchars($_POST['discord_id'] ?? ''); ?>">
</div>
<div class="mb-3">
<label class="form-label">Tipo de Destinatario</label>
<div>
<input type="radio" id="testRecipientTypeChannel" name="test_recipient_type" value="channel"
<?php echo (($_POST['test_recipient_type'] ?? 'channel') === 'channel') ? 'checked' : ''; ?>>
<label for="testRecipientTypeChannel">Canal</label>
<input type="radio" id="testRecipientTypeUser" name="test_recipient_type" value="user" class="ms-3"
<?php echo (($_POST['test_recipient_type'] ?? 'channel') === 'user') ? 'checked' : ''; ?>>
<label for="testRecipientTypeUser">Usuario</label>
</div>
</div>
<div class="mb-3">
<label for="test_content" class="form-label">Contenido del Mensaje</label>
<textarea class="form-control" id="test_content" name="test_content" rows="3" required><?php
echo htmlspecialchars($_POST['test_content'] ?? 'Mensaje de prueba desde el bot.');
?></textarea>
</div>
<button type="submit" class="btn btn-primary">Enviar Prueba a Discord</button>
</form>
</div>
</div>
<?php if (isset($discordResponse) && $discordResponse !== null): ?>
<div class="card shadow-sm mb-4">
<div class="card-header"><h5 class="mb-0">Respuesta de la API de Discord</h5></div>
<div class="card-body"><pre><code><?php print_r($discordResponse); ?></code></pre></div>
</div>
<?php endif; ?>
<div class="card shadow-sm mb-4">
<div class="card-header"><h5 class="mb-0">Verificar Permisos del Bot de Discord</h5></div>
<div class="card-body">
<form action="" method="POST" class="mb-4">
<input type="hidden" name="check_permissions" value="1">
<div class="mb-3">
<label for="guild_id" class="form-label">ID del Servidor de Discord</label>
<input type="text" class="form-control" id="guild_id" name="guild_id" required
value="<?php echo htmlspecialchars($_POST['guild_id'] ?? ''); ?>"
placeholder="Ej: 123456789012345678">
</div>
<button type="submit" class="btn btn-info">Verificar Permisos de Discord</button>
</form>
<?php if (!empty($permissionsInfo)): ?>
<div class="alert alert-info">
<h6>Resultado de la verificación de Discord:</h6>
<ul class="mb-0">
<li>Servidor: <strong><?php echo htmlspecialchars($permissionsInfo['guild_name']); ?></strong></li>
<li>Es Administrador:
<span class="badge bg-<?php echo $permissionsInfo['bot_has_admin'] ? 'success' : 'danger'; ?>">
<?php echo $permissionsInfo['bot_has_admin'] ? 'Sí' : 'No'; ?>
</span>
</li>
</ul>
</div>
<?php endif; ?>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header"><h5 class="mb-0">Verificar Estado del Webhook de Telegram</h5></div>
<div class="card-body">
<p>Obtén el estado actual de tu webhook directamente desde la API de Telegram para diagnosticar problemas de conexión.</p>
<form action="" method="POST">
<input type="hidden" name="check_telegram_webhook" value="1">
<button type="submit" class="btn btn-info">Verificar Webhook de Telegram</button>
</form>
<?php if (isset($telegramWebhookInfo) && is_array($telegramWebhookInfo)): ?>
<div class="mt-4">
<h6>Resultado de la verificación de Telegram:</h6>
<?php if (empty($telegramWebhookInfo['url'])): ?>
<div class="alert alert-danger">
<strong>No hay webhook configurado.</strong> El bot está funcionando en modo 'getUpdates'.
</div>
<?php else: ?>
<ul class="list-group">
<li class="list-group-item"><strong>URL:</strong> <code><?php echo htmlspecialchars($telegramWebhookInfo['url']); ?></code></li>
<li class="list-group-item"><strong>Actualizaciones pendientes:</strong>
<span class="badge bg-<?php echo $telegramWebhookInfo['pending_update_count'] > 0 ? 'warning' : 'success'; ?>">
<?php echo $telegramWebhookInfo['pending_update_count']; ?>
</span>
</li>
<?php if (!empty($telegramWebhookInfo['last_error_date'])): ?>
<li class="list-group-item list-group-item-danger">
<strong>Último error:</strong> <?php echo date('Y-m-d H:i:s', $telegramWebhookInfo['last_error_date']); ?>
<br>
<strong>Mensaje:</strong> <?php echo htmlspecialchars($telegramWebhookInfo['last_error_message']); ?>
</li>
<?php else: ?>
<li class="list-group-item list-group-item-success"><strong>Último error:</strong> Ninguno reportado.</li>
<?php endif; ?>
<li class="list-group-item"><strong>Máx. Conexiones:</strong> <?php echo $telegramWebhookInfo['max_connections'] ?? 'No definido'; ?></li>
<li class="list-group-item"><strong>Actualizaciones permitidas:</strong>
<?php if (!empty($telegramWebhookInfo['allowed_updates']) && count($telegramWebhookInfo['allowed_updates']) > 0): ?>
<code><?php echo implode(', ', $telegramWebhookInfo['allowed_updates']); ?></code>
<?php else: ?>
<span>Todos los tipos (por defecto).</span>
<?php endif; ?>
</li>
</ul>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header"><h5 class="mb-0">Gestionar Webhook de Telegram</h5></div>
<div class="card-body">
<p>Usa estos botones para eliminar o re-configurar el webhook de Telegram. Esto es útil para forzar a Telegram a actualizar la dirección IP de tu servidor si ha cambiado (por ejemplo, con DuckDNS).</p>
<form action="" method="POST" class="d-inline me-2">
<input type="hidden" name="delete_telegram_webhook" value="1">
<button type="submit" class="btn btn-danger" onclick="return confirm('¿Estás seguro de que quieres eliminar el webhook? El bot dejará de recibir actualizaciones.');">Eliminar Webhook</button>
</form>
<form action="" method="POST" class="d-inline">
<input type="hidden" name="set_telegram_webhook" value="1">
<button type="submit" class="btn btn-success">Configurar Webhook</button>
</form>
</div>
</div>
</div>
</div>
<!-- Scripts de Bootstrap -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<script>
// Toggle del menú lateral
function toggleSidebar() {
document.getElementById('wrapper').classList.toggle('toggled');
}
// Función para copiar al portapapeles
function copyToClipboard(elementId) {
var copyText = document.getElementById(elementId);
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
var button = event.target;
var originalText = button.innerHTML;
button.innerHTML = '¡Copiado!';
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-success');
setTimeout(function() {
button.innerHTML = originalText;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 2000);
}
</script>
<?php require_once __DIR__ . '/../templates/footer.php'; ?>

42
admin/update_language_flag.php Executable file
View File

@@ -0,0 +1,42 @@
<?php
// admin/update_language_flag.php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/db.php';
header('Content-Type: application/json');
// Solo para administradores
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
echo json_encode(['success' => false, 'error' => 'Acceso denegado.']);
exit;
}
// Verificar que la solicitud sea AJAX y POST
if (strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'xmlhttprequest' || $_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Solicitud no válida.']);
exit;
}
$data = json_decode(file_get_contents('php://input'), true);
$langId = $data['id'] ?? null;
$flagEmoji = $data['flag_emoji'] ?? '';
if ($langId === null) {
echo json_encode(['success' => false, 'error' => 'ID de idioma no proporcionado.']);
exit;
}
try {
$stmt = $pdo->prepare("UPDATE supported_languages SET flag_emoji = ? WHERE id = ?");
$stmt->execute([$flagEmoji, $langId]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
error_log("Error en update_language_flag.php: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => 'Error en la base de datos.']);
}
?>

View File

@@ -0,0 +1,49 @@
<?php
// admin/update_language_status.php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/db.php';
header('Content-Type: application/json');
// Verificar que la solicitud sea AJAX y POST
if (strtolower($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'xmlhttprequest' || $_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Solicitud no válida.']);
exit;
}
// Solo para administradores
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
echo json_encode(['success' => false, 'error' => 'Acceso denegado.']);
exit;
}
// Obtener datos del cuerpo de la solicitud
$data = json_decode(file_get_contents('php://input'), true);
$langId = $data['id'] ?? null;
$isActive = isset($data['is_active']) ? (int)$data['is_active'] : null;
if ($langId === null || $isActive === null) {
echo json_encode(['success' => false, 'error' => 'Datos incompletos.']);
exit;
}
try {
$stmt = $pdo->prepare("UPDATE supported_languages SET is_active = ? WHERE id = ?");
$stmt->execute([$isActive, $langId]);
if ($stmt->rowCount() > 0) {
echo json_encode(['success' => true]);
} else {
// No se actualizó ninguna fila, podría ser que el ID no exista
echo json_encode(['success' => false, 'error' => 'El idioma no fue encontrado o el estado ya era el mismo.']);
}
} catch (PDOException $e) {
// Loguear el error real en el servidor
error_log("Error en update_language_status.php: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => 'Error en la base de datos.']);
}
?>

201
admin/users.php Executable file
View File

@@ -0,0 +1,201 @@
<?php
require_once __DIR__ . '/../includes/session_check.php';
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/activity_logger.php';
// Admin-only access
if ($_SESSION['role'] !== 'admin') {
header('Location: ../index.php?error=unauthorized');
exit();
}
// Inicializar variables para el modo edición
$edit_mode = false;
$edit_id = null;
$edit_username = '';
$edit_role = 'user';
$edit_telegram_chat_id = '';
// Handle GET actions for editing
if (isset($_GET['edit'])) {
$edit_id = $_GET['edit'];
$stmt = $pdo->prepare("SELECT id, username, role, telegram_chat_id FROM users WHERE id = ?");
$stmt->execute([$edit_id]);
$user_to_edit = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user_to_edit) {
$edit_mode = true;
$edit_username = $user_to_edit['username'];
$edit_role = $user_to_edit['role'];
$edit_telegram_chat_id = $user_to_edit['telegram_chat_id'];
} else {
$error = "Usuario no encontrado.";
}
}
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Action: Create or Update User
if (isset($_POST['save_user'])) {
$username = $_POST['username'];
$role = $_POST['role'];
$telegram_chat_id = trim($_POST['telegram_chat_id']);
$is_edit = isset($_POST['edit_id']);
if (empty($username) || empty($role)) {
$error = "El nombre de usuario y el rol son obligatorios.";
} elseif (!empty($telegram_chat_id) && !is_numeric($telegram_chat_id)) {
$error = "El ID de Chat de Telegram debe ser un número.";
} else {
$chat_id_to_save = empty($telegram_chat_id) ? null : $telegram_chat_id;
try {
if ($is_edit) {
$edit_id = $_POST['edit_id'];
$details = 'Admin ' . $_SESSION['username'] . ' updated user: ' . $username . ' (ID: ' . $edit_id . ').';
if (!empty($_POST['password'])) {
$hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET username = ?, password = ?, role = ?, telegram_chat_id = ? WHERE id = ?");
$stmt->execute([$username, $hashedPassword, $role, $chat_id_to_save, $edit_id]);
$details .= ' Password was changed.';
} else {
$stmt = $pdo->prepare("UPDATE users SET username = ?, role = ?, telegram_chat_id = ? WHERE id = ?");
$stmt->execute([$username, $role, $chat_id_to_save, $edit_id]);
}
log_activity($_SESSION['user_id'], 'User Updated', $details);
header('Location: users.php?success=updated');
exit();
} else {
if (empty($_POST['password'])) {
$error = "La contraseña es obligatoria para nuevos usuarios.";
} else {
$hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password, role, telegram_chat_id) VALUES (?, ?, ?, ?)");
$stmt->execute([$username, $hashedPassword, $role, $chat_id_to_save]);
$new_user_id = $pdo->lastInsertId();
log_activity($_SESSION['user_id'], 'User Created', 'Admin ' . $_SESSION['username'] . ' created new user: ' . $username . ' (ID: ' . $new_user_id . ')');
header('Location: users.php?success=created');
exit();
}
}
} catch (PDOException $e) {
$error = ($e->errorInfo[1] == 1062) ? "El nombre de usuario ya existe." : "Error al guardar el usuario: " . $e->getMessage();
if ($is_edit) {
$edit_mode = true;
$edit_id = $_POST['edit_id'];
$edit_username = $username;
$edit_role = $role;
$edit_telegram_chat_id = $telegram_chat_id;
}
}
}
}
// ... (Otras acciones POST como eliminar, etc. se mantienen aquí)
}
// Fetch all users to display
$users = $pdo->query("SELECT id, username, role, created_at, telegram_chat_id FROM users ORDER BY username ASC")->fetchAll(PDO::FETCH_ASSOC);
require_once __DIR__ . '/../templates/header.php';
?>
<div class="container-fluid">
<h1 class="mt-4" data-translate="true">Administrar Usuarios</h1>
<?php if (isset($error)): ?><div class="alert alert-danger"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if (isset($_GET['success'])): /* ... Lógica de mensajes de éxito ... */ endif; ?>
<!-- Create/Edit User Form -->
<div class="card shadow-sm mb-4">
<div class="card-header">
<h5 class="mb-0" data-translate="true"><?= $edit_mode ? 'Editar Usuario' : 'Crear Nuevo Usuario' ?></h5>
</div>
<div class="card-body">
<form action="users.php" method="POST">
<?php if ($edit_mode): ?>
<input type="hidden" name="edit_id" value="<?= $edit_id ?>">
<?php endif; ?>
<div class="row align-items-end">
<div class="col-md-3 mb-3">
<label for="username" class="form-label" data-translate="true">Usuario</label>
<input type="text" class="form-control" id="username" name="username"
value="<?= htmlspecialchars($edit_username) ?>" required>
</div>
<div class="col-md-3 mb-3">
<label for="password" class="form-label">
<span data-translate="true">Contraseña</span> <?php if ($edit_mode): ?><small class="text-muted" data-translate="true">(no cambiar)</small><?php endif; ?>
</label>
<input type="password" class="form-control" id="password" name="password" <?= !$edit_mode ? 'required' : '' ?>>
</div>
<div class="col-md-2 mb-3">
<label for="role" class="form-label" data-translate="true">Rol</label>
<select class="form-select" id="role" name="role" required>
<option value="user" <?= ($edit_role === 'user') ? 'selected' : '' ?> data-translate="true">Usuario</option>
<option value="admin" <?= ($edit_role === 'admin') ? 'selected' : '' ?> data-translate="true">Admin</option>
</select>
</div>
<div class="col-md-2 mb-3">
<label for="telegram_chat_id" class="form-label" data-translate="true">ID Chat Telegram</label>
<input type="text" class="form-control" id="telegram_chat_id" name="telegram_chat_id"
value="<?= htmlspecialchars($edit_telegram_chat_id) ?>" placeholder="(Opcional)">
</div>
<div class="col-md-2 mb-3">
<button type="submit" name="save_user" class="btn btn-<?= $edit_mode ? 'success' : 'primary' ?> w-100" data-translate="true">
<?= $edit_mode ? 'Actualizar' : 'Crear' ?>
</button>
<?php if ($edit_mode): ?>
<a href="users.php" class="btn btn-outline-secondary w-100 mt-2" data-translate="true">Cancelar</a>
<?php endif; ?>
</div>
</div>
</form>
</div>
</div>
<!-- Users List -->
<div class="card shadow-sm">
<div class="card-header"><h5 data-translate="true">Lista de Usuarios</h5></div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th data-translate="true">ID</th>
<th data-translate="true">Usuario</th>
<th data-translate="true">Rol</th>
<th data-translate="true">ID Chat Telegram</th>
<th data-translate="true">Creado en</th>
<th class="text-center" data-translate="true">Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?= $user['id'] ?></td>
<td><?= htmlspecialchars($user['username']) ?></td>
<td><?= ucfirst($user['role']) ?></td>
<td>
<?php if (!empty($user['telegram_chat_id'])): ?>
<span class="badge bg-info text-dark"><?= htmlspecialchars($user['telegram_chat_id']) ?></span>
<?php else: ?>
<span class="badge bg-secondary" data-translate="true">No vinculado</span>
<?php endif; ?>
</td>
<td><?= date('d/m/Y H:i', strtotime($user['created_at'])) ?></td>
<td class="text-center">
<a href="?edit=<?= $user['id'] ?>#username" class="btn btn-sm btn-primary" title="Editar" data-translate-title="true">
<i class="bi bi-pencil"></i>
</a>
<!-- Otros botones de acción (modal, eliminar) -->
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modales y Scripts -->
<?php require_once __DIR__ . '/../templates/footer.php'; ?>