From e3b1d064427b6eccd1e27142fc4019deccab7c67 Mon Sep 17 00:00:00 2001 From: nickpons666 Date: Sat, 7 Mar 2026 00:59:20 -0600 Subject: [PATCH] =?UTF-8?q?fix(discord):=20soluci=C3=B3n=20definitiva=20de?= =?UTF-8?q?=20persistencia=20con=20registro=20global=20y=20custom=5Fids=20?= =?UTF-8?q?est=C3=A1ticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- botdiscord/bot.py | 106 +++++++++++++++++----------------------------- botdiscord/ui.py | 90 +++++++++++++-------------------------- 2 files changed, 68 insertions(+), 128 deletions(-) diff --git a/botdiscord/bot.py b/botdiscord/bot.py index 4b0737e..02e2539 100644 --- a/botdiscord/bot.py +++ b/botdiscord/bot.py @@ -6,10 +6,11 @@ import discord from discord.ext import commands from discord import app_commands 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 -from botdiscord.ui import TranslationView, ConfigView +from botdiscord.ui import PersistentTranslationView, ConfigView from botdiscord.translate import get_reverse_mapping, load_lang_mappings load_config() @@ -23,117 +24,88 @@ async def on_ready(): init_db() load_lang_mappings("discord") - # Registramos la vista persistente para que los botones funcionen tras reinicios - # Primero obtenemos todos los idiomas habilitados globalmente para crear la vista base - try: - from botdiscord.translate import get_lang_mapping - lang_mapping = get_lang_mapping("discord") - all_langs = list(lang_mapping.keys()) - bot.add_view(TranslationView(all_langs)) - except Exception as e: - print(f"Error registrando vista persistente: {e}") + # Registramos la vista persistente GLOBALMENTE + # Esto asegura que cualquier botón con el custom_id adecuado sea escuchado + bot.add_view(PersistentTranslationView()) print(f"Bot Discord conectado como {bot.user}") try: synced = await bot.tree.sync() print(f"Sincronizados {len(synced)} comandos.") except Exception as e: - print(e) + print(f"Error sync: {e}") def get_active_langs_for_guild(guild_id): - from botdiscord.translate import get_reverse_mapping, get_flag_mapping - + from botdiscord.translate import get_reverse_mapping active = get_active_languages(guild_id) - print(f"[BOT DEBUG] guild_id={guild_id}, active from db: {active}") if not active: active = get_bot_languages("discord") - print(f"[BOT DEBUG] active from bot_languages: {active}") if not active: active = [lang["code"] for lang in get_languages()] - print(f"[BOT DEBUG] active from config: {active}") reverse_mapping = get_reverse_mapping("discord") - print(f"[BOT DEBUG] reverse_mapping: {reverse_mapping}") - - result = [reverse_mapping[l] for l in active if l in reverse_mapping] - print(f"[BOT DEBUG] langs_to_show: {result}") - return result + # Devolvemos códigos para filtrar la vista persistente + return active @bot.event async def on_message(message): - if message.author.bot: - return + if message.author.bot: return text_content = message.content.strip() - has_attachments = len(message.attachments) > 0 - has_embeds = len(message.embeds) > 0 + if not text_content and not message.attachments: return - is_sticker = message.stickers - is_gif_url = text_content.startswith('https://tenor.com/') or text_content.startswith('https://giphy.com/') - is_discord_emoji = re.fullmatch(r'<(a?):[a-zA-Z0-9_]+:[0-9]+>', text_content) - - if is_sticker or is_gif_url or is_discord_emoji: + # Filtros de stickers/emojis/etc + if message.stickers or text_content.startswith('https://tenor.com/') or \ + re.fullmatch(r'<(a?):[a-zA-Z0-9_]+:[0-9]+>', text_content): return - if text_content and len(text_content) < 2: - return - - if not text_content and not has_attachments: - return - - if has_attachments and not text_content: - return + if text_content and len(text_content) < 2: return - active_langs = get_active_langs_for_guild(message.guild.id) - if not active_langs: - return + active_codes = get_active_langs_for_guild(message.guild.id) + if not active_codes: return - import html + # Escapar y procesar menciones text_escaped = html.escape(message.content) - - # Buscamos menciones que ya están escapadas (<@...>) mention_pattern = re.compile(r'<@!?(\d+)>|<@&(\d+)>|<#(\d+)>') mentions_map = {} def replace_mention(match): - # Usamos una etiqueta autocerrada para que el traductor no intente cerrarla él mismo placeholder = f"" - # Guardamos la mención original (sin escapar) para restaurarla luego mentions_map[placeholder] = html.unescape(match.group(0)) return placeholder 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 + # Creamos una vista filtrada basada en la persistente para mostrar solo los botones activos + # Pero los botones mantienen sus custom_ids globales + from botdiscord.ui import TranslationButton + view = discord.ui.View(timeout=None) + + # Cargamos mapeos para etiquetas de botones + from botdiscord.translate import get_name_to_code, get_flag_mapping + name_to_code = get_name_to_code("discord") + flag_mapping = get_flag_mapping("discord") + code_to_name = {v: k for k, v in name_to_code.items()} - if langs_to_show: - print(f"[BOT DEBUG] Creating TranslationView with langs: {langs_to_show}") - view = TranslationView(langs_to_show) - print(f"[BOT DEBUG] View created, sending message...") - try: - await message.reply("¿Traducir este mensaje?", view=view, mention_author=False) - print(f"[BOT DEBUG] Message sent successfully") - except Exception as e: - print(f"[BOT DEBUG] Error sending message: {e}") + for code in active_codes: + name = code_to_name.get(code, code) + flag = flag_mapping.get(code, "") + view.add_item(TranslationButton(name, code, flag)) -@bot.tree.command(name="configurar", description="Configura los idiomas de traducción para este servidor") + try: + await message.reply("¿Traducir este mensaje?", view=view, mention_author=False) + except Exception as e: + print(f"Error enviando reply: {e}") + +@bot.tree.command(name="configurar", description="Configura los idiomas de traducción") @app_commands.checks.has_permissions(administrator=True) async def configurar(interaction: discord.Interaction): view = ConfigView(interaction.guild_id, "discord") - await interaction.response.send_message( - "Selecciona los idiomas que quieres habilitar para los botones de traducción:", - view=view, - ephemeral=True - ) + await interaction.response.send_message("Selecciona idiomas habilitados:", view=view, ephemeral=True) def run_discord_bot(): token = get_discord_token() - if not token or token == "TU_DISCORD_BOT_TOKEN": - print("ERROR: Configura el token de Discord en config.yaml") - return bot.run(token) if __name__ == "__main__": diff --git a/botdiscord/ui.py b/botdiscord/ui.py index 3c4f382..9881a53 100644 --- a/botdiscord/ui.py +++ b/botdiscord/ui.py @@ -1,49 +1,37 @@ import discord +import html +import re 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, languages: list = None): - super().__init__(timeout=None) - - if languages: - flag_mapping = get_flag_mapping() - name_to_code = get_name_to_code() - - for lang in languages: - lang_code = name_to_code.get(lang) - flag = flag_mapping.get(lang_code, "") if lang_code else "" - # El custom_id es vital para la persistencia - custom_id = f"btn_trans_{lang_code}" - self.add_item(TranslationButton(lang, lang_code, flag, custom_id)) - class TranslationButton(discord.ui.Button): - def __init__(self, lang_name: str, lang_code: str, flag: str, custom_id: str): + def __init__(self, lang_name: str, lang_code: str, flag: str): label = flag if flag else lang_name + # custom_id estático: esto es lo que Discord usa para vincular el clic + custom_id = f"btn_trans_{lang_code}" super().__init__(label=label, style=discord.ButtonStyle.primary, custom_id=custom_id) self.lang_code = lang_code async def callback(self, interaction: discord.Interaction): - # 1. Avisamos a Discord que estamos procesando (evita el "Interacción fallida" por timeout) + # Evita el "Interacción fallida" por timeout de 3s await interaction.response.defer() try: if not interaction.message.reference: - await interaction.followup.send("⚠️ No se pudo encontrar la referencia al mensaje original.", ephemeral=True) + await interaction.followup.send("⚠️ No se pudo encontrar el mensaje original.", ephemeral=True) return original_msg_id = interaction.message.reference.message_id db_msg = get_message(original_msg_id) if not db_msg: - # Si no está en MySQL, intentamos ver si el mensaje actual tiene el texto (fallback) await interaction.followup.send("⚠️ Mensaje no encontrado en la base de datos.", ephemeral=True) return text = db_msg['content'] mentions_map = db_msg['mentions_map'] - # Verificamos caché + # Recuperar o traducir cached = get_cached_translation(original_msg_id, self.lang_code) if cached: translated = cached @@ -51,27 +39,30 @@ class TranslationButton(discord.ui.Button): translated = await translate_text(text, self.lang_code) save_translation(original_msg_id, self.lang_code, translated) - # Procesar traducción - import html - import re + # Limpiar texto translated = html.unescape(translated) - if mentions_map: for placeholder, mention in mentions_map.items(): - match = re.search(r'm\d+', placeholder) - if not match: continue - tag_num = match.group() - open_pattern = re.compile(rf'<\s*{tag_num}\s*/?\s*>') - translated = open_pattern.sub(mention, translated) - close_pattern = re.compile(rf'<\s*/\s*{tag_num}\s*>') - translated = close_pattern.sub('', translated) + # El mapa ya viene con el formato : "<@123>" + translated = translated.replace(placeholder, mention) + # Por si el traductor quitó el espacio: + translated = translated.replace(placeholder.replace(" ", ""), mention) - # 2. Usamos edit_original_response porque ya hicimos defer() + # Editar el mensaje que contiene los botones await interaction.edit_original_response(content=translated, view=self.view) except Exception as e: - print(f"[ERROR UI] Error en callback de traducción: {e}") - await interaction.followup.send(f"❌ Error al traducir: {str(e)}", ephemeral=True) + print(f"[ERROR UI] {e}") + await interaction.followup.send(f"❌ Error: {str(e)}", ephemeral=True) + +class PersistentTranslationView(discord.ui.View): + def __init__(self): + super().__init__(timeout=None) + # Cargamos los botones para todos los idiomas habilitados en la config + # Esto asegura que cualquier botón de cualquier idioma sea reconocido + from botdiscord.config import get_languages + for lang in get_languages(): + self.add_item(TranslationButton(lang['name'], lang['code'], lang.get('flag', ''))) class ConfigSelect(discord.ui.Select): def __init__(self, guild_id: int, bot_type: str = "discord"): @@ -82,38 +73,15 @@ class ConfigSelect(discord.ui.Select): options = [] for name, code in lang_mapping.items(): flag = flag_mapping.get(code, "") - if flag: - options.append(discord.SelectOption(label=flag, value=name, default=(code in active))) - else: - options.append(discord.SelectOption(label=name, value=name, default=(code in active))) - super().__init__( - placeholder="Selecciona los idiomas activos...", - min_values=0, - max_values=len(options), - options=options - ) + options.append(discord.SelectOption(label=f"{flag} {name}" if flag else name, value=name, default=(code in active))) + super().__init__(placeholder="Selecciona idiomas...", min_values=0, max_values=len(options), options=options) async def callback(self, interaction: discord.Interaction): from botdiscord.database import set_active_languages - from botdiscord.translate import get_lang_mapping, get_flag_mapping - - guild_id = interaction.guild_id lang_mapping = get_lang_mapping("discord") - flag_mapping = get_flag_mapping("discord") selected_codes = [lang_mapping[val] for val in self.values] - - set_active_languages(guild_id, selected_codes) - - selected_flags = [] - for val in self.values: - code = lang_mapping.get(val) - flag = flag_mapping.get(code, "") if code else "" - selected_flags.append(flag if flag else val) - - await interaction.response.send_message( - f"Configuración actualizada: {', '.join(selected_flags)}", - ephemeral=True - ) + set_active_languages(interaction.guild_id, selected_codes) + await interaction.response.send_message("Configuración actualizada.", ephemeral=True) class ConfigView(discord.ui.View): def __init__(self, guild_id: int, bot_type: str = "discord"):