feat: Añadir sistema de mensajes de bienvenida con traducción para Discord
- Nueva tabla 'welcome_messages' en la base de datos - Panel web con página de configuración de bienvenida (/welcome) - Listar, crear, editar y eliminar mensajes por servidor - Vista previa del mensaje - Plantillas predefinidas - Bot Discord: - Nuevo intent 'members' para detectar nuevos usuarios - Evento on_member_join que envía mensaje de bienvenida - Botones de traducción en mensajes de bienvenida - Actualizada configuración de MySQL en docker-compose.yml - Añadido logging de debug para traducciones
This commit is contained in:
@@ -128,6 +128,7 @@ async def login(request: Request):
|
||||
if verify_admin(username, password):
|
||||
response = RedirectResponse(url="/dashboard", status_code=status.HTTP_303_SEE_OTHER)
|
||||
response.set_cookie(key="auth", value="ok", httponly=True)
|
||||
response.set_cookie(key="username", value=username, httponly=True)
|
||||
return response
|
||||
|
||||
return templates.TemplateResponse("login.html", {
|
||||
@@ -140,10 +141,12 @@ async def dashboard(request: Request):
|
||||
if request.cookies.get("auth") != "ok":
|
||||
return RedirectResponse(url="/login")
|
||||
|
||||
username = request.cookies.get("username", "")
|
||||
config = get_config()
|
||||
return templates.TemplateResponse("dashboard.html", {
|
||||
"request": request,
|
||||
"config": config
|
||||
"config": config,
|
||||
"username": username
|
||||
})
|
||||
|
||||
@app.get("/config")
|
||||
@@ -151,10 +154,12 @@ async def config_page(request: Request):
|
||||
if request.cookies.get("auth") != "ok":
|
||||
return RedirectResponse(url="/login")
|
||||
|
||||
username = request.cookies.get("username", "")
|
||||
config = get_config()
|
||||
return templates.TemplateResponse("config.html", {
|
||||
"request": request,
|
||||
"config": config
|
||||
"config": config,
|
||||
"username": username
|
||||
})
|
||||
|
||||
@app.get("/languages")
|
||||
@@ -248,6 +253,7 @@ async def update_language_flags(request: Request):
|
||||
async def logout():
|
||||
response = RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
|
||||
response.delete_cookie("auth")
|
||||
response.delete_cookie("username")
|
||||
return response
|
||||
|
||||
@app.get("/admins")
|
||||
@@ -316,6 +322,81 @@ async def update_admin_post(request: Request):
|
||||
|
||||
return RedirectResponse(url="/admins?success=1", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@app.get("/welcome")
|
||||
async def welcome_page(request: Request):
|
||||
if request.cookies.get("auth") != "ok":
|
||||
return RedirectResponse(url="/login")
|
||||
|
||||
from botdiscord.database import get_all_welcome_configs
|
||||
|
||||
configs = get_all_welcome_configs()
|
||||
config_dict = {c['guild_id']: c for c in configs} if configs else {}
|
||||
|
||||
selected_guild = request.query_params.get("guild")
|
||||
selected_cfg = None
|
||||
if selected_guild and selected_guild.isdigit():
|
||||
selected_cfg = config_dict.get(int(selected_guild))
|
||||
|
||||
success = request.query_params.get("success") == "1"
|
||||
error = request.query_params.get("error") == "1"
|
||||
|
||||
return templates.TemplateResponse("welcome.html", {
|
||||
"request": request,
|
||||
"configs": config_dict,
|
||||
"config": config_dict,
|
||||
"selected_guild": int(selected_guild) if selected_guild and selected_guild.isdigit() else None,
|
||||
"selected_cfg": selected_cfg,
|
||||
"success": success,
|
||||
"error": error,
|
||||
"new_form": request.query_params.get("new") == "1"
|
||||
})
|
||||
|
||||
@app.post("/welcome/save")
|
||||
async def save_welcome(request: Request):
|
||||
if request.cookies.get("auth") != "ok":
|
||||
raise HTTPException(status_code=401)
|
||||
|
||||
form = await request.form()
|
||||
guild_id = form.get("guild_id")
|
||||
channel_id = form.get("channel_id")
|
||||
message_content = form.get("message_content")
|
||||
enabled = form.get("enabled") == "1"
|
||||
|
||||
if not guild_id or not channel_id or not message_content:
|
||||
return RedirectResponse(url="/welcome?error=1", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
try:
|
||||
from botdiscord.database import save_welcome_message
|
||||
save_welcome_message(
|
||||
int(guild_id),
|
||||
int(channel_id),
|
||||
message_content,
|
||||
enabled
|
||||
)
|
||||
return RedirectResponse(url="/welcome?success=1", status_code=status.HTTP_303_SEE_OTHER)
|
||||
except Exception as e:
|
||||
print(f"Error saving welcome message: {e}")
|
||||
return RedirectResponse(url="/welcome?error=1", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@app.post("/welcome/delete")
|
||||
async def delete_welcome(request: Request):
|
||||
if request.cookies.get("auth") != "ok":
|
||||
raise HTTPException(status_code=401)
|
||||
|
||||
form = await request.form()
|
||||
guild_id = form.get("guild_id")
|
||||
|
||||
if guild_id:
|
||||
try:
|
||||
from botdiscord.database import delete_welcome_message
|
||||
delete_welcome_message(int(guild_id))
|
||||
return RedirectResponse(url="/welcome?success=1", status_code=status.HTTP_303_SEE_OTHER)
|
||||
except Exception as e:
|
||||
print(f"Error deleting welcome message: {e}")
|
||||
return RedirectResponse(url="/welcome?error=1", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
return RedirectResponse(url="/welcome", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
web_config = get_web_config()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% set lang = request.cookies.get('panel_lang', 'es') %}
|
||||
{% set is_admin = username == 'nickpons666' %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ lang }}">
|
||||
<head>
|
||||
@@ -55,6 +56,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card text-white bg-info mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="bi bi-emoji-smile"></i> {{ "Bienvenida" | translate(lang) }}</h5>
|
||||
<p class="card-text">{{ "Configurar mensaje de bienvenida" | translate(lang) }}</p>
|
||||
<a href="/welcome" class="btn btn-light btn-sm">{{ "Configurar" | translate(lang) }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card text-white bg-dark mb-3">
|
||||
<div class="card-body">
|
||||
@@ -82,7 +95,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "LibreTranslate URL" | translate(lang) }}</th>
|
||||
<td>{{ config.libretranslate.url if config.libretranslate.url else '❌ No configurado' | translate(lang) }}</td>
|
||||
<td>{{ config.libretranslate.url if is_admin else '********' if config.libretranslate.url else '❌ No configurado' | translate(lang) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Idiomas activos" | translate(lang) }}</th>
|
||||
@@ -95,20 +108,20 @@
|
||||
{% if config.database.type == 'mysql' %}
|
||||
<tr>
|
||||
<th>{{ "Host MySQL" | translate(lang) }}</th>
|
||||
<td>{{ config.database.host }}:{{ config.database.port }}</td>
|
||||
<td>{{ config.database.host ~ ':' ~ config.database.port if is_admin else '********' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Base de Datos" | translate(lang) }}</th>
|
||||
<td>{{ config.database.name }}</td>
|
||||
<td>{{ config.database.name if is_admin else '********' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ "Usuario MySQL" | translate(lang) }}</th>
|
||||
<td>{{ config.database.user }}</td>
|
||||
<td>{{ config.database.user if is_admin else '********' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th>{{ "Ruta SQLite" | translate(lang) }}</th>
|
||||
<td>{{ config.database.path }}</td>
|
||||
<td>{{ config.database.path if is_admin else '********' }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
258
panel/templates/welcome.html
Normal file
258
panel/templates/welcome.html
Normal file
@@ -0,0 +1,258 @@
|
||||
{% set lang = request.cookies.get('panel_lang', 'es') %}
|
||||
{% set is_admin = username == 'nickpons666' %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ lang }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ "Mensaje de Bienvenida - Bots de Traducción" | translate(lang) }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/dashboard">
|
||||
<i class="bi bi-translate"></i> {{ "Bots de Traducción" | translate(lang) }}
|
||||
</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="dropdown me-3">
|
||||
<button class="btn btn-outline-light btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-translate"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item {{ 'active' if lang == 'es' }}" href="/set-lang/es">Español</a></li>
|
||||
<li><a class="dropdown-item {{ 'active' if lang == 'en' }}" href="/set-lang/en">English</a></li>
|
||||
<li><a class="dropdown-item {{ 'active' if lang == 'pt' }}" href="/set-lang/pt">Português</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="/dashboard" class="btn btn-outline-light btn-sm me-2">{{ "Dashboard" | translate(lang) }}</a>
|
||||
<a href="/logout" class="btn btn-outline-light btn-sm">{{ "Cerrar Sesión" | translate(lang) }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5">
|
||||
<h2 class="mb-4">👋 {{ "Mensajes de Bienvenida" | translate(lang) }}</h2>
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle"></i> {{ "Configuración guardada correctamente." | translate(lang) }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i> {{ "Error al guardar. Verifica los datos." | translate(lang) }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
{% if configs %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-list"></i> {{ "Servidores Configurados" | translate(lang) }}</h5>
|
||||
<button class="btn btn-sm btn-primary" onclick="nuevoMensaje()">
|
||||
<i class="bi bi-plus"></i> {{ "Nuevo" | translate(lang) }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for guild_id, cfg in configs.items() %}
|
||||
<div class="list-group-item {{ 'active' if selected_guild == guild_id else '' }}"
|
||||
style="cursor: pointer;"
|
||||
onclick="seleccionarGuild('{{ guild_id }}')">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>Guild ID:</strong> {{ guild_id }}<br>
|
||||
<small><i class="bi bi-chat-dots"></i> Channel: {{ cfg['channel_id'] }}</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
{% if cfg['enabled'] %}
|
||||
<span class="badge bg-success">{{ "Activo" | translate(lang) }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ "Inactivo" | translate(lang) }}</span>
|
||||
{% endif %}
|
||||
<form method="POST" action="/welcome/delete" class="d-inline"
|
||||
onsubmit="return confirm('¿Eliminar esta configuración?')">
|
||||
<input type="hidden" name="guild_id" value="{{ guild_id }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">{{ cfg['message_content'][:50] if cfg['message_content'] else '' }}...</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-inbox" style="font-size: 3rem; color: #ccc;"></i>
|
||||
<p class="mt-3 text-muted">{{ "No hay mensajes de bienvenida configurados." | translate(lang) }}</p>
|
||||
<button class="btn btn-primary" onclick="nuevoMensaje()">
|
||||
<i class="bi bi-plus"></i> {{ "Crear primer mensaje" | translate(lang) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card" id="formularioCard" style="{{ 'display: none;' if not selected_guild and not new_form else '' }}">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-discord"></i>
|
||||
{{ "Editar Mensaje" | translate(lang) if selected_guild else "Nuevo Mensaje" | translate(lang) }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/welcome/save">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ "Servidor (Guild ID)" | translate(lang) }}</label>
|
||||
<input type="number" class="form-control" name="guild_id" id="guild_id"
|
||||
value="{{ selected_cfg['guild_id'] if selected_cfg else '' }}" required
|
||||
placeholder="{{ 'Ej: 123456789012345678' | translate(lang) }}">
|
||||
<div class="form-text">{{ "Ingresa el ID del servidor de Discord" | translate(lang) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ "Canal de Bienvenida (Channel ID)" | translate(lang) }}</label>
|
||||
<input type="number" class="form-control" name="channel_id" id="channel_id"
|
||||
value="{{ selected_cfg['channel_id'] if selected_cfg else '' }}" required
|
||||
placeholder="{{ 'Ej: 123456789012345678' | translate(lang) }}">
|
||||
<div class="form-text">{{ "ID del canal donde se enviarán los mensajes de bienvenida" | translate(lang) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ "Mensaje de Bienvenida" | translate(lang) }}</label>
|
||||
<textarea class="form-control" name="message_content" id="message_content" rows="4" required>{{ selected_cfg['message_content'] if selected_cfg else '' }}</textarea>
|
||||
<div class="form-text">
|
||||
{{ "Placeholders:" | translate(lang) }}
|
||||
<code>{user_mention}</code>, <code>{username}</code>, <code>{server_name}</code>, <code>{member_count}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="enabled" id="enabled" value="1"
|
||||
{% if selected_cfg and selected_cfg['enabled'] %}checked{% endif %}>
|
||||
<label class="form-check-label" for="enabled">
|
||||
{{ "Habilitar mensaje de bienvenida" | translate(lang) }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> {{ "Guardar" | translate(lang) }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="ocultarFormulario()">
|
||||
{{ "Cancelar" | translate(lang) }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-7">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-eye"></i> {{ "Vista Previa" | translate(lang) }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="discord-preview p-3 rounded" style="background-color: #36393f; color: #dcddde;">
|
||||
<div id="preview_content">
|
||||
{% if selected_cfg and selected_cfg['message_content'] %}
|
||||
{{ selected_cfg['message_content'] }}
|
||||
{% else %}
|
||||
¡Bienvenido {user_mention} a {server_name}! Ahora somos {member_count} miembros.
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr style="border-color: #40444b; margin: 10px 0;">
|
||||
<small style="color: #b9bbbe;">¿Traducir este mensaje?</small>
|
||||
<div class="d-flex flex-wrap gap-1 mt-2">
|
||||
<button class="btn btn-sm btn-secondary" disabled>🇬🇧 EN</button>
|
||||
<button class="btn btn-sm btn-secondary" disabled>🇪🇸 ES</button>
|
||||
<button class="btn btn-sm btn-secondary" disabled>🇫🇷 FR</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">
|
||||
<i class="bi bi-info-circle"></i> {{ "Vista previa en Discord" | translate(lang) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-lightbulb"></i> {{ "Plantillas" | translate(lang) }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-secondary text-start" onclick="setExample(1)">
|
||||
<i class="bi bi-star"></i> <strong>¡Bienvenido {user_mention} a {server_name}! 🎉</strong>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary text-start" onclick="setExample(2)">
|
||||
<i class="bi bi-heart"></i> <strong>¡Hola {username}!</strong> Nos alegra que te hayas unido. {server_name} ahora tiene {member_count} miembros. ¡Disfruta!
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary text-start" onclick="setExample(3)">
|
||||
<i class="bi bi-info-circle"></i> <strong>👋 ¡Bienvenido/a {user_mention} a {server_name}!</strong><br>
|
||||
<small>Actualmente somos {member_count} miembros.</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const examples = {
|
||||
1: '¡Bienvenido {user_mention} a {server_name}! 🎉',
|
||||
2: '¡Hola {username}! Nos alegra que te hayas unido. {server_name} ahora tiene {member_count} miembros. ¡Disfruta tu estancia! 😊',
|
||||
3: '👋 ¡Bienvenido/a {user_mention} a **{server_name}**!\n\nActualmente somos {member_count} miembros. Lee las reglas y presentate en #presentaciones.'
|
||||
};
|
||||
|
||||
function setExample(num) {
|
||||
document.getElementById('message_content').value = examples[num];
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const content = document.getElementById('message_content').value || '¡Bienvenido {user_mention} a {server_name}! 🎉';
|
||||
document.getElementById('preview_content').innerText = content;
|
||||
}
|
||||
|
||||
document.getElementById('message_content').addEventListener('input', updatePreview);
|
||||
|
||||
function seleccionarGuild(guildId) {
|
||||
window.location.href = '/welcome?guild=' + guildId;
|
||||
}
|
||||
|
||||
function nuevoMensaje() {
|
||||
document.getElementById('formularioCard').style.display = 'block';
|
||||
document.getElementById('guild_id').value = '';
|
||||
document.getElementById('channel_id').value = '';
|
||||
document.getElementById('message_content').value = '';
|
||||
document.getElementById('enabled').checked = true;
|
||||
document.getElementById('preview_content').innerText = '¡Bienvenido {user_mention} a {server_name}! 🎉';
|
||||
}
|
||||
|
||||
function ocultarFormulario() {
|
||||
document.getElementById('formularioCard').style.display = 'none';
|
||||
}
|
||||
|
||||
{% if selected_guild or new_form %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('formularioCard').style.display = 'block';
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user