feat: Migrate to ultrathin monolith architecture
Consolidate 3 separate applications (reports-app, data-entry-app, telegram-bot) into a unified
architecture with single backend and frontend:
Backend Changes:
- Unified FastAPI backend at backend/ with modular structure
- Modules: reports, data_entry, telegram in backend/modules/
- Centralized config.py and main.py with all routers registered
- Single worker mode (--workers 1) for Telegram bot compatibility
- Shared Oracle connection pool and JWT authentication
- Unified requirements.txt and environment configuration
Frontend Changes:
- Single Vue.js SPA with module-based routing
- Unified frontend at src/ with modules in src/modules/{reports,data-entry}/
- Shared components and stores in src/shared/
- Error boundaries for module isolation
- Dual API proxy in Vite for module communication
Infrastructure:
- New unified startup scripts: start-prod.sh, start-test.sh, start-backend.sh
- Environment templates: .env.dev.example, .env.test.example, .env.prod.example
- Updated deployment scripts for Windows IIS
- Simplified SSH tunnel management
Documentation:
- Comprehensive CLAUDE.md with architecture overview
- Module-specific docs in docs/{data-entry,telegram}/
- Architecture decision records in docs/ARCHITECTURE-DECISIONS.md
- Deployment guides consolidated in deployment/windows/docs/
This migration reduces complexity, improves maintainability, and enables easier
deployment while maintaining all existing functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
316
backend/modules/telegram/bot_main.py
Normal file
316
backend/modules/telegram/bot_main.py
Normal file
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
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 backend.modules.telegram.db import (
|
||||
init_database,
|
||||
cleanup_expired_codes,
|
||||
cleanup_expired_sessions,
|
||||
cleanup_expired_email_codes
|
||||
)
|
||||
|
||||
# Import bot handlers
|
||||
from backend.modules.telegram.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 email authentication handler
|
||||
from backend.modules.telegram.bot.email_handlers import email_login_handler
|
||||
|
||||
# Import internal API
|
||||
from backend.modules.telegram.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 email authentication conversation handler (must be before other handlers)
|
||||
application.add_handler(email_login_handler)
|
||||
|
||||
# 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()
|
||||
expired_email_codes = await cleanup_expired_email_codes()
|
||||
logger.info(f"✅ Cleanup complete: {expired_codes} codes, {expired_sessions} sessions, {expired_email_codes} email codes 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, sessions, and email codes.
|
||||
"""
|
||||
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()
|
||||
expired_email_codes = await cleanup_expired_email_codes()
|
||||
logger.info(f"✅ Scheduled cleanup: {expired_codes} codes, {expired_sessions} sessions, {expired_email_codes} email codes 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)
|
||||
Reference in New Issue
Block a user