From 0c0a1811efbabcbfddb2b7fb6a7bd06b6a233b32 Mon Sep 17 00:00:00 2001 From: nickpons666 Date: Fri, 6 Mar 2026 20:32:15 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20implementar=20cach=C3=A9=20de=20traducc?= =?UTF-8?q?iones=20y=20persistencia=20de=20mensajes=20en=20MySQL=20para=20?= =?UTF-8?q?Discord=20y=20Telegram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- botdiscord/bot.py | 5 +- botdiscord/database.py | 123 ++++++++++++++++++++++++++++++++++++ botdiscord/ui.py | 23 ++++++- bottelegram/telegram_bot.py | 43 +++++++++---- 4 files changed, 179 insertions(+), 15 deletions(-) diff --git a/botdiscord/bot.py b/botdiscord/bot.py index 0948b19..70fee3f 100644 --- a/botdiscord/bot.py +++ b/botdiscord/bot.py @@ -8,7 +8,7 @@ from discord import app_commands import re from botdiscord.config import get_discord_token, load_config, get_languages -from botdiscord.database import init_db, get_active_languages, get_bot_languages +from botdiscord.database import init_db, get_active_languages, get_bot_languages, save_message from botdiscord.ui import TranslationView, ConfigView from botdiscord.translate import get_reverse_mapping, load_lang_mappings @@ -93,6 +93,9 @@ async def on_message(message): text_to_translate = mention_pattern.sub(replace_mention, text_escaped) + # Guardamos el mensaje en la base de datos para persistencia y caché + save_message(message.id, message.guild.id, message.author.id, text_to_translate, mentions_map, 'discord') + langs_to_show = active_langs if langs_to_show: diff --git a/botdiscord/database.py b/botdiscord/database.py index bef8edc..0ff184b 100644 --- a/botdiscord/database.py +++ b/botdiscord/database.py @@ -1,5 +1,6 @@ import sqlite3 import mysql.connector +import json from mysql.connector import Error as MySQLError from botdiscord.config import get_db_config, get_db_type @@ -87,6 +88,26 @@ def init_db(): 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''') + conn.commit() cursor.close() else: @@ -100,6 +121,24 @@ def init_db(): (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))''') conn.commit() conn.close() @@ -256,3 +295,87 @@ def set_config_value(key: str, value: str): 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 diff --git a/botdiscord/ui.py b/botdiscord/ui.py index 69f817f..436bd1d 100644 --- a/botdiscord/ui.py +++ b/botdiscord/ui.py @@ -1,5 +1,6 @@ import discord from botdiscord.translate import get_lang_mapping, get_flag_mapping, get_name_to_code, translate_text +from botdiscord.database import get_message, save_translation, get_cached_translation class TranslationView(discord.ui.View): def __init__(self, text: str, languages: list, original_message, attachments=None, mentions_map=None): @@ -31,8 +32,26 @@ class TranslationButton(discord.ui.Button): self.mentions_map = mentions_map or {} async def callback(self, interaction: discord.Interaction): - # Traducimos el texto - translated = await translate_text(self.text, self.lang_code) + # Intentamos recuperar el mensaje de la base de datos si falta contexto + msg_context = None + if not self.text or not self.mentions_map: + # Recuperamos de la BD usando el ID del mensaje al que se responde + # En Discord, si es un reply, el original_message es el mensaje original + msg_id = self.original_message.id + db_msg = get_message(msg_id) + if db_msg: + self.text = db_msg['content'] + self.mentions_map = db_msg['mentions_map'] + + # Verificamos si ya tenemos la traducción en caché + cached = get_cached_translation(self.original_message.id, self.lang_code) + if cached: + translated = cached + else: + # Traducimos el texto + translated = await translate_text(self.text, self.lang_code) + # Guardamos en caché + save_translation(self.original_message.id, self.lang_code, translated) # Desescapamos el HTML para recuperar caracteres especiales import html diff --git a/bottelegram/telegram_bot.py b/bottelegram/telegram_bot.py index f4d428c..c713d83 100644 --- a/bottelegram/telegram_bot.py +++ b/bottelegram/telegram_bot.py @@ -10,7 +10,10 @@ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, ContextTypes, filters from botdiscord.config import load_config, get_telegram_token, get_libretranslate_url -from botdiscord.database import init_db as init_shared_db +from botdiscord.database import ( + init_db as init_shared_db, + save_message, get_message, save_translation, get_cached_translation +) from botdiscord.translate import load_lang_mappings, get_lang_mapping, get_flag_mapping, get_name_to_code load_config() @@ -132,9 +135,8 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): reply_markup=reply_markup ) - with lock: - cleanup_pending_translations() - pending_translations[str(sent_message.message_id)] = text + # Guardamos en MySQL en lugar del JSON local + save_message(sent_message.message_id, update.effective_chat.id, update.effective_user.id, text, {}, 'telegram') async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE): caption = update.message.caption @@ -147,23 +149,40 @@ async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE): parse_mode="Markdown", reply_markup=reply_markup ) - with lock: - cleanup_pending_translations() - pending_translations[str(sent_message.message_id)] = caption + # Guardamos en MySQL + save_message(sent_message.message_id, update.effective_chat.id, update.effective_user.id, caption, {}, 'telegram') async def translation_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() lang_code = query.data.replace("trans_", "") - message_id = str(query.message.message_id) + message_id = query.message.message_id - text = pending_translations.get(message_id) - if not text: - await query.message.reply_text("⚠️ No se encontró el texto original. Es posible que el bot se haya reiniciado recientemente.") + # Intentamos obtener del caché de traducciones primero + cached = get_cached_translation(message_id, lang_code) + + # Recuperamos el mensaje original de MySQL + db_msg = get_message(message_id) + + if not db_msg: + await query.message.reply_text("⚠️ No se encontró el texto original en la base de datos.") return - translated = await translate_text_telegram(text, lang_code) + text = db_msg['content'] + + if cached: + translated = cached + else: + # Traducimos usando la lógica compartida + from botdiscord.translate import translate_text + translated = await translate_text(text, lang_code) + # Guardamos en caché MySQL + save_translation(message_id, lang_code, translated) + + # Desescapamos el HTML para Telegram + import html + translated = html.unescape(translated) lang_mapping = get_lang_mapping("telegram") flag_mapping = get_flag_mapping("telegram")