import sqlite3 import mysql.connector import json from mysql.connector import Error as MySQLError from botdiscord.config import get_db_config, get_db_type _connection = None def get_connection(): global _connection db_type = get_db_type() if db_type == "mysql": db_config = get_db_config() # Si no hay conexión o no está activa, intentamos conectar/reconectar if _connection is None: try: _connection = mysql.connector.connect( host=db_config.get("host"), port=db_config.get("port"), user=db_config.get("user"), password=db_config.get("password"), database=db_config.get("name"), consume_results=True, connection_timeout=5 ) except MySQLError as e: print(f"CRITICAL: Could not establish initial MySQL connection: {e}") raise # Verificar salud de la conexión existente try: _connection.ping(reconnect=True, attempts=3, delay=1) except MySQLError: try: _connection = mysql.connector.connect( host=db_config.get("host"), port=db_config.get("port"), user=db_config.get("user"), password=db_config.get("password"), database=db_config.get("name"), consume_results=True, connection_timeout=5 ) except MySQLError as e: print(f"CRITICAL: MySQL reconnection failed: {e}") raise return _connection else: import os from botdiscord.config import get_db_path db_path = get_db_path() db_dir = os.path.dirname(db_path) if db_dir and not os.path.exists(db_dir): os.makedirs(db_dir, exist_ok=True) return sqlite3.connect(db_path) def close_connection(): global _connection if _connection and _connection.is_connected(): _connection.close() _connection = None def _execute_query(query, params=None, fetch=False): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() try: cursor.execute(query, params or ()) if fetch: result = cursor.fetchall() else: conn.commit() result = cursor.lastrowid return result finally: cursor.close() else: conn = get_connection() cursor = conn.cursor() try: cursor.execute(query, params or ()) if fetch: result = cursor.fetchall() else: conn.commit() result = cursor.lastrowid return result finally: cursor.close() def init_db(): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute('''CREATE TABLE IF NOT EXISTS guild_languages (guild_id BIGINT NOT NULL, lang_code VARCHAR(10) NOT NULL, PRIMARY KEY (guild_id, lang_code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') cursor.execute('''CREATE TABLE IF NOT EXISTS bot_config (`key` VARCHAR(255) NOT NULL, value TEXT, PRIMARY KEY (`key`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') cursor.execute('''CREATE TABLE IF NOT EXISTS available_languages (code VARCHAR(10) NOT NULL, name VARCHAR(100) NOT NULL, flag VARCHAR(20), PRIMARY KEY (code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') cursor.execute('''CREATE TABLE IF NOT EXISTS bot_languages (bot_type VARCHAR(50) NOT NULL, lang_code VARCHAR(10) NOT NULL, PRIMARY KEY (bot_type, lang_code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') # Nuevas tablas para caché y persistencia cursor.execute('''CREATE TABLE IF NOT EXISTS messages (message_id BIGINT PRIMARY KEY, guild_id BIGINT, author_id BIGINT, content TEXT NOT NULL, mentions_map JSON, bot_type ENUM('discord', 'telegram') NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') cursor.execute('''CREATE TABLE IF NOT EXISTS translations (id INT AUTO_INCREMENT PRIMARY KEY, message_id BIGINT NOT NULL, target_lang VARCHAR(10) NOT NULL, translated_text TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (message_id) REFERENCES messages(message_id) ON DELETE CASCADE, UNIQUE KEY idx_msg_lang (message_id, target_lang)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') # Tabla para traducciones de la interfaz web cursor.execute('''CREATE TABLE IF NOT EXISTS ui_translations (id INT AUTO_INCREMENT PRIMARY KEY, text_hash VARCHAR(64) NOT NULL, original_text TEXT NOT NULL, target_lang VARCHAR(10) NOT NULL, translated_text TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY idx_ui_hash_lang (text_hash, target_lang)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') # Migración automática: Añadir text_hash si no existe try: cursor.execute("SHOW COLUMNS FROM ui_translations LIKE 'text_hash'") if not cursor.fetchone(): print("Migrating ui_translations: Adding text_hash column...") # Primero borramos el índice viejo si existe (basado en el código anterior) try: cursor.execute("ALTER TABLE ui_translations DROP INDEX idx_ui_lang") except: pass # Añadimos la columna y el nuevo índice cursor.execute("ALTER TABLE ui_translations ADD COLUMN text_hash VARCHAR(64) NOT NULL AFTER id") cursor.execute("ALTER TABLE ui_translations ADD UNIQUE KEY idx_ui_hash_lang (text_hash, target_lang)") # Limpiamos la tabla para evitar conflictos de hash con datos viejos cursor.execute("DELETE FROM ui_translations") except Exception as e: print(f"Migration warning: {e}") conn.commit() # Tabla para administradores del panel web cursor.execute('''CREATE TABLE IF NOT EXISTS admins (id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS guild_languages (guild_id INTEGER, lang_code TEXT, PRIMARY KEY (guild_id, lang_code))''') c.execute('''CREATE TABLE IF NOT EXISTS bot_config (key TEXT PRIMARY KEY, value TEXT)''') c.execute('''CREATE TABLE IF NOT EXISTS available_languages (code TEXT PRIMARY KEY, name TEXT, flag TEXT)''') c.execute('''CREATE TABLE IF NOT EXISTS bot_languages (bot_type TEXT, lang_code TEXT, PRIMARY KEY (bot_type, lang_code))''') # SQLite equivalents c.execute('''CREATE TABLE IF NOT EXISTS messages (message_id INTEGER PRIMARY KEY, guild_id INTEGER, author_id INTEGER, content TEXT NOT NULL, mentions_map TEXT, bot_type TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') c.execute('''CREATE TABLE IF NOT EXISTS translations (id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER NOT NULL, target_lang TEXT NOT NULL, translated_text TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(message_id, target_lang))''') # SQLite equivalent for UI translations c.execute('''CREATE TABLE IF NOT EXISTS ui_translations (id INTEGER PRIMARY KEY AUTOINCREMENT, original_text TEXT NOT NULL, target_lang TEXT NOT NULL, translated_text TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(original_text, target_lang))''') # SQLite equivalent for admins c.execute('''CREATE TABLE IF NOT EXISTS admins (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') conn.commit() conn.close() def set_available_languages(languages: list): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("DELETE FROM available_languages") for lang in languages: cursor.execute("INSERT INTO available_languages (code, name, flag) VALUES (%s, %s, %s)", (lang.get("code"), lang.get("name"), lang.get("flag", ""))) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("DELETE FROM available_languages") for lang in languages: c.execute("INSERT OR REPLACE INTO available_languages (code, name, flag) VALUES (?, ?, ?)", (lang.get("code"), lang.get("name"), lang.get("flag", ""))) conn.commit() conn.close() def get_available_languages() -> list: db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("SELECT code, name, flag FROM available_languages ORDER BY name") rows = cursor.fetchall() cursor.close() return [{"code": row[0], "name": row[1], "flag": row[2] or ""} for row in rows] else: conn = get_connection() c = conn.cursor() c.execute("SELECT code, name, flag FROM available_languages ORDER BY name") langs = [{"code": row[0], "name": row[1], "flag": row[2] or ""} for row in c.fetchall()] conn.close() return langs def set_bot_languages(bot_type: str, lang_codes: list): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("DELETE FROM bot_languages WHERE bot_type = %s", (bot_type,)) for code in lang_codes: cursor.execute("INSERT INTO bot_languages (bot_type, lang_code) VALUES (%s, %s)", (bot_type, code)) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("DELETE FROM bot_languages WHERE bot_type = ?", (bot_type,)) for code in lang_codes: c.execute("INSERT INTO bot_languages (bot_type, lang_code) VALUES (?, ?)", (bot_type, code)) conn.commit() conn.close() def get_bot_languages(bot_type: str) -> list: db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("SELECT lang_code FROM bot_languages WHERE bot_type = %s", (bot_type,)) rows = cursor.fetchall() cursor.close() return [row[0] for row in rows] else: conn = get_connection() c = conn.cursor() c.execute("SELECT lang_code FROM bot_languages WHERE bot_type = ?", (bot_type,)) langs = [row[0] for row in c.fetchall()] conn.close() return langs def get_active_languages(guild_id: int) -> list: db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("SELECT lang_code FROM guild_languages WHERE guild_id = %s", (guild_id,)) rows = cursor.fetchall() cursor.close() return [row[0] for row in rows] else: conn = get_connection() c = conn.cursor() c.execute("SELECT lang_code FROM guild_languages WHERE guild_id = ?", (guild_id,)) langs = [row[0] for row in c.fetchall()] conn.close() return langs def set_active_languages(guild_id: int, lang_codes: list): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("DELETE FROM guild_languages WHERE guild_id = %s", (guild_id,)) for code in lang_codes: cursor.execute("INSERT INTO guild_languages (guild_id, lang_code) VALUES (%s, %s)", (guild_id, code)) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("DELETE FROM guild_languages WHERE guild_id = ?", (guild_id,)) for code in lang_codes: c.execute("INSERT INTO guild_languages (guild_id, lang_code) VALUES (?, ?)", (guild_id, code)) conn.commit() conn.close() def get_config_value(key: str) -> str: db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("SELECT value FROM bot_config WHERE `key` = %s", (key,)) row = cursor.fetchone() cursor.close() return row[0] if row else None else: conn = get_connection() c = conn.cursor() c.execute("SELECT value FROM bot_config WHERE key = ?", (key,)) row = c.fetchone() conn.close() return row[0] if row else None def set_config_value(key: str, value: str): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("INSERT INTO bot_config (`key`, value) VALUES (%s, %s) ON DUPLICATE KEY UPDATE value = %s", (key, value, value)) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("INSERT OR REPLACE INTO bot_config (key, value) VALUES (?, ?)", (key, value)) conn.commit() conn.close() def save_message(message_id: int, guild_id: int, author_id: int, content: str, mentions_map: dict, bot_type: str): db_type = get_db_type() mentions_json = json.dumps(mentions_map) if db_type == "mysql": conn = get_connection() cursor = conn.cursor() query = """INSERT INTO messages (message_id, guild_id, author_id, content, mentions_map, bot_type) VALUES (%s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE content = %s, mentions_map = %s""" cursor.execute(query, (message_id, guild_id, author_id, content, mentions_json, bot_type, content, mentions_json)) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("INSERT OR REPLACE INTO messages (message_id, guild_id, author_id, content, mentions_map, bot_type) VALUES (?, ?, ?, ?, ?, ?)", (message_id, guild_id, author_id, content, mentions_json, bot_type)) conn.commit() conn.close() def get_message(message_id: int): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor(dictionary=True) cursor.execute("SELECT content, mentions_map, bot_type FROM messages WHERE message_id = %s", (message_id,)) row = cursor.fetchone() cursor.close() if row: if row['mentions_map']: row['mentions_map'] = json.loads(row['mentions_map']) return row else: conn = get_connection() c = conn.cursor() c.execute("SELECT content, mentions_map, bot_type FROM messages WHERE message_id = ?", (message_id,)) row = c.fetchone() conn.close() if row: return { 'content': row[0], 'mentions_map': json.loads(row[1]) if row[1] else {}, 'bot_type': row[2] } return None def save_translation(message_id: int, target_lang: str, translated_text: str): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() query = "INSERT INTO translations (message_id, target_lang, translated_text) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE translated_text = %s" cursor.execute(query, (message_id, target_lang, translated_text, translated_text)) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("INSERT OR REPLACE INTO translations (message_id, target_lang, translated_text) VALUES (?, ?, ?)", (message_id, target_lang, translated_text)) conn.commit() conn.close() def get_cached_translation(message_id: int, target_lang: str) -> str: db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("SELECT translated_text FROM translations WHERE message_id = %s AND target_lang = %s", (message_id, target_lang)) row = cursor.fetchone() cursor.close() return row[0] if row else None else: conn = get_connection() c = conn.cursor() c.execute("SELECT translated_text FROM translations WHERE message_id = ? AND target_lang = ?", (message_id, target_lang)) row = c.fetchone() conn.close() return row[0] if row else None import hashlib def _normalize_text(text: str) -> str: """Normaliza el texto para que el caché sea consistente.""" if not text: return "" # Quitar espacios en extremos, normalizar saltos de línea y quitar espacios dobles text = text.strip().replace('\r\n', '\n') return " ".join(text.split()) def _get_text_hash(text: str) -> str: """Genera un hash único para el texto normalizado.""" norm = _normalize_text(text) return hashlib.sha256(norm.encode('utf-8')).hexdigest() def get_ui_translation(text: str, target_lang: str) -> str: db_type = get_db_type() text_hash = _get_text_hash(text) try: if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("SELECT translated_text FROM ui_translations WHERE text_hash = %s AND target_lang = %s", (text_hash, target_lang)) row = cursor.fetchone() cursor.close() return row[0] if row else None else: conn = get_connection() c = conn.cursor() c.execute("SELECT translated_text FROM ui_translations WHERE text_hash = ? AND target_lang = ?", (text_hash, target_lang)) row = c.fetchone() conn.close() return row[0] if row else None except Exception as e: print(f"Database error in get_ui_translation: {e}") return None def save_ui_translation(text: str, target_lang: str, translated_text: str): db_type = get_db_type() text_hash = _get_text_hash(text) try: if db_type == "mysql": conn = get_connection() cursor = conn.cursor() query = """INSERT INTO ui_translations (text_hash, original_text, target_lang, translated_text) VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE translated_text = %s""" cursor.execute(query, (text_hash, text, target_lang, translated_text, translated_text)) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("INSERT OR REPLACE INTO ui_translations (text_hash, original_text, target_lang, translated_text) VALUES (?, ?, ?, ?)", (text_hash, text, target_lang, translated_text)) conn.commit() conn.close() except Exception as e: print(f"Database error in save_ui_translation: {e}") # Funciones para administradores def get_admins(): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor(dictionary=True) cursor.execute("SELECT id, username, created_at FROM admins ORDER BY username") rows = cursor.fetchall() cursor.close() return rows else: conn = get_connection() c = conn.cursor() c.execute("SELECT id, username, created_at FROM admins ORDER BY username") rows = [{"id": r[0], "username": r[1], "created_at": r[2]} for r in c.fetchall()] conn.close() return rows def get_admin_by_username(username: str): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor(dictionary=True) cursor.execute("SELECT * FROM admins WHERE username = %s", (username,)) row = cursor.fetchone() cursor.close() return row else: conn = get_connection() c = conn.cursor() c.execute("SELECT id, username, password_hash, created_at FROM admins WHERE username = ?", (username,)) row = c.fetchone() conn.close() if row: return {"id": row[0], "username": row[1], "password_hash": row[2], "created_at": row[3]} return None def add_admin(username: str, password_hash: str): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("INSERT INTO admins (username, password_hash) VALUES (%s, %s)", (username, password_hash)) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("INSERT INTO admins (username, password_hash) VALUES (?, ?)", (username, password_hash)) conn.commit() conn.close() def delete_admin(admin_id: int): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("DELETE FROM admins WHERE id = %s", (admin_id,)) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("DELETE FROM admins WHERE id = ?", (admin_id,)) conn.commit() conn.close() def update_admin_password(admin_id: int, password_hash: str): db_type = get_db_type() if db_type == "mysql": conn = get_connection() cursor = conn.cursor() cursor.execute("UPDATE admins SET password_hash = %s WHERE id = %s", (password_hash, admin_id)) conn.commit() cursor.close() else: conn = get_connection() c = conn.cursor() c.execute("UPDATE admins SET password_hash = ? WHERE id = ?", (password_hash, admin_id)) conn.commit() conn.close()