Fix Discord channel activation system

- Fix MySQL boolean conversion in toggle_channel_status
- Improve cache management with 5-second timeout
- Add bulk channel selection and toggle functionality
- Fix Jinja2 template syntax errors
- Add comprehensive debugging for channel status queries
- Implement real-time channel activation without container restart
This commit is contained in:
2026-03-20 06:41:35 -06:00
parent 100fef5c90
commit 39f531a331
6 changed files with 743 additions and 9 deletions

View File

@@ -1,5 +1,7 @@
import os import os
import sys import sys
import signal
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import discord import discord
@@ -9,7 +11,7 @@ import re
import html import html
from botdiscord.config import get_discord_token, load_config, get_languages from botdiscord.config import get_discord_token, load_config, get_languages
from botdiscord.database import init_db, get_active_languages, get_bot_languages, save_message, get_welcome_message from botdiscord.database import init_db, get_active_languages, get_bot_languages, save_message, get_welcome_message, is_channel_enabled
from botdiscord.ui import PersistentTranslationView, ConfigView, WelcomeTranslationView, TranslationButton from botdiscord.ui import PersistentTranslationView, ConfigView, WelcomeTranslationView, TranslationButton
from botdiscord.translate import get_reverse_mapping, load_lang_mappings, get_name_to_code, get_flag_mapping from botdiscord.translate import get_reverse_mapping, load_lang_mappings, get_name_to_code, get_flag_mapping
@@ -115,6 +117,14 @@ def get_active_langs_for_guild(guild_id):
async def on_message(message): async def on_message(message):
if message.author.bot: return if message.author.bot: return
# Verificar si el canal está habilitado para traducción
channel_enabled = is_channel_enabled(message.channel.id)
print(f"[Bot] Mensaje en canal {message.channel.id} ({message.channel.name}) - Habilitado: {channel_enabled} - Timestamp: {time.strftime('%H:%M:%S')}")
if not channel_enabled:
print(f"[Bot] Ignorando mensaje en canal desactivado: {message.channel.name}")
return
text_content = message.content.strip() text_content = message.content.strip()
if not text_content: return if not text_content: return

View File

@@ -190,6 +190,19 @@ def init_db():
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''')
cursor.execute('''CREATE TABLE IF NOT EXISTS discord_servers
(server_id BIGINT NOT NULL PRIMARY KEY,
server_name VARCHAR(255) NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''')
cursor.execute('''CREATE TABLE IF NOT EXISTS discord_channels
(channel_id BIGINT NOT NULL PRIMARY KEY,
channel_name VARCHAR(255) NOT NULL,
server_id BIGINT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES discord_servers(server_id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''')
conn.commit() conn.commit()
cursor.close() cursor.close()
else: else:
@@ -243,6 +256,20 @@ def init_db():
channel_id INTEGER NOT NULL, channel_id INTEGER NOT NULL,
message_content TEXT NOT NULL, message_content TEXT NOT NULL,
enabled INTEGER DEFAULT 1)''') enabled INTEGER DEFAULT 1)''')
c.execute('''CREATE TABLE IF NOT EXISTS discord_servers
(server_id INTEGER PRIMARY KEY,
server_name TEXT NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
c.execute('''CREATE TABLE IF NOT EXISTS discord_channels
(channel_id INTEGER PRIMARY KEY,
channel_name TEXT NOT NULL,
server_id INTEGER NOT NULL,
is_active INTEGER DEFAULT 1,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES discord_servers(server_id))''')
conn.commit() conn.commit()
conn.close() conn.close()
@@ -716,3 +743,232 @@ def delete_welcome_message(guild_id: int):
c.execute("DELETE FROM welcome_messages WHERE guild_id = ?", (guild_id,)) c.execute("DELETE FROM welcome_messages WHERE guild_id = ?", (guild_id,))
conn.commit() conn.commit()
conn.close() conn.close()
# Funciones para gestión de servidores y canales de Discord
def sync_discord_servers(servers: list):
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
for server in servers:
server_id = server.get('id')
server_name = server.get('name', '')
cursor.execute("""INSERT INTO discord_servers (server_id, server_name)
VALUES (%s, %s)
ON DUPLICATE KEY UPDATE server_name = %s""",
(server_id, server_name, server_name))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
for server in servers:
server_id = server.get('id')
server_name = server.get('name', '')
c.execute("""INSERT OR REPLACE INTO discord_servers (server_id, server_name)
VALUES (?, ?)""", (server_id, server_name))
conn.commit()
conn.close()
def get_discord_servers() -> list:
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT server_id, server_name, added_at FROM discord_servers ORDER BY server_name")
rows = cursor.fetchall()
cursor.close()
return rows
else:
conn = get_connection()
c = conn.cursor()
c.execute("SELECT server_id, server_name, added_at FROM discord_servers ORDER BY server_name")
rows = [{"server_id": r[0], "server_name": r[1], "added_at": r[2]} for r in c.fetchall()]
conn.close()
return rows
def sync_discord_channels(server_id: int, channels: list):
db_type = get_db_type()
try:
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
for channel in channels:
channel_id = channel.get('id')
channel_name = channel.get('name', '')
cursor.execute("""INSERT INTO discord_channels (channel_id, channel_name, server_id)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE channel_name = %s, server_id = %s""",
(channel_id, channel_name, server_id, channel_name, server_id))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
for channel in channels:
channel_id = channel.get('id')
channel_name = channel.get('name', '')
c.execute("""INSERT OR REPLACE INTO discord_channels (channel_id, channel_name, server_id)
VALUES (?, ?, ?)""", (channel_id, channel_name, server_id))
conn.commit()
conn.close()
# Limpiar todo el cache de canales después de sincronizar
clear_channel_cache()
print(f"[DB] Canales sincronizados para servidor {server_id}, cache limpiado")
except Exception as e:
print(f"[DB] Error sincronizando canales del servidor {server_id}: {e}")
raise
def get_discord_channels(server_id: int = None) -> list:
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor(dictionary=True)
if server_id:
cursor.execute("SELECT channel_id, channel_name, server_id, is_active, added_at FROM discord_channels WHERE server_id = %s ORDER BY channel_name", (server_id,))
else:
cursor.execute("SELECT channel_id, channel_name, server_id, is_active, added_at FROM discord_channels ORDER BY server_id, channel_name")
rows = cursor.fetchall()
cursor.close()
return rows
else:
conn = get_connection()
c = conn.cursor()
if server_id:
c.execute("SELECT channel_id, channel_name, server_id, is_active, added_at FROM discord_channels WHERE server_id = ? ORDER BY channel_name", (server_id,))
else:
c.execute("SELECT channel_id, channel_name, server_id, is_active, added_at FROM discord_channels ORDER BY server_id, channel_name")
rows = [{"channel_id": r[0], "channel_name": r[1], "server_id": r[2], "is_active": bool(r[3]), "added_at": r[4]} for r in c.fetchall()]
conn.close()
return rows
def toggle_channel_status(channel_id: int, is_active: bool):
db_type = get_db_type()
try:
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
cursor.execute("UPDATE discord_channels SET is_active = %s WHERE channel_id = %s",
(1 if is_active else 0, channel_id))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
c.execute("UPDATE discord_channels SET is_active = ? WHERE channel_id = ?",
(1 if is_active else 0, channel_id))
conn.commit()
conn.close()
# Limpiar el cache para forzar re-lectura
clear_channel_cache(channel_id)
print(f"[DB] Canal {channel_id} actualizado a {is_active}, cache limpiado")
except Exception as e:
print(f"[DB] Error actualizando canal {channel_id}: {e}")
raise
# Cache simple para estados de canales (evita consultas excesivas)
_channel_status_cache = {}
_cache_timeout = 5 # segundos
_last_cache_update = {}
def is_channel_enabled(channel_id: int) -> bool:
global _channel_status_cache, _last_cache_update
db_type = get_db_type()
current_time = time.time()
# Verificar si tenemos un cache válido
if (channel_id in _channel_status_cache and
channel_id in _last_cache_update and
current_time - _last_cache_update[channel_id] < _cache_timeout):
result = _channel_status_cache[channel_id]
print(f"[DB Cache] Canal {channel_id} estado (cache): {result}")
return result
try:
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT is_active FROM discord_channels WHERE channel_id = %s", (channel_id,))
row = cursor.fetchone()
cursor.close()
print(f"[DB] MySQL Query: SELECT is_active FROM discord_channels WHERE channel_id = {channel_id}")
print(f"[DB] MySQL Row: {row}")
result = bool(row[0]) if row else False
else:
conn = get_connection()
c = conn.cursor()
c.execute("SELECT is_active FROM discord_channels WHERE channel_id = ?", (channel_id,))
row = c.fetchone()
conn.close()
print(f"[DB] SQLite Query: SELECT is_active FROM discord_channels WHERE channel_id = {channel_id}")
print(f"[DB] SQLite Row: {row}")
result = bool(row[0]) if row else False
# Actualizar cache
_channel_status_cache[channel_id] = result
_last_cache_update[channel_id] = current_time
print(f"[DB] Canal {channel_id} estado (DB): {result} - Timestamp: {time.strftime('%H:%M:%S')}")
return result
except Exception as e:
print(f"[DB] Error verificando canal {channel_id}: {e}")
# En caso de error, devolver False por seguridad
return False
def clear_channel_cache(channel_id: int = None):
"""Limpia el cache de un canal específico o de todos"""
global _channel_status_cache, _last_cache_update
if channel_id:
_channel_status_cache.pop(channel_id, None)
_last_cache_update.pop(channel_id, None)
print(f"[DB] Cache limpiado para canal {channel_id}")
# Forzar expiración inmediata poniendo un timestamp antiguo
_last_cache_update[channel_id] = 0
else:
_channel_status_cache.clear()
_last_cache_update.clear()
print(f"[DB] Todo el cache de canales limpiado")
def delete_server_channels(server_id: int):
"""Elimina todos los canales de un servidor específico"""
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM discord_channels WHERE server_id = %s", (server_id,))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
c.execute("DELETE FROM discord_channels WHERE server_id = ?", (server_id,))
conn.commit()
conn.close()
def delete_discord_server(server_id: int):
"""Elimina un servidor y todos sus canales"""
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM discord_servers WHERE server_id = %s", (server_id,))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
c.execute("DELETE FROM discord_servers WHERE server_id = ?", (server_id,))
conn.commit()
conn.close()

View File

@@ -8,9 +8,14 @@ from fastapi.templating import Jinja2Templates
from fastapi import Request from fastapi import Request
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from pydantic import BaseModel from pydantic import BaseModel
from dotenv import load_dotenv
from passlib.hash import pbkdf2_sha256 as hasher from passlib.hash import pbkdf2_sha256 as hasher
from botdiscord.config import load_config, get_web_config, get_libretranslate_url, get_db_type from botdiscord.config import load_config, get_web_config, get_libretranslate_url, get_db_type
# Asegurar que las variables de entorno se carguen correctamente
load_dotenv()
load_config() # Cargamos configuración inmediatamente load_config() # Cargamos configuración inmediatamente
from botdiscord.database import ( from botdiscord.database import (
@@ -397,6 +402,188 @@ async def delete_welcome(request: Request):
return RedirectResponse(url="/welcome", status_code=status.HTTP_303_SEE_OTHER) return RedirectResponse(url="/welcome", status_code=status.HTTP_303_SEE_OTHER)
@app.get("/discord-channels")
async def discord_channels_page(request: Request):
if request.cookies.get("auth") != "ok":
return RedirectResponse(url="/login")
from botdiscord.database import get_discord_servers, get_discord_channels
servers = get_discord_servers()
channels = get_discord_channels()
# Agrupar canales por servidor
servers_with_channels = {}
for server in servers:
server_id = server['server_id']
servers_with_channels[server_id] = {
'server_info': server,
'channels': [ch for ch in channels if ch['server_id'] == server_id]
}
success = request.query_params.get("success") == "1"
error = request.query_params.get("error") == "1"
synced = request.query_params.get("synced") == "1"
return templates.TemplateResponse("discord_channels.html", {
"request": request,
"servers_with_channels": servers_with_channels,
"success": success,
"error": error,
"synced": synced
})
@app.post("/discord-channels/sync")
async def sync_discord_servers(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
try:
print("[Panel] Iniciando sincronización de Discord...")
# Diagnóstico de variables de entorno
print("[Panel] Variables de entorno disponibles:")
discord_env_vars = {k: v for k, v in os.environ.items() if 'DISCORD' in k or 'TOKEN' in k}
for key, value in discord_env_vars.items():
print(f" {key}: {'***' if 'TOKEN' in key else value}")
# Importar y usar el módulo de sincronización
from botdiscord.server_sync import sync_discord_servers_from_api
result = await sync_discord_servers_from_api()
print(f"[Panel] Resultado de sincronización: {result}")
if result:
return RedirectResponse(url="/discord-channels?synced=1", status_code=status.HTTP_303_SEE_OTHER)
else:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"[Panel] Error en sincronización: {e}")
import traceback
traceback.print_exc()
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/discord-channels/toggle")
async def toggle_discord_channel(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
form = await request.form()
channel_id = form.get("channel_id")
is_active = form.get("is_active") == "true"
if not channel_id:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
try:
from botdiscord.database import toggle_channel_status
toggle_channel_status(int(channel_id), is_active)
return RedirectResponse(url="/discord-channels?success=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"Error toggling channel: {e}")
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/discord-channels/delete-server")
async def delete_discord_server(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
form = await request.form()
server_id = form.get("server_id")
if not server_id:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
try:
from botdiscord.database import delete_discord_server
delete_discord_server(int(server_id))
return RedirectResponse(url="/discord-channels?success=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"Error deleting server: {e}")
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/discord-channels/reload-bot")
async def reload_discord_bot(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
try:
print("[Panel] Enviando señal de recarga al bot de Discord...")
# Importar y ejecutar la función de recarga
from botdiscord.bot_reload import reload_bot_config
result = await reload_bot_config()
if result:
return RedirectResponse(url="/discord-channels?success=1", status_code=status.HTTP_303_SEE_OTHER)
else:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"[Panel] Error recargando bot: {e}")
import traceback
traceback.print_exc()
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/discord-channels/bulk-toggle")
async def bulk_toggle_discord_channels(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
form = await request.form()
channel_ids = form.getlist("channel_ids")
bulk_action = form.get("bulk_action")
if not channel_ids or not bulk_action:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
try:
from botdiscord.database import toggle_channel_status
is_active = (bulk_action == "activate")
success_count = 0
for channel_id_str in channel_ids:
try:
channel_id = int(channel_id_str)
toggle_channel_status(channel_id, is_active)
success_count += 1
except ValueError:
continue
print(f"[Panel] Bulk toggle: {success_count} canales {'activados' if is_active else 'desactivados'}")
return RedirectResponse(url="/discord-channels?success=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"[Panel] Error en bulk toggle: {e}")
import traceback
traceback.print_exc()
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.get("/diagnosis")
async def diagnosis_page(request: Request):
if request.cookies.get("auth") != "ok":
return RedirectResponse(url="/login")
# Obtener variables de entorno relevantes
env_vars = {
'DISCORD_TOKEN': os.getenv('DISCORD_TOKEN'),
'TELEGRAM_TOKEN': os.getenv('TELEGRAM_TOKEN'),
'LIBRETRANSLATE_URL': os.getenv('LIBRETRANSLATE_URL'),
'DB_TYPE': os.getenv('DB_TYPE'),
'DB_HOST': os.getenv('DB_HOST'),
'DB_PORT': os.getenv('DB_PORT'),
'DB_NAME': os.getenv('DB_NAME'),
'DB_USER': os.getenv('DB_USER')
}
# Obtener configuración cargada
config = get_config()
return templates.TemplateResponse("diagnosis.html", {
"request": request,
"env_vars": env_vars,
"config": config
})
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
web_config = get_web_config() web_config = get_web_config()

View File

@@ -1,4 +1,5 @@
{% set lang = request.cookies.get('panel_lang', 'es') %} {% set lang = request.cookies.get('panel_lang', 'es') %}
{% set is_admin = username == 'nickpons666' %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ lang }}"> <html lang="{{ lang }}">
<head> <head>
@@ -48,17 +49,17 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ "Token de Discord" | translate(lang) }}</label> <label class="form-label">{{ "Token de Discord" | translate(lang) }}</label>
<input type="text" class="form-control" <input type="text" class="form-control"
value="{{ config.discord.token }}" readonly> value="{{ config.discord.token if is_admin else '****************************************' }}" readonly>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ "Token de Telegram" | translate(lang) }}</label> <label class="form-label">{{ "Token de Telegram" | translate(lang) }}</label>
<input type="text" class="form-control" <input type="text" class="form-control"
value="{{ config.telegram.token }}" readonly> value="{{ config.telegram.token if is_admin else '****************************************' }}" readonly>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ "URL de LibreTranslate" | translate(lang) }}</label> <label class="form-label">{{ "URL de LibreTranslate" | translate(lang) }}</label>
<input type="text" class="form-control" <input type="text" class="form-control"
value="{{ config.libretranslate.url }}" readonly> value="{{ config.libretranslate.url if is_admin else '****************************************' }}" readonly>
</div> </div>
</div> </div>
</div> </div>
@@ -107,28 +108,28 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ "Host MySQL" | translate(lang) }}</label> <label class="form-label">{{ "Host MySQL" | translate(lang) }}</label>
<input type="text" class="form-control" <input type="text" class="form-control"
value="{{ config.database.host }}" readonly> value="{{ config.database.host if is_admin else '****************************************' }}" readonly>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ "Puerto MySQL" | translate(lang) }}</label> <label class="form-label">{{ "Puerto MySQL" | translate(lang) }}</label>
<input type="number" class="form-control" <input type="number" class="form-control"
value="{{ config.database.port }}" readonly> value="{{ config.database.port if is_admin else '****************************************' }}" readonly>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ "Usuario MySQL" | translate(lang) }}</label> <label class="form-label">{{ "Usuario MySQL" | translate(lang) }}</label>
<input type="text" class="form-control" <input type="text" class="form-control"
value="{{ config.database.user }}" readonly> value="{{ config.database.user if is_admin else '****************************************' }}" readonly>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ "Nombre de Base de Datos" | translate(lang) }}</label> <label class="form-label">{{ "Nombre de Base de Datos" | translate(lang) }}</label>
<input type="text" class="form-control" <input type="text" class="form-control"
value="{{ config.database.name }}" readonly> value="{{ config.database.name if is_admin else '****************************************' }}" readonly>
</div> </div>
{% else %} {% else %}
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ "Ruta de la base de datos" | translate(lang) }}</label> <label class="form-label">{{ "Ruta de la base de datos" | translate(lang) }}</label>
<input type="text" class="form-control" <input type="text" class="form-control"
value="{{ config.database.path }}" readonly> value="{{ config.database.path if is_admin else '****************************************' }}" readonly>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -77,6 +77,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4">
<div class="card text-white bg-warning mb-3">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-hash"></i> {{ "Canales de Discord" | translate(lang) }}</h5>
<p class="card-text">{{ "Administrar canales habilitados para traducción" | translate(lang) }}</p>
<a href="/discord-channels" class="btn btn-dark btn-sm">{{ "Administrar" | translate(lang) }}</a>
</div>
</div>
</div>
</div> </div>
<div class="card mt-4"> <div class="card mt-4">

View File

@@ -0,0 +1,270 @@
{% set lang = request.cookies.get('panel_lang', 'es') %}
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ "Canales de Discord - 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">
<style>
.channel-item {
border-left: 4px solid #6f42c1;
transition: all 0.3s ease;
}
.channel-item:hover {
border-left-color: #563d7c;
background-color: #f8f9fa;
}
.channel-inactive {
border-left-color: #dc3545;
opacity: 0.7;
}
.server-card {
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 20px;
}
.server-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
border-radius: 8px 8px 0 0;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-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: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #28a745;
}
input:checked + .slider:before {
transform: translateX(26px);
}
</style>
</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">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>📱 {{ "Canales de Discord" | translate(lang) }}</h2>
<div>
<a href="/diagnosis" class="btn btn-outline-secondary me-2">
<i class="bi bi-bug"></i> {{ "Diagnóstico" | translate(lang) }}
</a>
<form method="post" action="/discord-channels/sync" class="d-inline">
<button type="submit" class="btn btn-primary" onclick="return confirm('{{ '¿Sincronizar servidores y canales desde Discord?' | translate(lang) }}')">
<i class="bi bi-arrow-repeat"></i> {{ "Sincronizar" | translate(lang) }}
</button>
</form>
</div>
</div>
{% if success %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle"></i> {{ "Configuración actualizada correctamente." | translate(lang) }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if synced %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle"></i> {{ "Servidores y canales sincronizados desde Discord." | 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-circle"></i> {{ "Error al realizar la operación. Verifica la configuración del bot." | translate(lang) }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if servers_with_channels|length == 0 %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> {{ "No hay servidores sincronizados. Usa el botón 'Sincronizar' para obtener los servidores y canales de Discord." | translate(lang) }}
</div>
{% else %}
{% for server_id, server_data in servers_with_channels.items() %}
<div class="card mb-4">
<div class="card-header bg-dark text-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-server"></i> {{ server_data.server_info.server_name }}
<small class="text-muted">({{ server_data.server_info.server_id }})</small>
</h5>
<form method="post" action="/discord-channels/delete-server" class="d-inline">
<input type="hidden" name="server_id" value="{{ server_id }}">
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('{{ '¿Eliminar este servidor y todos sus canales?' | translate(lang) }}')">
<i class="bi bi-trash"></i> {{ "Eliminar" | translate(lang) }}
</button>
</form>
</div>
</div>
<div class="card-body">
{% if server_data.channels|length == 0 %}
<p class="text-muted">{{ "No hay canales en este servidor." | translate(lang) }}</p>
{% else %}
<form method="post" action="/discord-channels/bulk-toggle">
<input type="hidden" name="server_id" value="{{ server_id }}">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th width="50px">
<input type="checkbox" class="form-check-input" id="selectAll_{{ server_id }}"
onchange="toggleAllChannels({{ server_id }})">
</th>
<th>{{ "Canal" | translate(lang) }}</th>
<th>{{ "ID" | translate(lang) }}</th>
<th width="100px">{{ "Estado" | translate(lang) }}</th>
<th width="120px">{{ "Acciones" | translate(lang) }}</th>
</tr>
</thead>
<tbody>
{% for channel in server_data.channels %}
<tr>
<td>
<input type="checkbox" class="form-check-input channel-checkbox"
name="channel_ids" value="{{ channel.channel_id }}"
data-server="{{ server_id }}">
</td>
<td>
<i class="bi bi-hash"></i> {{ channel.channel_name }}
</td>
<td><code>{{ channel.channel_id }}</code></td>
<td>
{% if channel.is_active %}
<span class="badge bg-success">{{ "Activo" | translate(lang) }}</span>
{% else %}
<span class="badge bg-secondary">{{ "Inactivo" | translate(lang) }}</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<form method="post" action="/discord-channels/toggle" class="d-inline">
<input type="hidden" name="channel_id" value="{{ channel.channel_id }}">
<input type="hidden" name="is_active" value="{{ 'false' if channel.is_active else 'true' }}">
<button type="submit" class="btn {% if channel.is_active %}btn-warning{% else %}btn-success{% endif %} btn-sm"
title="{{ 'Desactivar' if channel.is_active else 'Activar' | translate(lang) }}">
<i class="bi bi-{% if channel.is_active %}pause{% else %}play{% endif %}"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
<button type="submit" name="bulk_action" value="activate"
class="btn btn-success btn-sm me-2" disabled>
<i class="bi bi-play"></i> {{ "Activar seleccionados" | translate(lang) }}
</button>
<button type="submit" name="bulk_action" value="deactivate"
class="btn btn-warning btn-sm" disabled>
<i class="bi bi-pause"></i> {{ "Desactivar seleccionados" | translate(lang) }}
</button>
</div>
</form>
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function toggleAllChannels(serverId) {
const selectAll = document.getElementById('selectAll_' + serverId);
const checkboxes = document.querySelectorAll('.channel-checkbox[data-server="' + serverId + '"]');
const bulkButtons = selectAll.closest('form').querySelectorAll('button[type="submit"][name="bulk_action"]');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll.checked;
});
// Habilitar/deshabilitar botones bulk
const anyChecked = document.querySelectorAll('.channel-checkbox:checked').length > 0;
bulkButtons.forEach(btn => btn.disabled = !anyChecked);
}
function updateBulkButtons() {
const anyChecked = document.querySelectorAll('.channel-checkbox:checked').length > 0;
document.querySelectorAll('button[type="submit"][name="bulk_action"]').forEach(btn => {
btn.disabled = !anyChecked;
});
// Actualizar checkboxes de "seleccionar todo"
document.querySelectorAll('[id^="selectAll_"]').forEach(selectAll => {
const serverId = selectAll.id.replace('selectAll_', '');
const checkboxes = document.querySelectorAll('.channel-checkbox[data-server="' + serverId + '"]');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
const anyChecked = Array.from(checkboxes).some(cb => cb.checked);
selectAll.checked = allChecked;
selectAll.indeterminate = anyChecked && !allChecked;
});
}
// Agregar event listeners a todos los checkboxes
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.channel-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBulkButtons);
});
});
</script>
</body>
</html>