Files
roa2web-service-auto/reports-app/telegram-bot/app/main.py
Marius Mutu 20f6c52785 Load .env before app imports to ensure environment variables are available
Move load_dotenv() call before all app.* imports in main.py to fix the
import-time environment variable loading issue.

Problem:
- Previously, load_dotenv() was called AFTER importing app.* modules
- When app.api.client was imported, it read BACKEND_URL at import time
- At that moment, .env was not yet loaded, so it used the hardcoded default
- This caused the bot to connect to wrong backend port even when .env was
  correctly configured

Solution:
- Move load_dotenv() to line 20-21, immediately after standard library imports
- This ensures .env is loaded BEFORE any app.* modules are imported
- Now all modules see the correct environment variables from .env file
- Also updated BACKEND_URL default from 8001 to 8000 for consistency

Flow now:
1. Import standard libraries (os, Path, dotenv, etc.)
2. Load .env file (line 20-21) 
3. Import app.* modules (which can now read env vars correctly) 
4. Import telegram-python-bot and other dependencies
5. Start application

This follows Python best practices for environment variable loading and
ensures reliable configuration loading in Windows Service deployments.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 00:43:57 +02:00

297 lines
10 KiB
Python

"""
Main entry point for ROA2WEB Telegram Bot
This bot provides access to the ROA2WEB ERP system through Telegram
using direct command handlers for financial data queries.
"""
import asyncio
import logging
import os
from pathlib import Path
from dotenv import load_dotenv
import uvicorn
from threading import Thread
# ============================================================================
# LOAD ENVIRONMENT VARIABLES FIRST - BEFORE ANY APP IMPORTS
# ============================================================================
# This ensures all modules can access environment variables at import time
env_path = Path(__file__).parent.parent / '.env'
load_dotenv(env_path)
# Telegram imports
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
MessageHandler,
filters
)
# Import database initialization
from app.db import init_database, cleanup_expired_codes, cleanup_expired_sessions
# Import bot handlers
from app.bot.handlers import (
start_command,
help_command,
clear_command,
companies_command,
unlink_command,
selectcompany_command,
dashboard_command,
sold_command,
facturi_command,
trezorerie_command,
# FAZA 3: New command handlers with button interface
menu_command,
trezorerie_casa_command,
trezorerie_banca_command,
clienti_command,
furnizori_command,
evolutie_command,
# Text message handlers
handle_text_message,
# FAZA 4: Callback and error handlers
button_callback,
error_handler
)
# Import internal API
from app.internal_api import internal_api
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Environment variables (already loaded above)
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
BACKEND_URL = os.getenv('BACKEND_URL', 'http://localhost:8000')
INTERNAL_API_PORT = int(os.getenv('INTERNAL_API_PORT', '8002'))
# ============================================================================
# TELEGRAM BOT SETUP
# ============================================================================
def create_telegram_application() -> Application:
"""
Create and configure the Telegram bot application.
Returns:
Application: Configured Telegram application
"""
logger.info("Creating Telegram application...")
# Create application
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
# Register essential command handlers
application.add_handler(CommandHandler("start", start_command))
application.add_handler(CommandHandler("menu", menu_command))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("unlink", unlink_command))
# =========================================================================
# LEGACY COMMAND HANDLERS (kept for backwards compatibility, hidden from help)
# =========================================================================
# NOTE: These commands are redundant with the button interface.
# They're kept for users who already know them, but we push buttons in help.
# Consider removing completely if migration is successful.
application.add_handler(CommandHandler("clear", clear_command))
application.add_handler(CommandHandler("companies", companies_command))
application.add_handler(CommandHandler("selectcompany", selectcompany_command))
application.add_handler(CommandHandler("dashboard", dashboard_command))
application.add_handler(CommandHandler("sold", sold_command))
application.add_handler(CommandHandler("facturi", facturi_command))
application.add_handler(CommandHandler("trezorerie", trezorerie_command))
application.add_handler(CommandHandler("trezorerie_casa", trezorerie_casa_command))
application.add_handler(CommandHandler("trezorerie_banca", trezorerie_banca_command))
application.add_handler(CommandHandler("clienti", clienti_command))
application.add_handler(CommandHandler("furnizori", furnizori_command))
application.add_handler(CommandHandler("evolutie", evolutie_command))
# Text message handler (for direct code input and future NLP)
# IMPORTANT: This must be registered BEFORE CallbackQueryHandler
# filters.TEXT & ~filters.COMMAND ensures we only process non-command text messages
application.add_handler(MessageHandler(
filters.TEXT & ~filters.COMMAND,
handle_text_message
))
# FAZA 4: Register callback query handler (for inline buttons)
application.add_handler(CallbackQueryHandler(button_callback))
# Register error handler
application.add_error_handler(error_handler)
logger.info("Telegram application configured with all handlers")
return application
# ============================================================================
# INTERNAL API SERVER
# ============================================================================
def run_internal_api():
"""
Run the internal FastAPI server in a separate thread.
This API handles communication from the backend (saving auth codes).
"""
logger.info(f"Starting internal API on port {INTERNAL_API_PORT}...")
uvicorn.run(
internal_api,
host="0.0.0.0",
port=INTERNAL_API_PORT,
log_level="info"
)
# ============================================================================
# STARTUP/SHUTDOWN
# ============================================================================
async def startup():
"""
Initialize the bot application on startup.
"""
logger.info("🚀 ROA2WEB Telegram Bot - Starting up...")
# Initialize database
try:
logger.info("Initializing SQLite database...")
await init_database()
logger.info("✅ Database initialized successfully")
except Exception as e:
logger.error(f"❌ Failed to initialize database: {e}")
raise
# Cleanup expired data
try:
logger.info("Cleaning up expired data...")
expired_codes = await cleanup_expired_codes()
expired_sessions = await cleanup_expired_sessions()
logger.info(f"✅ Cleanup complete: {expired_codes} codes, {expired_sessions} sessions removed")
except Exception as e:
logger.warning(f"⚠️ Cleanup failed (non-critical): {e}")
logger.info("✅ Startup complete")
async def shutdown():
"""
Clean up resources on shutdown.
"""
logger.info("👋 ROA2WEB Telegram Bot - Shutting down...")
logger.info("✅ Shutdown complete")
async def scheduled_cleanup():
"""
Background task to periodically clean up expired data.
Runs every hour to remove expired auth codes and sessions.
"""
while True:
try:
await asyncio.sleep(3600) # Sleep for 1 hour
logger.info("🧹 Running scheduled cleanup...")
expired_codes = await cleanup_expired_codes()
expired_sessions = await cleanup_expired_sessions()
logger.info(f"✅ Scheduled cleanup: {expired_codes} codes, {expired_sessions} sessions removed")
except Exception as e:
logger.error(f"❌ Error in scheduled cleanup: {e}")
# ============================================================================
# MAIN APPLICATION
# ============================================================================
async def main():
"""
Main application entry point.
Runs both the Telegram bot and internal API server concurrently.
"""
try:
# Run startup
await startup()
# Create Telegram application
telegram_app = create_telegram_application()
# Start internal API in a separate thread
api_thread = Thread(target=run_internal_api, daemon=True)
api_thread.start()
logger.info(f"✅ Internal API started on port {INTERNAL_API_PORT}")
# Start scheduled cleanup task in background
cleanup_task = asyncio.create_task(scheduled_cleanup())
logger.info("✅ Scheduled cleanup task started")
# Initialize and start Telegram bot
logger.info("🤖 Starting Telegram bot polling...")
await telegram_app.initialize()
await telegram_app.start()
await telegram_app.updater.start_polling(drop_pending_updates=True)
logger.info("✅ Telegram bot is now running and polling for updates")
logger.info(f"📱 Bot ready to receive messages at @{(await telegram_app.bot.get_me()).username}")
logger.info("🎯 Bot is operational with direct command handlers!")
# Keep running until interrupted
await asyncio.Event().wait()
except KeyboardInterrupt:
logger.info("⚠️ Received interrupt signal")
except Exception as e:
logger.error(f"❌ Fatal error: {e}", exc_info=True)
raise
finally:
# Stop Telegram bot gracefully
try:
if 'telegram_app' in locals():
logger.info("Stopping Telegram bot...")
await telegram_app.updater.stop()
await telegram_app.stop()
await telegram_app.shutdown()
logger.info("✅ Telegram bot stopped")
except Exception as e:
logger.error(f"Error stopping Telegram bot: {e}")
await shutdown()
# ============================================================================
# ENTRY POINT
# ============================================================================
if __name__ == "__main__":
# Check required environment variables
if not os.getenv('TELEGRAM_BOT_TOKEN'):
logger.error("❌ TELEGRAM_BOT_TOKEN is required")
logger.error("Please set it in .env file")
exit(1)
# Display startup banner
logger.info("=" * 60)
logger.info(" ROA2WEB TELEGRAM BOT")
logger.info(" Financial ERP Assistant with Direct Commands")
logger.info("=" * 60)
# Run the main application
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("👋 Application stopped by user")
except Exception as e:
logger.error(f"❌ Application failed: {e}", exc_info=True)
exit(1)