main.py
Rama main · réplica de lectura del repositorio original.
#!/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! 👋")