Ctrl K
ComunidadGuías…

main.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""
Telebrief - Automated Telegram Digest Generator

Main entry point for the application.
Starts the scheduler and bot command handler.
"""

import asyncio
import signal
import sys
from contextlib import suppress

from src.config_loader import load_config
from src.utils import setup_logging
from src.scheduler import DigestScheduler
from src.bot_commands import BotCommandHandler
from src.mcp_server import build_server
from src.setup_wizard import main as run_setup_wizard


class TelebriefApp:
    """Main application controller."""

    def __init__(self):
        """Initialize the application."""
        self.config = None
        self.logger = None
        self.scheduler = None
        self.bot_handler = None
        self.mcp = None
        self.mcp_task = None
        self.shutdown_event = asyncio.Event()

    async def initialize(self):
        """Load configuration and set up components."""
        try:
            # Load configuration
            print("Loading configuration...")
            self.config = load_config()

            # Set up logging
            self.logger = setup_logging(self.config.log_level)
            self.logger.info("=" * 70)
            self.logger.info("🚀 TELEBRIEF STARTING")
            self.logger.info("=" * 70)

            # Display configuration
            self.logger.info(f"Configured channels: {len(self.config.channels)}")
            for ch in self.config.channels:
                self.logger.info(f"  • {ch.name} ({ch.id})")

            self.logger.info(
                f"Schedule: Daily at {self.config.settings.schedule_time} {self.config.settings.timezone}"
            )
            self.logger.info(f"Target user: {self.config.settings.target_user_id}")
            self.logger.info(
                f"AI provider: {self.config.settings.ai_provider}, model: {self.config.settings.ai_model}"
            )

            # Initialize scheduler
            self.logger.info("Initializing scheduler...")
            self.scheduler = DigestScheduler(self.config, self.logger)

            # Initialize bot command handler
            self.logger.info("Initializing bot command handler...")
            self.bot_handler = BotCommandHandler(self.config, self.logger, self.scheduler)
            self.bot_handler.setup_application()

            # Initialize MCP server (optional)
            if self.config.mcp.enabled:
                self.logger.info("Initializing MCP server...")
                self.mcp = build_server(self.config, self.logger)

            self.logger.info("✅ Initialization complete")
            return True

        except FileNotFoundError as e:
            print(f"❌ Configuration error: {e}")
            print("\nPlease ensure:")
            print("1. config.yaml exists and is properly configured")
            print("2. .env file exists with required API credentials")
            print("\nSee .env.example and SPECIFICATION.md for details.")
            return False

        except ValueError as e:
            print(f"❌ Configuration error: {e}")
            return False

        except Exception as e:
            print(f"❌ Initialization failed: {e}")
            import traceback

            traceback.print_exc()
            return False

    async def run(self):
        """Run the application."""
        # Start scheduler
        self.logger.info("Starting scheduler...")
        self.scheduler.start()

        # Start bot
        self.logger.info("Starting bot command handler...")
        await self.bot_handler.run()

        # Start MCP server in the same event loop, so it shares the Telegram session
        if self.mcp:
            mcp_cfg = self.config.mcp
            self.logger.info(
                f"Starting MCP server on http://{mcp_cfg.host}:{mcp_cfg.port}{mcp_cfg.path}"
            )
            self.mcp_task = asyncio.create_task(
                self.mcp.run_streamable_http_async(
                    host=mcp_cfg.host,
                    port=mcp_cfg.port,
                    streamable_http_path=mcp_cfg.path,
                )
            )

        self.logger.info("=" * 70)
        self.logger.info("✅ TELEBRIEF IS RUNNING")
        self.logger.info("=" * 70)
        self.logger.info("Scheduler: Active")
        self.logger.info(f"Next digest: {self.scheduler.get_next_run_time()}")
        self.logger.info("Bot commands: Active")
        if self.mcp_task:
            mcp_cfg = self.config.mcp
            self.logger.info(f"MCP server: http://{mcp_cfg.host}:{mcp_cfg.port}{mcp_cfg.path}")
        self.logger.info("")
        self.logger.info("Available commands in Telegram:")
        self.logger.info("  /digest - Generate digest instantly")
        self.logger.info("  /status - Show status")
        self.logger.info("  /help - Show help")
        self.logger.info("")
        self.logger.info("Press Ctrl+C to stop")
        self.logger.info("=" * 70)

        # Wait for shutdown signal
        await self.shutdown_event.wait()

    async def shutdown(self):
        """Graceful shutdown."""
        self.logger.info("=" * 70)
        self.logger.info("🛑 SHUTTING DOWN TELEBRIEF")
        self.logger.info("=" * 70)

        # Stop scheduler
        if self.scheduler:
            self.logger.info("Stopping scheduler...")
            self.scheduler.stop()

        # Stop bot
        if self.bot_handler:
            self.logger.info("Stopping bot...")
            await self.bot_handler.stop()

        # Stop MCP server
        # ponytail: cancelling the task makes uvicorn log a CancelledError traceback on
        # the way out — cosmetic, right after the line below. Build uvicorn.Server here
        # and flip should_exit instead if that log noise ever matters.
        if self.mcp_task:
            self.logger.info("Stopping MCP server...")
            self.mcp_task.cancel()
            with suppress(asyncio.CancelledError):
                await self.mcp_task

        self.logger.info("✅ Shutdown complete")
        self.logger.info("=" * 70)

        # Signal that shutdown is complete
        self.shutdown_event.set()


async def main():
    """Main entry point."""
    app = TelebriefApp()

    # Initialize
    if not await app.initialize():
        sys.exit(1)

    # Set up signal handlers for graceful shutdown
    loop = asyncio.get_running_loop()
    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.add_signal_handler(sig, lambda: asyncio.create_task(app.shutdown()))

    try:
        # Run application
        await app.run()

    except KeyboardInterrupt:
        pass

    except Exception as e:
        app.logger.error(f"Fatal error: {e}", exc_info=True)
        sys.exit(1)

    finally:
        # Ensure clean shutdown
        if not app.shutdown_event.is_set():
            await app.shutdown()


def run_mcp_stdio() -> int:
    """Serve only the MCP tools over stdio, for clients that launch the server themselves."""
    try:
        config = load_config()
    except (FileNotFoundError, ValueError) as e:
        print(f"❌ Configuration error: {e}", file=sys.stderr)
        return 1
    build_server(config, setup_logging(config.log_level)).run("stdio")
    return 0


if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "init":
        sys.exit(run_setup_wizard(sys.argv[2] if len(sys.argv) > 2 else "."))
    if len(sys.argv) > 1 and sys.argv[1] == "mcp":
        sys.exit(run_mcp_stdio())

    print(
        """
╔══════════════════════════════════════════════════════════╗
║                                                          ║
║   ████████╗███████╗██╗     ███████╗██████╗ ██████╗     ║
║   ╚══██╔══╝██╔════╝██║     ██╔════╝██╔══██╗██╔══██╗    ║
║      ██║   █████╗  ██║     █████╗  ██████╔╝██████╔╝    ║
║      ██║   ██╔══╝  ██║     ██╔══╝  ██╔══██╗██╔══██╗    ║
║      ██║   ███████╗███████╗███████╗██████╔╝██║  ██║    ║
║      ╚═╝   ╚══════╝╚══════╝╚══════╝╚═════╝ ╚═╝  ╚═╝    ║
║                                                          ║
║         Automated Telegram Digest Generator             ║
║                   Powered by AI                          ║
║                                                          ║
╚══════════════════════════════════════════════════════════╝
    """
    )

    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n\nGoodbye! 👋")