""" main.py — Entrypoint de Pedrito Hechakuaa. Arranca el bot de Telegram, el poller y el servidor HTTP de health check en paralelo. Al arrancar: aplica migraciones y migra el tenant principal del .env si no existe. """ from __future__ import annotations import asyncio import signal import sys import structlog from telegram.ext import Application import health as health_mod import vault as vault_mod from config import get_settings from database import get_tenant, run_migrations, save_tenant from health import HealthServer, record_poll from onboarding import OnboardingFlow from poller import Poller from telegram_bot import PedritoBot # Configurar structlog structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.TimeStamper(fmt="iso"), structlog.dev.ConsoleRenderer() if sys.stderr.isatty() else structlog.processors.JSONRenderer(), ] ) logger = structlog.get_logger(__name__) async def _migrar_tenant_principal(settings) -> None: """ Migra el tenant principal del .env a la DB si todavía no existe. Permite que el dueño del bot siga funcionando sin re-registrarse. """ chat_id = settings.telegram_chat_id existente = await get_tenant(chat_id) if existente: logger.info("tenant_principal_ya_existe", chat_id=chat_id) return logger.info("migrando_tenant_principal_del_env", chat_id=chat_id) usuario = settings.descifrar_usuario() password = settings.descifrar_password() try: credentials_enc = vault_mod.cifrar_credenciales( chat_id=chat_id, usuario=usuario, password=password, fernet_key=settings.fernet_key, ) finally: del usuario, password await save_tenant( chat_id, nombre_preferido="Admin", tono="cotidiano", timezone="America/Asuncion", credentials_enc=credentials_enc, estado="activo", consentimiento_aceptado=1, ) logger.info("tenant_principal_migrado", chat_id=chat_id) async def main() -> None: settings = get_settings() logger.info( "pedrito_hechakuaa_iniciando", poll_interval_minutes=settings.poll_interval_minutes, horario=f"{settings.poll_hora_inicio}:00-{settings.poll_hora_fin}:00", chat_id=settings.telegram_chat_id, webhook_mode=bool(settings.webhook_url), ) # Migraciones y tenant principal await run_migrations(settings.db_path) await _migrar_tenant_principal(settings) # Bot de Telegram onboarding = OnboardingFlow(settings) pedrito_bot = PedritoBot( token=settings.telegram_bot_token, settings=settings, onboarding=onboarding, ) app: Application = pedrito_bot.build() # Poller poller = Poller( settings=settings, bot=pedrito_bot, on_poll_done=record_poll, ) pedrito_bot.set_notif_callback( lambda chat_id: poller.revisar_ahora(chat_id=chat_id, es_manual=True) ) # Manejo de señales para shutdown limpio loop = asyncio.get_running_loop() health_server: HealthServer | None = None def handle_shutdown(sig: int) -> None: logger.info("shutdown_recibido", signal=sig) poller.detener() loop.stop() for sig in (signal.SIGINT, signal.SIGTERM): try: loop.add_signal_handler(sig, handle_shutdown, sig) except NotImplementedError: pass # Windows no soporta add_signal_handler async with app: await app.initialize() await app.start() logger.info("bot_telegram_iniciado") # Servidor HTTP de health check (y webhook si está configurado) health_server = HealthServer(settings=settings, application=app) await health_server.start() if settings.webhook_url: # Registrar el webhook con Telegram webhook_url = f"{settings.webhook_url.rstrip('/')}/webhook" await app.bot.set_webhook( url=webhook_url, secret_token=settings.webhook_secret or None, drop_pending_updates=True, ) logger.info("webhook_registrado", url=webhook_url) telegram_task = asyncio.create_task(asyncio.sleep(0)) # placeholder else: # Long-polling telegram_task = asyncio.create_task( app.updater.start_polling(drop_pending_updates=True) ) poller_task = asyncio.create_task(poller.run_forever()) logger.info("pedrito_hechakuaa_corriendo") try: if settings.webhook_url: await poller_task else: await asyncio.gather(telegram_task, poller_task) except asyncio.CancelledError: pass finally: if health_server: await health_server.stop() if settings.webhook_url: try: await app.bot.delete_webhook() except Exception: pass else: await app.updater.stop() await app.stop() logger.info("pedrito_hechakuaa_detenido") if __name__ == "__main__": asyncio.run(main())