diff --git a/telegram_bot.py b/telegram_bot.py index 0a66de0..9d420c0 100644 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -10,6 +10,7 @@ Responsabilidades: from __future__ import annotations import re +import unicodedata from datetime import datetime, timezone from typing import TYPE_CHECKING from zoneinfo import ZoneInfo @@ -47,6 +48,41 @@ logger = structlog.get_logger(__name__) PJ_URL = "https://apps.csj.gov.py/gestion-partes/notificaciones-electronicas" +# ────────────────────────────────────────────────────────────────────────────── +# Mapeo de frases libres a comandos (sin LLM) +# ────────────────────────────────────────────────────────────────────────────── + +_FRASES_COMANDOS: dict[str, str] = { + "novedades": "notif", + "actualizaciones": "notif", + "que hay nuevo": "notif", + "qué hay nuevo": "notif", + "hay algo nuevo": "notif", + "alguna novedad": "notif", + "notificaciones": "notif", + "notificacion": "notif", + "notificación": "notif", + "revisar": "notif", + "actualizar": "notif", + "estado": "estado", + "como estas": "estado", + "cómo estás": "estado", + "ayuda": "ayuda", + "help": "ayuda", + "que podes hacer": "ayuda", + "qué podés hacer": "ayuda", + "comandos": "ayuda", +} + + +def _normalizar(texto: str) -> str: + """Lowercase, sin acentos, sin puntuación — para comparación de frases.""" + texto = texto.lower().strip() + texto = unicodedata.normalize("NFD", texto) + texto = "".join(c for c in texto if unicodedata.category(c) != "Mn") + texto = re.sub(r"[^\w\s]", " ", texto) + return re.sub(r"\s+", " ", texto).strip() + # ────────────────────────────────────────────────────────────────────────────── # Helpers de formato # ────────────────────────────────────────────────────────────────────────────── @@ -626,6 +662,45 @@ class PedritoBot: if self._onboarding.is_in_onboarding(chat_id): await self._onboarding.handle_mensaje(update, context) + async def _intentar_mapear_comando( + self, + update: Update, + context: ContextTypes.DEFAULT_TYPE, + texto: str, + ) -> bool: + """ + Intenta mapear texto libre a un comando conocido sin LLM. + Retorna True si encontró match y ejecutó el handler correspondiente. + """ + texto_norm = _normalizar(texto) + texto_lower = texto.lower() + + # Regex especiales sobre texto original (preserva la "/") + m_exp = re.search(r"\bexp(?:ediente)?\b\s+(\d+(?:/\d+)?)", texto_lower) + if m_exp: + context.args = [m_exp.group(1)] # type: ignore[assignment] + await self.cmd_exp(update, context) + return True + + m_pdf = re.search(r"\bpdf\b\s+(\d+/\d+)", texto_lower) + if m_pdf: + context.args = [m_pdf.group(1)] # type: ignore[assignment] + await self.cmd_pdf(update, context) + return True + + # Matching por frases del diccionario + for frase, comando in _FRASES_COMANDOS.items(): + if _normalizar(frase) in texto_norm: + if comando == "notif": + await self.cmd_notif(update, context) + elif comando == "estado": + await self.cmd_estado(update, context) + elif comando == "ayuda": + await self.cmd_ayuda(update, context) + return True + + return False + async def handle_texto_libre(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Responde a texto que no es comando ni está en un flujo de onboarding.""" chat_id = update.effective_chat.id # type: ignore[union-attr] @@ -635,11 +710,16 @@ class PedritoBot: return # el onboarding lo maneja en handle_mensaje, no responder acá tenant = await get_tenant(chat_id) - if tenant and tenant.get("estado") == "activo": - texto = _escape_md(_texto_tono("texto_no_reconocido", tenant)) - await update.effective_message.reply_text(texto, parse_mode=ParseMode.MARKDOWN_V2) # type: ignore[union-attr] - # Usuarios sin registro: silencio total. - # No confirmar que el bot existe ni qué hace — quien lo debe usar ya sabe cómo. + if not (tenant and tenant.get("estado") == "activo"): + # Usuarios sin registro: silencio total. + return + + texto_original = (update.effective_message.text or "") # type: ignore[union-attr] + if await self._intentar_mapear_comando(update, context, texto_original): + return + + texto = _escape_md(_texto_tono("texto_no_reconocido", tenant)) + await update.effective_message.reply_text(texto, parse_mode=ParseMode.MARKDOWN_V2) # type: ignore[union-attr] # ────────────────────────────────────────────────────────────────────────── # Handlers de callbacks