diff --git a/botdiscord/bot.py b/botdiscord/bot.py index d3d8afa..9e6ebc2 100644 --- a/botdiscord/bot.py +++ b/botdiscord/bot.py @@ -1,5 +1,7 @@ import os import sys +import signal +import time sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import discord @@ -9,7 +11,7 @@ import re import html 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.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): 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() if not text_content: return diff --git a/botdiscord/database.py b/botdiscord/database.py index f6e1b96..14b7f2f 100644 --- a/botdiscord/database.py +++ b/botdiscord/database.py @@ -190,6 +190,19 @@ def init_db(): created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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() cursor.close() else: @@ -243,6 +256,20 @@ def init_db(): channel_id INTEGER NOT NULL, message_content TEXT NOT NULL, 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.close() @@ -716,3 +743,232 @@ def delete_welcome_message(guild_id: int): c.execute("DELETE FROM welcome_messages WHERE guild_id = ?", (guild_id,)) conn.commit() 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() diff --git a/panel/main.py b/panel/main.py index a59fd81..062e78c 100644 --- a/panel/main.py +++ b/panel/main.py @@ -8,9 +8,14 @@ from fastapi.templating import Jinja2Templates from fastapi import Request from fastapi.responses import RedirectResponse from pydantic import BaseModel +from dotenv import load_dotenv from passlib.hash import pbkdf2_sha256 as hasher 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 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) +@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__": import uvicorn web_config = get_web_config() diff --git a/panel/templates/config.html b/panel/templates/config.html index a978a9f..71264af 100644 --- a/panel/templates/config.html +++ b/panel/templates/config.html @@ -1,4 +1,5 @@ {% set lang = request.cookies.get('panel_lang', 'es') %} +{% set is_admin = username == 'nickpons666' %}
@@ -48,17 +49,17 @@{{ "Administrar canales habilitados para traducción" | translate(lang) }}
+ {{ "Administrar" | translate(lang) }} +{{ "No hay canales en este servidor." | translate(lang) }}
+ {% else %} + + {% endif %} +