from __future__ import annotations # Should be the very first line
from dotenv import load_dotenv
load_dotenv() # Load environment variables from .env file at the very beginning
import asyncio
import tomllib # Added import for tomllib (Python 3.11+)
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
filters,
CallbackQueryHandler,
)
from telegram.ext import ContextTypes
from config import telegram, logging_cfg, prometheus_cfg
from core.dependency_injection import initialize_dependencies
from llm.async_client import AsyncLLMClient
from sheets.async_client import AsyncSheetsManager
from scheduler.tasks import Scheduler
from handlers.common import (
start_handler,
help_handler,
cancel_handler,
reset_handler,
unknown_handler,
confirm_reset,
cancel_reset,
)
from handlers.goal_setting import build_setgoal_conv
from handlers.task_management import get_task_handlers
from handlers.goals import get_goals_handlers
from utils.logging import setup_logging
from core.exceptions import BotError
from utils.sentry_integration import setup_sentry
from prometheus_client import start_http_server # For metrics
from core.metrics import APP_INFO # For app version metric
DEFAULT_ERROR_TEXT = "❌ Произошла ошибка. Попробуйте позже или обратитесь в поддержку."
# ---------------------------------------------------------------------------
# PTB >=20.9 includes a fix for the __polling_cleanup_cb slot, no extra patch needed.
# ---------------------------------------------------------------------------
# ---------------------------------
# Logging setup
# ---------------------------------
logger = setup_logging(logging_cfg.level)
[документация]
async def error_handler(update, context: ContextTypes.DEFAULT_TYPE):
"""Global error handler for the Telegram bot application.
Logs the error and attempts to send a user-friendly message to the chat
where the error occurred.
Args:
update: The Telegram Update that caused the error.
context: The PTB context, containing the error in `context.error`.
"""
error = context.error
logger.error("Error processing update=%s", update, exc_info=error)
# Friendly message to the user
if isinstance(error, BotError):
user_msg = error.user_friendly
else:
user_msg = DEFAULT_ERROR_TEXT # Use constant
if update and update.effective_chat:
try:
await context.bot.send_message(update.effective_chat.id, user_msg)
except Exception: # noqa: BLE001
pass
[документация]
async def main_async():
"""Main async entry point for the Telegram bot application."""
# Initialize Sentry (if configured)
setup_sentry()
# Token check
if not telegram.token:
logger.error("TELEGRAM_BOT_TOKEN is not set") # Translated log
return
# Initialize dependencies (DI container)
sheets_client = AsyncSheetsManager()
llm_client = AsyncLLMClient()
initialize_dependencies(sheets_client, llm_client)
# Create Telegram Application
application = Application.builder().token(telegram.token).build()
# Create scheduler with current event loop (for backward compatibility)
loop = asyncio.get_running_loop()
scheduler = Scheduler(sheets_client, llm_client, event_loop=loop)
# Register handlers
application.add_handler(CommandHandler("help", help_handler))
application.add_handler(CommandHandler("cancel", cancel_handler))
application.add_handler(CommandHandler("reset", reset_handler))
# /start depends on scheduler
application.add_handler(start_handler(scheduler))
# Reset confirmation handlers (must be before goals handlers)
application.add_handler(
CallbackQueryHandler(confirm_reset, pattern="^confirm_reset$")
)
application.add_handler(
CallbackQueryHandler(cancel_reset, pattern="^cancel_reset$")
)
# Multi-goal handlers
for handler in get_goals_handlers():
application.add_handler(handler)
# Task management handlers
for handler in get_task_handlers():
application.add_handler(handler)
# Legacy /setgoal conversation (updated for new architecture)
application.add_handler(build_setgoal_conv())
# Unknown commands - filter all except known ones
known_cmds = r"^(\/)(start|help|setgoal|my_goals|add_goal|today|motivation|status|check|cancel|reset)(?:@\w+)?"
unknown_cmd_filter = filters.Command() & ~filters.Regex(known_cmds)
application.add_handler(MessageHandler(unknown_cmd_filter, unknown_handler))
# Set error handler
application.add_error_handler(error_handler)
# Initialize application
await application.initialize()
# Start Prometheus metrics server
start_http_server(prometheus_cfg.port)
logger.info(
"Prometheus metrics available", port=prometheus_cfg.port, path="/metrics"
)
# Start scheduler AFTER application is initialized
scheduler.start()
# Get version from pyproject.toml
try:
# Python 3.11+ uses tomllib, which expects a binary file stream for load.
with open("pyproject.toml", "rb") as f:
pyproject = tomllib.load(f)
version = pyproject.get("project", {}).get("version", "unknown")
except Exception: # Broad exception for file not found, parse error, etc.
version = "0.2.3" # Fallback version
logger.info("Bot started with multi-goal support", version=version)
APP_INFO.labels(version=version).set(1) # Set version metric
# Start the bot
await application.start()
await application.updater.start_polling()
# Keep the application running
await asyncio.Event().wait()
[документация]
def main():
"""Main entry point that creates and runs the event loop."""
try:
asyncio.run(main_async())
except KeyboardInterrupt:
logger.info("Bot stopped by user")
if __name__ == "__main__":
main()