Cache System (Backend): - Implemented two-tier hybrid cache: L1 (in-memory) + L2 (SQLite) - L1 cache: Fast dictionary-based with 5-minute TTL for hot data - L2 cache: Persistent SQLite with 1-hour TTL for warm data - Cache decorator with automatic tier management and fallback - Cache key generation with per-user isolation - Event monitoring system for cache statistics - Cache benchmarking utilities for performance testing - Added cache management endpoints: /api/cache/stats, /api/cache/clear, /api/cache/benchmark - Cache configuration via environment variables (CACHE_ENABLED, CACHE_L1_TTL, etc.) Backend Services: - Updated dashboard_service to use @cached decorator with request context - Added cache support to invoice_service and treasury_service - Integrated cache manager into main.py with lifespan events - Added Request parameter to service methods for cache metadata Frontend Enhancements: - New CacheStatsView.vue for real-time cache monitoring dashboard - Cache store (cacheStore.js) for state management - Updated router to include /cache-stats route - Navigation updates in DashboardHeader and HamburgerMenu - Cache stats accessible from main navigation Telegram Bot Improvements: - Enhanced formatters with YTD comparison data - Improved menu navigation and button layout - Better error handling and user feedback - Bot startup improvements with graceful shutdown Auth & Middleware: - Enhanced middleware with cache metadata injection - Improved request state handling for cache source tracking Development: - Updated start-dev.sh with better error handling - Added TELEGRAM_EMAIL_AUTH_PLAN.md documentation - Updated requirements.txt with aiosqlite for async SQLite Performance: - L1 cache provides <1ms response for hot data - L2 cache provides ~5ms response for warm data - Database queries only for cold data or cache misses - Cache hit rates tracked and displayed in real-time 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
304 lines
10 KiB
Python
304 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,
|
|
# FAZA 6: Cache management commands
|
|
clearcache_command,
|
|
togglecache_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))
|
|
|
|
# FAZA 6: Cache management commands
|
|
application.add_handler(CommandHandler("clearcache", clearcache_command))
|
|
application.add_handler(CommandHandler("togglecache", togglecache_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)
|